Login and logout

Description

Login and logout related programming activities in Plone

Introduction

This chapter contains login and logout related code snippets.

Login entry points

There are two login points in Plone

/login view (appended to any content URL) directs you to the page where you came from after the login.

/login_form view does login without the redirect back to the original page.

In addition, the /logout action logs the user out.

The logic that drives the login process is implemented using the CMF form controller framework (legacy). To customize it, you need to override one or more of the login_* scripts. This can be accomplished in two ways: register your own skin directory or use z3c.jbot. Note that in both cases, you need to copy the .metadata file as well.

Extracting credentials

Extracting credentials try to extract log-in (username, password) from HTTP request.

Below is an example how to extract and authenticate the user manually. It is mostly suitable for unit testing. Note that given login field isn’t necessarily the username. For example, betahaus.emaillogin add-on authenticates users by their email addresses.

Credential extraction will go through all plug-ins registered for PlonePAS system.

The first found login/password pair attempt will be used for user authentication.

Unit test example:

def extract_credentials(self, login, password):
    """ Spoof HTTP login attempt.

    Functional test using zope.testbrowser would be
    more appropriate way to test this.
    """

    request  = self.portal.REQUEST

    # Assume publishing process has succeeded and object has been found by traversing
    # (this is usually set by ZPublisher)
    request['PUBLISHED'] = self.portal

    # More ugly ZPublisher stubs
    request['PARENTS'] = [self.portal]
    request.steps = [self.portal]

    # Spoof HTTP request login fields
    request["__ac_name"] = login
    request["__ac_password"] = password

    # Call PluggableAuthService._extractUserIds()
    # which will return a list of user ids  extracted from the request
    plugins = self.portal.acl_users.plugins

    users = self.portal.acl_users._extractUserIds(request, plugins)

    if len(users) == 0:
        return None

    self.assertEqual(len(users), 1)

    # User will be none if the authentication fails
    # or anonymous if there were no credential fields in HTTP request
    return users[0]

Authenticating the user

Using username and password

Authenticating the user will check that username and password are correct.

Pluggable Authentication Service (acl_users under site root) will go through all authentication plug-ins and return the first successful authenticated users.

Read more in PlonePAS.

Unit test example:

def authenticate_using_credentials(self, login, password):

    request = self.portal.REQUEST

    # Will return valid user object
    user = self.portal.acl_users.authenticate(login, password, request)
    self.assertNotEqual(user, None)

Using username only

Useful for sudo style logins.

def loginUser(self, username):
    """
    Login Plone user (without password)
    """
    self.context.acl_users.session._setupSession(username, self.context.REQUEST.RESPONSE)
    self.request.RESPONSE.redirect(self.portal_state.portal_url())

See also

Post-login actions

Post-login actions are executed after a successful login. Post-login actions which you could want to change are

  • Where to redirect the user after login

  • Setting the status message after login

You can use the collective.onlogin package to set up many actions for you.

If you need more control, post-login code can be executed with events defined in PluggableAuthService service.

  • IUserLoggedInEvent

  • IUserInitialLoginInEvent (logs for the first time)

  • IUserLoggedOutEvent

Here is an example how to redirect a user to a custom folder after he/she logs in (overrides standard Plone login behavior)

configure.zcml:

<configure
    xmlns="http://namespaces.zope.org/zope"
    i18n_domain="my.package">

    <subscriber
        for="Products.PluggableAuthService.interfaces.events.IUserLoggedInEvent"
        handler=".postlogin.logged_in_handler"
        />

</configure>

postlogin.py:

# Python imports
import logging

# ZODB imports
from ZODB.POSException import ConflictError

# Zope imports
from AccessControl import getSecurityManager
from zope.interface import Interface
from zope.component import getUtility
from zope.app.component.hooks import getSite
from zope.globalrequest import getRequest

# CMFCore imports
from Products.CMFCore import permissions

# Plone imports
from Products.CMFPlone.interfaces.siteroot import IPloneSiteRoot

# Logger output for this module
logger = logging.getLogger(__name__)

#: Site root relative path where we look for the folder with an edit access
CUSTOM_USER_FOLDERS = "fi/yritykset"


def redirect_to_edit_access_folder(user):
    """
    Redirects the user to a folder he/she has editor access.

    This is for use cases where you have a owned content
    (e.g. company/product data) on a shared site.

    You want to make it simple for the users with limited knowledge to edit their own data
    by redirecting to the edit view right after the login.

    :return: URL if we should redirect, otherwise None
    """

    # Fetch the site related to the current HTTP request
    portal = getSite()

    # Look for portal relative paths where the items are
    try:
        target = portal.unrestrictedTraverse(CUSTOM_USER_FOLDERS)
    except ConflictError:
        # Transaction retries must be
        # always handled specially in exception handlers
        raise
    except Exception, e:
        # Let the login proceed even if the folder has been deleted
        # don't make it impossible to login to the site
        logger.exception(e)
        return None

    # Check if the current user has Editor access
    # in the any items of the folder
    sm = getSecurityManager()

    for obj in target.listFolderContents():
        if sm.checkPermission(permissions.ModifyPortalContent, obj):
            logger.info("Redirecting user %s to %s" % (user, obj))
            return obj.absolute_url() + "/edit"

    logger.warn("User %s did not have his/her own content item in %s" % (user, target))

    # Let the normal login proceed to the page "You are now logged in" etc.
    return None


