Login and logout¶
Login and logout related programming activities in Plone
- Login entry points
- Extracting credentials
- Authenticating the user
- Post-login actions
- Post-logout actions
- Entry points to logged in member handling
- Login as another user ("sudo")
- Getting logged in users
- Locking user account after too many retries
- Hyperlinks to authenticated Plone content in Microsoft Office
- Single Sign-On and Active Directory
- Preventing duplicate logins from different browsers
There are two login points in Plone
view (appended to any content URL) directs you to the page
where you came from after the login.
view does login without the redirect back to the original
In addition, the
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
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
file as well.
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
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)
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())
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.
IUserInitialLoginInEvent(logs for the first time)
Here is an example how to redirect a user to a custom folder after he/she logs in (overrides standard Plone login behavior)
<configure xmlns="http://namespaces.zope.org/zope" i18n_domain="my.package"> <subscriber for="Products.PluggableAuthService.interfaces.events.IUserLoggedInEvent" handler=".postlogin.logged_in_handler" /> </configure>
# 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)
when the user logs out via Log out menu item.
You cannot catch session timeout events this way... only explicit logout action.
<subscriber for="Products.PlonePAS.events.UserLoggedOutEvent" handler=".smartcard.clear_extra_cookies_on_logout" />
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)
Products.PluggableAuthService.PluggableAuthService._extractUserIds(). It will try to extract credentials from incoming HTTP
request, using different "extract" plug-ins of
is also known as
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.
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
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" />
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
For security reasons, you might want to locking users after too many tries of logins. This protects user accounts against brute force attacks.
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: