Back to index

plone3  3.1.7
oid.py
Go to the documentation of this file.
00001 from Acquisition import aq_parent
00002 from AccessControl.SecurityInfo import ClassSecurityInfo
00003 from Products.PageTemplates.PageTemplateFile import PageTemplateFile
00004 from Products.PluggableAuthService.plugins.BasePlugin import BasePlugin
00005 from Products.PluggableAuthService.utils import classImplements
00006 from Products.PluggableAuthService.interfaces.plugins \
00007                 import IAuthenticationPlugin, IUserEnumerationPlugin
00008 from plone.openid.interfaces import IOpenIdExtractionPlugin
00009 from plone.openid.store import ZopeStore
00010 from zExceptions import Redirect
00011 import transaction
00012 from openid.yadis.discover import DiscoveryFailure
00013 from openid.consumer.consumer import Consumer, SUCCESS
00014 import logging
00015 
00016 manage_addOpenIdPlugin = PageTemplateFile("../www/openidAdd", globals(), 
00017                 __name__="manage_addOpenIdPlugin")
00018 
00019 logger = logging.getLogger("PluggableAuthService")
00020 
00021 def addOpenIdPlugin(self, id, title='', REQUEST=None):
00022     """Add a OpenID plugin to a Pluggable Authentication Service.
00023     """
00024     p=OpenIdPlugin(id, title)
00025     self._setObject(p.getId(), p)
00026 
00027     if REQUEST is not None:
00028         REQUEST["RESPONSE"].redirect("%s/manage_workspace"
00029                 "?manage_tabs_message=OpenID+plugin+added." %
00030                 self.absolute_url())
00031 
00032 
00033 class OpenIdPlugin(BasePlugin):
00034     """OpenID authentication plugin.
00035     """
00036 
00037     meta_type = "OpenID plugin"
00038     security = ClassSecurityInfo()
00039 
00040     def __init__(self, id, title=None):
00041         self._setId(id)
00042         self.title=title
00043         self.store=ZopeStore()
00044 
00045 
00046     def getTrustRoot(self):
00047         pas=self._getPAS()
00048         site=aq_parent(pas)
00049         return site.absolute_url()
00050 
00051 
00052     def getConsumer(self):
00053         session=self.REQUEST["SESSION"]
00054         return Consumer(session, self.store)
00055 
00056 
00057     def extractOpenIdServerResponse(self, request, creds):
00058         """Process incoming redirect from an OpenId server.
00059 
00060         The redirect is detected by looking for the openid.mode
00061         form parameters. If it is found the creds parameter is
00062         cleared and filled with the found credentials.
00063         """
00064 
00065         mode=request.form.get("openid.mode", None)
00066         if mode=="id_res":
00067             # id_res means 'positive assertion' in OpenID, more commonly
00068             # described as 'positive authentication'
00069             creds.clear()
00070             creds["openid.source"]="server"
00071             creds["janrain_nonce"]=request.form.get("janrain_nonce")
00072             for (field,value) in request.form.iteritems():
00073                 if field.startswith("openid.") or field.startswith("openid1_"):
00074                     creds[field]=request.form[field]
00075         elif mode=="cancel":
00076             # cancel is a negative assertion in the OpenID protocol,
00077             # which means the user did not authorize correctly.
00078             pass
00079 
00080 
00081     # IOpenIdExtractionPlugin implementation
00082     def initiateChallenge(self, identity_url, return_to=None):
00083         consumer=self.getConsumer()
00084         try:
00085             auth_request=consumer.begin(identity_url)
00086         except DiscoveryFailure, e:
00087             logger.info("openid consumer discovery error for identity %s: %s",
00088                     identity_url, e[0])
00089             return
00090         except KeyError, e:
00091             logger.info("openid consumer error for identity %s: %s",
00092                     identity_url, e.why)
00093             pass
00094             
00095         if return_to is None:
00096             return_to=self.REQUEST.form.get("came_from", None)
00097         if not return_to or 'janrain_nonce' in return_to:
00098             # The conditional on janrain_nonce here is to handle the case where
00099             # the user logs in, logs out, and logs in again in succession.  We
00100             # were ending up with duplicate open ID variables on the second response
00101             # from the OpenID provider, which was breaking the second login.
00102             return_to=self.getTrustRoot()
00103 
00104         url=auth_request.redirectURL(self.getTrustRoot(), return_to)
00105 
00106         # There is evilness here: we can not use a normal RESPONSE.redirect
00107         # since further processing of the request will happily overwrite
00108         # our redirect. So instead we raise a Redirect exception, However
00109         # raising an exception aborts all transactions, which means our
00110         # session changes are not stored. So we do a commit ourselves to
00111         # get things working.
00112         # XXX this also f**ks up ZopeTestCase
00113         transaction.commit()
00114         raise Redirect, url
00115 
00116 
00117     # IExtractionPlugin implementation
00118     def extractCredentials(self, request):
00119         """This method performs the PAS credential extraction.
00120 
00121         It takes either the zope cookie and extracts openid credentials
00122         from it, or a redirect from an OpenID server.
00123         """
00124         creds={}
00125         identity=request.form.get("__ac_identity_url", None)
00126         if identity is not None and identity != "":
00127             self.initiateChallenge(identity)
00128             return creds
00129             
00130         self.extractOpenIdServerResponse(request, creds)
00131         return creds
00132 
00133 
00134     # IAuthenticationPlugin implementation
00135     def authenticateCredentials(self, credentials):
00136         if not credentials.has_key("openid.source"):
00137             return None
00138 
00139         if credentials["openid.source"]=="server":
00140             consumer=self.getConsumer()
00141             
00142             # remove the extractor key that PAS adds to the credentials,
00143             # or python-openid will complain
00144             query = credentials.copy()
00145             del query['extractor']
00146             
00147             result=consumer.complete(query, self.REQUEST.ACTUAL_URL)
00148             identity=result.identity_url
00149             
00150             if result.status==SUCCESS:
00151                 self._getPAS().updateCredentials(self.REQUEST,
00152                         self.REQUEST.RESPONSE, identity, "")
00153                 return (identity, identity)
00154             else:
00155                 logger.info("OpenId Authentication for %s failed: %s",
00156                                 identity, result.message)
00157 
00158         return None
00159 
00160 
00161     # IUserEnumerationPlugin implementation
00162     def enumerateUsers(self, id=None, login=None, exact_match=False,
00163             sort_by=None, max_results=None, **kw):
00164         """Slightly evil enumerator.
00165 
00166         This is needed to be able to get PAS to return a user which it should
00167         be able to handle but who can not be enumerated.
00168 
00169         We do this by checking for the exact kind of call the PAS getUserById
00170         implementation makes
00171         """
00172         if id and login and id!=login:
00173             return None
00174 
00175         if (id and not exact_match) or kw:
00176             return None
00177 
00178         key=id and id or login
00179 
00180         if not (key.startswith("http:") or key.startswith("https:")):
00181             return None
00182 
00183         return [ {
00184                     "id" : key,
00185                     "login" : key,
00186                     "pluginid" : self.getId(),
00187                 } ]
00188 
00189 
00190 
00191 classImplements(OpenIdPlugin, IOpenIdExtractionPlugin, IAuthenticationPlugin,
00192                 IUserEnumerationPlugin)
00193 
00194