def logged_in_handler(event):
    """
    Listen to the event and perform the action accordingly.
    """

    user = event.object

    url = redirect_to_edit_access_folder(user)
    if url:
        request = getRequest()
        if request is None:
            # HTTP request is not present e.g.
            # when doing unit testing / calling scripts from command line
            return

        # check if came_from is not empty, then clear it up, otherwise further
        # Plone scripts will override our redirect
        if request.get('came_from', None):
            request['came_from'] = ''
            request.form['came_from'] = ''
        request.RESPONSE.redirect(url)

Post-logout actions

Products.PlonePAS.tools.membership fires Products.PlonePAS.events.UserLoggedOutEvent when the user logs out via Log out menu item.

Note

You cannot catch session timeout events this way… only explicit logout action.

Example ZCML

<subscriber for="Products.PlonePAS.events.UserLoggedOutEvent"
    handler=".smartcard.clear_extra_cookies_on_logout" />

Example Python:

def clear_extra_cookies_on_logout(event):
    """
    Logout event handler.

    When user explicitly logs out from the Logout menu, clear our privileges smartcard cookie.
    """

    # Which cookie we want to clear
    cookie_name = SmartcardHelper.PRIVILEDGED_COOKIE_NAME

    request = event.object.REQUEST
    # YES CAPS LOCK WAS MUST WHEN ZOPE 2 WAS INVENTED
    # SOMEWHERE AROUND NINETIES. THEN IT WAS THE CRUISE
    # CONTROL FOR COOLNESS AND ZOPE IS SOO COOOOOL.
    response = request.RESPONSE
    # Voiding our special cookie on logout
    response.expireCookie(cookie_name)

More info

Entry points to logged in member handling

See Products.PluggableAuthService.PluggableAuthService._extractUserIds(). It will try to extract credentials from incoming HTTP request, using different “extract” plug-ins of PAS framework.

PluggableAuthService is also known as acl_users persistent object in the site root.

For each set of extracted credentials, try to authenticate a user; accumulate a list of the IDs of such users over all our authentication and extraction plugins.

PluggableAuthService may use ZCacheable pattern to see if the user data exists already in the cache, based on any extracted credentials, instead of actually checking whether the credentials are valid or not. PluggableAuthService must be set to have cache end. By default it is not set, but installing LDAP sets it to RAM cache.

More info

PAS cache settings

Here is a short view snippet to set PAS cache state:

from Products.Five.browser import BrowserView
from zope.app.component.hooks import getSite

from Products.CMFCore.utils import getToolByName

class PASCacheController(BrowserView):
    """
    Set PAS caching parameters from browser address bar.
    """

    def getPAS(self):
        site=getSite()
        return getToolByName(site, "acl_users")

    def setPASCache(self, value):
        """
        Enables or disables pluggable authentication service caching.

        The setting is stored persistently in PAS

        This caches credentials for authenticated users after the first login.

        This will make authentication and permission operations little bit faster.
        The downside is that the cache must be purged if you want to remove old values from there.
        (user has been deleted, etc.)

        More info

        * https://github.com/plone/plone.app.ldap/blob/master/plone/app/ldap/ploneldap/util.py

        """

        pas = self.getPAS()

        if value:

            # Enable

            if pas.ZCacheable_getManager() is None:
                pas.ZCacheable_setManagerId(manager_id="RAMCache")

            pas.ZCacheable_setEnabled(True)

        else:
            # Disable
            pas.ZCacheable_setManagerId(None)
            pas.ZCacheable_setEnabled(False)


    def __call__(self):
        """ Serve HTTP GET queries.
        """

        cache_value = self.request.form.get("cache", None)

        if cache_value is None:
            # Output help text
            return "Use: http://localhost/@@pas-cache-controller?cache=true"

        value = (cache_value == "true")

        self.setPASCache(value)

        return "Set value to:" + str(value)

… and related ZCML

<browser:page
 for="Products.CMFCore.interfaces.ISiteRoot"
 name="pas-cache-controller"
 class=".pascache.PASCacheController"
 permission="cmf.ManagePortal"
/>

Login as another user (“sudo”)

If you need to login to production system another user and you do not know the password, there is an add-on product for it

Another option

Getting logged in users

Todo

Was somewhere, but can’t find where.

Locking user account after too many retries

For security reasons, you might want to locking users after too many tries of logins. This protects user accounts against brute force attacks.

Single Sign-On and Active Directory

Plone can be used in a Microsoft Active Directory environment (or standard Kerberos environment) such that users are automatically and transparently authenticated to Plone without requesting credentials from the user.

This is quite an advanced topic and requires some set up on the server, but can be achieved with Plone running on either Unix/Linux or Windows environments.

More details can be found in this presentation from Plone Open Garden 2013:

Preventing duplicate logins from different browsers