############################################################################## # # Copyright (c) 2001 Zope Corporation and Contributors. All Rights # Reserved. # # This software is subject to the provisions of the Zope Public License, # Version 2.0 (ZPL). A copy of the ZPL should accompany this # distribution. # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS # FOR A PARTICULAR PURPOSE # ############################################################################## """ Class: CASAuthHelper $Id: CASAuthHelper.py,v 1.4 2004/05/06 19:41:03 tseaver Exp $ """ from base64 import encodestring, decodestring import urllib from zExceptions import Redirect from AccessControl.SecurityInfo import ClassSecurityInfo from OFS.PropertyManager import PropertyManager from App.class_init import default__class_init__ as InitializeClass from Products.PageTemplates.PageTemplateFile import PageTemplateFile from Products.PageTemplates.ZopePageTemplate import manage_addPageTemplate from Products.PluggableAuthService.plugins.BasePlugin import BasePlugin from Products.PluggableAuthService.interfaces.plugins import \ IExtractionPlugin, IChallengePlugin, IAuthenticationPlugin, \ ICredentialsResetPlugin manage_addCASAuthHelperForm = PageTemplateFile( 'zmi/manage_addCASAuthHelperForm', globals()) class ProtectedAuthInfo: """An object where the username is not accessible from user code This object prevents the user name to be accessed or changed from anything by protected code. This means that we can always be sure that the username returned from _getUsername() has not been compromised by user code. This means we can store this object in a session, to have a session authentication. """ def _setAuthInfo(self, authinfo): self.__authinfo = authinfo def _getAuthInfo(self): return self.__authinfo def addCASAuthHelper( dispatcher , id , title=None , REQUEST=None ): """ Add a CASAuthHelper to a Pluggable Auth Service. """ sp = CASAuthHelper(id, title) dispatcher._setObject(sp.getId(), sp) if REQUEST is not None: REQUEST['RESPONSE'].redirect( '%s/manage_workspace' '?manage_tabs_message=' 'CASAuthHelper+added.' % dispatcher.absolute_url() ) class CASAuthHelper(PropertyManager, BasePlugin): """ Multi-plugin for managing details of CAS Authentication. """ __implements__ = ( IExtractionPlugin , IChallengePlugin , ICredentialsResetPlugin , IAuthenticationPlugin ) meta_type = 'CAS Auth Helper' login_url = 'https://your.cas.server:port/cas/login' logout_url = 'https://your.cas.server:port/cas/logout' validate_url = 'https://your.cas.server:port/cas/validate' session_var = '__ac' security = ClassSecurityInfo() _properties = ( { 'id' : 'title' , 'label' : 'Title' , 'type' : 'string' , 'mode' : 'w' } , { 'id' : 'login_url' , 'label' : 'CAS Login URL' , 'type' : 'string' , 'mode' : 'w' } , { 'id' : 'logout_url' , 'label' : 'CAS Logout URL' , 'type' : 'string' , 'mode' : 'w' } , {'id' : 'validate_url', 'type' : 'string', 'label' : 'Ticket validation URL', 'mode' : 'w', } , {'id' : 'session_var', 'type' : 'string', 'label' : 'Session credentials id', 'mode' : 'w', } ) manage_options = ( BasePlugin.manage_options[:1] + \ PropertyManager.manage_options + \ BasePlugin.manage_options[2:] ) def __init__(self, id, title=None): self._setId(id) self.title = title security.declarePrivate('extractCredentials') def extractCredentials(self, request): """ Extract credentials from session or 'request'. """ creds = {} session = request.SESSION username = None # First check if we have a ProtectedAuthInfo in the session ob = session.get(self.session_var) if ob is not None and isinstance(ob, ProtectedAuthInfo): username = ob._getAuthInfo() if username is None: # Not already authenticated. Is there a ticket in the URL? ticket = request.form.get('ticket') if ticket is None: return None # No CAS authentification username = self.validateTicket(request['URL'], ticket) if username is None: return None # Invalid CAS ticket # Successfull CAS authentication. Store the username # in a ProtectedAuthInfo in the session. ob = ProtectedAuthInfo() ob._setAuthInfo(username) session[self.session_var] = ob creds['login'] = username return creds def validateTicket(self, service, ticket): # prepare the GET parameters for checking the login checkparams = "?service=" + service + "&ticket=" + ticket # check the ticket casdata = urllib.URLopener().open(self.validate_url + checkparams) test = casdata.readline().strip() if test == 'yes': # user is validated username = casdata.readline().strip() return username else: # some unknown authentication error occurred return None def authenticateCredentials(self, credentials): if credentials['extractor'] != self.getId(): return (None, None) username = credentials['login'] return (username, username) security.declarePrivate('challenge') def challenge(self, request, response, **kw): """ Challenge the user for credentials. """ # Redirect if desired. url = self.getLoginURL() if url: came_from = request.get('came_from', None) if came_from is None: came_from = request['URL'] query = urllib.urlencode({'service': came_from}) #del response.headers['WWW-Authenticate'] response.redirect('%s?%s' % (url, query), lock=1) return 1 # Fall through to the standard unauthorized() call. return 0 security.declarePrivate('resetCredentials') def resetCredentials(self, request, response): """ Clears credentials and redirects to CAS logout page""" session = self.REQUEST.SESSION session[self.session_var] = None if self.logout_url: self.REQUEST.RESPONSE.redirect(self.logout_url) security.declarePrivate('getLoginURL') def getLoginURL(self): """ Where to send people for logging in """ return self.login_url InitializeClass(CASAuthHelper)