1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900 |
- # -*- test-case-name: openid.test.test_consumer -*-
- """OpenID support for Relying Parties (aka Consumers).
- This module documents the main interface with the OpenID consumer
- library. The only part of the library which has to be used and isn't
- documented in full here is the store required to create an
- C{L{Consumer}} instance. More on the abstract store type and
- concrete implementations of it that are provided in the documentation
- for the C{L{__init__<Consumer.__init__>}} method of the
- C{L{Consumer}} class.
- OVERVIEW
- ========
- The OpenID identity verification process most commonly uses the
- following steps, as visible to the user of this library:
- 1. The user enters their OpenID into a field on the consumer's
- site, and hits a login button.
- 2. The consumer site discovers the user's OpenID provider using
- the Yadis protocol.
- 3. The consumer site sends the browser a redirect to the
- OpenID provider. This is the authentication request as
- described in the OpenID specification.
- 4. The OpenID provider's site sends the browser a redirect
- back to the consumer site. This redirect contains the
- provider's response to the authentication request.
- The most important part of the flow to note is the consumer's site
- must handle two separate HTTP requests in order to perform the
- full identity check.
- LIBRARY DESIGN
- ==============
- This consumer library is designed with that flow in mind. The
- goal is to make it as easy as possible to perform the above steps
- securely.
- At a high level, there are two important parts in the consumer
- library. The first important part is this module, which contains
- the interface to actually use this library. The second is the
- C{L{openid.store.interface}} module, which describes the
- interface to use if you need to create a custom method for storing
- the state this library needs to maintain between requests.
- In general, the second part is less important for users of the
- library to know about, as several implementations are provided
- which cover a wide variety of situations in which consumers may
- use the library.
- This module contains a class, C{L{Consumer}}, with methods
- corresponding to the actions necessary in each of steps 2, 3, and
- 4 described in the overview. Use of this library should be as easy
- as creating an C{L{Consumer}} instance and calling the methods
- appropriate for the action the site wants to take.
- SESSIONS, STORES, AND STATELESS MODE
- ====================================
- The C{L{Consumer}} object keeps track of two types of state:
- 1. State of the user's current authentication attempt. Things like
- the identity URL, the list of endpoints discovered for that
- URL, and in case where some endpoints are unreachable, the list
- of endpoints already tried. This state needs to be held from
- Consumer.begin() to Consumer.complete(), but it is only applicable
- to a single session with a single user agent, and at the end of
- the authentication process (i.e. when an OP replies with either
- C{id_res} or C{cancel}) it may be discarded.
- 2. State of relationships with servers, i.e. shared secrets
- (associations) with servers and nonces seen on signed messages.
- This information should persist from one session to the next and
- should not be bound to a particular user-agent.
- These two types of storage are reflected in the first two arguments of
- Consumer's constructor, C{session} and C{store}. C{session} is a
- dict-like object and we hope your web framework provides you with one
- of these bound to the user agent. C{store} is an instance of
- L{openid.store.interface.OpenIDStore}.
- Since the store does hold secrets shared between your application and the
- OpenID provider, you should be careful about how you use it in a shared
- hosting environment. If the filesystem or database permissions of your
- web host allow strangers to read from them, do not store your data there!
- If you have no safe place to store your data, construct your consumer
- with C{None} for the store, and it will operate only in stateless mode.
- Stateless mode may be slower, put more load on the OpenID provider, and
- trusts the provider to keep you safe from replay attacks.
- Several store implementation are provided, and the interface is
- fully documented so that custom stores can be used as well. See
- the documentation for the C{L{Consumer}} class for more
- information on the interface for stores. The implementations that
- are provided allow the consumer site to store the necessary data
- in several different ways, including several SQL databases and
- normal files on disk.
- IMMEDIATE MODE
- ==============
- In the flow described above, the user may need to confirm to the
- OpenID provider that it's ok to disclose his or her identity.
- The provider may draw pages asking for information from the user
- before it redirects the browser back to the consumer's site. This
- is generally transparent to the consumer site, so it is typically
- ignored as an implementation detail.
- There can be times, however, where the consumer site wants to get
- a response immediately. When this is the case, the consumer can
- put the library in immediate mode. In immediate mode, there is an
- extra response possible from the server, which is essentially the
- server reporting that it doesn't have enough information to answer
- the question yet.
- USING THIS LIBRARY
- ==================
- Integrating this library into an application is usually a
- relatively straightforward process. The process should basically
- follow this plan:
- Add an OpenID login field somewhere on your site. When an OpenID
- is entered in that field and the form is submitted, it should make
- a request to your site which includes that OpenID URL.
- First, the application should L{instantiate a Consumer<Consumer.__init__>}
- with a session for per-user state and store for shared state.
- using the store of choice.
- Next, the application should call the 'C{L{begin<Consumer.begin>}}' method on the
- C{L{Consumer}} instance. This method takes the OpenID URL. The
- C{L{begin<Consumer.begin>}} method returns an C{L{AuthRequest}}
- object.
- Next, the application should call the
- C{L{redirectURL<AuthRequest.redirectURL>}} method on the
- C{L{AuthRequest}} object. The parameter C{return_to} is the URL
- that the OpenID server will send the user back to after attempting
- to verify his or her identity. The C{realm} parameter is the
- URL (or URL pattern) that identifies your web site to the user
- when he or she is authorizing it. Send a redirect to the
- resulting URL to the user's browser.
- That's the first half of the authentication process. The second
- half of the process is done after the user's OpenID Provider sends the
- user's browser a redirect back to your site to complete their
- login.
- When that happens, the user will contact your site at the URL
- given as the C{return_to} URL to the
- C{L{redirectURL<AuthRequest.redirectURL>}} call made
- above. The request will have several query parameters added to
- the URL by the OpenID provider as the information necessary to
- finish the request.
- Get a C{L{Consumer}} instance with the same session and store as
- before and call its C{L{complete<Consumer.complete>}} method,
- passing in all the received query arguments.
- There are multiple possible return types possible from that
- method. These indicate whether or not the login was successful,
- and include any additional information appropriate for their type.
- @var SUCCESS: constant used as the status for
- L{SuccessResponse<openid.consumer.consumer.SuccessResponse>} objects.
- @var FAILURE: constant used as the status for
- L{FailureResponse<openid.consumer.consumer.FailureResponse>} objects.
- @var CANCEL: constant used as the status for
- L{CancelResponse<openid.consumer.consumer.CancelResponse>} objects.
- @var SETUP_NEEDED: constant used as the status for
- L{SetupNeededResponse<openid.consumer.consumer.SetupNeededResponse>}
- objects.
- """
- import cgi
- import copy
- from urlparse import urlparse, urldefrag
- from openid import fetchers
- from openid.consumer.discover import discover, OpenIDServiceEndpoint, \
- DiscoveryFailure, OPENID_1_0_TYPE, OPENID_1_1_TYPE, OPENID_2_0_TYPE
- from openid.message import Message, OPENID_NS, OPENID2_NS, OPENID1_NS, \
- IDENTIFIER_SELECT, no_default, BARE_NS
- from openid import cryptutil
- from openid import oidutil
- from openid.association import Association, default_negotiator, \
- SessionNegotiator
- from openid.dh import DiffieHellman
- from openid.store.nonce import mkNonce, split as splitNonce
- from openid.yadis.manager import Discovery
- from openid import urinorm
- __all__ = ['AuthRequest', 'Consumer', 'SuccessResponse',
- 'SetupNeededResponse', 'CancelResponse', 'FailureResponse',
- 'SUCCESS', 'FAILURE', 'CANCEL', 'SETUP_NEEDED',
- ]
- def makeKVPost(request_message, server_url):
- """Make a Direct Request to an OpenID Provider and return the
- result as a Message object.
- @raises openid.fetchers.HTTPFetchingError: if an error is
- encountered in making the HTTP post.
- @rtype: L{openid.message.Message}
- """
- # XXX: TESTME
- resp = fetchers.fetch(server_url, body=request_message.toURLEncoded())
- # Process response in separate function that can be shared by async code.
- return _httpResponseToMessage(resp, server_url)
- def _httpResponseToMessage(response, server_url):
- """Adapt a POST response to a Message.
- @type response: L{openid.fetchers.HTTPResponse}
- @param response: Result of a POST to an OpenID endpoint.
- @rtype: L{openid.message.Message}
- @raises openid.fetchers.HTTPFetchingError: if the server returned a
- status of other than 200 or 400.
- @raises ServerError: if the server returned an OpenID error.
- """
- # Should this function be named Message.fromHTTPResponse instead?
- response_message = Message.fromKVForm(response.body)
- if response.status == 400:
- raise ServerError.fromMessage(response_message)
- elif response.status not in (200, 206):
- fmt = 'bad status code from server %s: %s'
- error_message = fmt % (server_url, response.status)
- raise fetchers.HTTPFetchingError(error_message)
- return response_message
- class Consumer(object):
- """An OpenID consumer implementation that performs discovery and
- does session management.
- @ivar consumer: an instance of an object implementing the OpenID
- protocol, but doing no discovery or session management.
- @type consumer: GenericConsumer
- @ivar session: A dictionary-like object representing the user's
- session data. This is used for keeping state of the OpenID
- transaction when the user is redirected to the server.
- @cvar session_key_prefix: A string that is prepended to session
- keys to ensure that they are unique. This variable may be
- changed to suit your application.
- """
- session_key_prefix = "_openid_consumer_"
- _token = 'last_token'
- _discover = staticmethod(discover)
- def __init__(self, session, store, consumer_class=None):
- """Initialize a Consumer instance.
- You should create a new instance of the Consumer object with
- every HTTP request that handles OpenID transactions.
- @param session: See L{the session instance variable<openid.consumer.consumer.Consumer.session>}
- @param store: an object that implements the interface in
- C{L{openid.store.interface.OpenIDStore}}. Several
- implementations are provided, to cover common database
- environments.
- @type store: C{L{openid.store.interface.OpenIDStore}}
- @see: L{openid.store.interface}
- @see: L{openid.store}
- """
- self.session = session
- if consumer_class is None:
- consumer_class = GenericConsumer
- self.consumer = consumer_class(store)
- self._token_key = self.session_key_prefix + self._token
- def begin(self, user_url, anonymous=False):
- """Start the OpenID authentication process. See steps 1-2 in
- the overview at the top of this file.
- @param user_url: Identity URL given by the user. This method
- performs a textual transformation of the URL to try and
- make sure it is normalized. For example, a user_url of
- example.com will be normalized to http://example.com/
- normalizing and resolving any redirects the server might
- issue.
- @type user_url: unicode
- @param anonymous: Whether to make an anonymous request of the OpenID
- provider. Such a request does not ask for an authorization
- assertion for an OpenID identifier, but may be used with
- extensions to pass other data. e.g. "I don't care who you are,
- but I'd like to know your time zone."
- @type anonymous: bool
- @returns: An object containing the discovered information will
- be returned, with a method for building a redirect URL to
- the server, as described in step 3 of the overview. This
- object may also be used to add extension arguments to the
- request, using its
- L{addExtensionArg<openid.consumer.consumer.AuthRequest.addExtensionArg>}
- method.
- @returntype: L{AuthRequest<openid.consumer.consumer.AuthRequest>}
- @raises openid.consumer.discover.DiscoveryFailure: when I fail to
- find an OpenID server for this URL. If the C{yadis} package
- is available, L{openid.consumer.discover.DiscoveryFailure} is
- an alias for C{yadis.discover.DiscoveryFailure}.
- """
- disco = Discovery(self.session, user_url, self.session_key_prefix)
- try:
- service = disco.getNextService(self._discover)
- except fetchers.HTTPFetchingError, why:
- raise DiscoveryFailure(
- 'Error fetching XRDS document: %s' % (why[0],), None)
- if service is None:
- raise DiscoveryFailure(
- 'No usable OpenID services found for %s' % (user_url,), None)
- else:
- return self.beginWithoutDiscovery(service, anonymous)
- def beginWithoutDiscovery(self, service, anonymous=False):
- """Start OpenID verification without doing OpenID server
- discovery. This method is used internally by Consumer.begin
- after discovery is performed, and exists to provide an
- interface for library users needing to perform their own
- discovery.
- @param service: an OpenID service endpoint descriptor. This
- object and factories for it are found in the
- L{openid.consumer.discover} module.
- @type service:
- L{OpenIDServiceEndpoint<openid.consumer.discover.OpenIDServiceEndpoint>}
- @returns: an OpenID authentication request object.
- @rtype: L{AuthRequest<openid.consumer.consumer.AuthRequest>}
- @See: Openid.consumer.consumer.Consumer.begin
- @see: openid.consumer.discover
- """
- auth_req = self.consumer.begin(service)
- self.session[self._token_key] = auth_req.endpoint
- try:
- auth_req.setAnonymous(anonymous)
- except ValueError, why:
- raise ProtocolError(str(why))
- return auth_req
- def complete(self, query, current_url):
- """Called to interpret the server's response to an OpenID
- request. It is called in step 4 of the flow described in the
- consumer overview.
- @param query: A dictionary of the query parameters for this
- HTTP request.
- @param current_url: The URL used to invoke the application.
- Extract the URL from your application's web
- request framework and specify it here to have it checked
- against the openid.return_to value in the response. If
- the return_to URL check fails, the status of the
- completion will be FAILURE.
- @returns: a subclass of Response. The type of response is
- indicated by the status attribute, which will be one of
- SUCCESS, CANCEL, FAILURE, or SETUP_NEEDED.
- @see: L{SuccessResponse<openid.consumer.consumer.SuccessResponse>}
- @see: L{CancelResponse<openid.consumer.consumer.CancelResponse>}
- @see: L{SetupNeededResponse<openid.consumer.consumer.SetupNeededResponse>}
- @see: L{FailureResponse<openid.consumer.consumer.FailureResponse>}
- """
- endpoint = self.session.get(self._token_key)
- message = Message.fromPostArgs(query)
- response = self.consumer.complete(message, endpoint, current_url)
- try:
- del self.session[self._token_key]
- except KeyError:
- pass
- if (response.status in ['success', 'cancel'] and
- response.identity_url is not None):
- disco = Discovery(self.session,
- response.identity_url,
- self.session_key_prefix)
- # This is OK to do even if we did not do discovery in
- # the first place.
- disco.cleanup(force=True)
- return response
- def setAssociationPreference(self, association_preferences):
- """Set the order in which association types/sessions should be
- attempted. For instance, to only allow HMAC-SHA256
- associations created with a DH-SHA256 association session:
- >>> consumer.setAssociationPreference([('HMAC-SHA256', 'DH-SHA256')])
- Any association type/association type pair that is not in this
- list will not be attempted at all.
- @param association_preferences: The list of allowed
- (association type, association session type) pairs that
- should be allowed for this consumer to use, in order from
- most preferred to least preferred.
- @type association_preferences: [(str, str)]
- @returns: None
- @see: C{L{openid.association.SessionNegotiator}}
- """
- self.consumer.negotiator = SessionNegotiator(association_preferences)
- class DiffieHellmanSHA1ConsumerSession(object):
- session_type = 'DH-SHA1'
- hash_func = staticmethod(cryptutil.sha1)
- secret_size = 20
- allowed_assoc_types = ['HMAC-SHA1']
- def __init__(self, dh=None):
- if dh is None:
- dh = DiffieHellman.fromDefaults()
- self.dh = dh
- def getRequest(self):
- cpub = cryptutil.longToBase64(self.dh.public)
- args = {'dh_consumer_public': cpub}
- if not self.dh.usingDefaultValues():
- args.update({
- 'dh_modulus': cryptutil.longToBase64(self.dh.modulus),
- 'dh_gen': cryptutil.longToBase64(self.dh.generator),
- })
- return args
- def extractSecret(self, response):
- dh_server_public64 = response.getArg(
- OPENID_NS, 'dh_server_public', no_default)
- enc_mac_key64 = response.getArg(OPENID_NS, 'enc_mac_key', no_default)
- dh_server_public = cryptutil.base64ToLong(dh_server_public64)
- enc_mac_key = oidutil.fromBase64(enc_mac_key64)
- return self.dh.xorSecret(dh_server_public, enc_mac_key, self.hash_func)
- class DiffieHellmanSHA256ConsumerSession(DiffieHellmanSHA1ConsumerSession):
- session_type = 'DH-SHA256'
- hash_func = staticmethod(cryptutil.sha256)
- secret_size = 32
- allowed_assoc_types = ['HMAC-SHA256']
- class PlainTextConsumerSession(object):
- session_type = 'no-encryption'
- allowed_assoc_types = ['HMAC-SHA1', 'HMAC-SHA256']
- def getRequest(self):
- return {}
- def extractSecret(self, response):
- mac_key64 = response.getArg(OPENID_NS, 'mac_key', no_default)
- return oidutil.fromBase64(mac_key64)
- class SetupNeededError(Exception):
- """Internally-used exception that indicates that an immediate-mode
- request cancelled."""
- def __init__(self, user_setup_url=None):
- Exception.__init__(self, user_setup_url)
- self.user_setup_url = user_setup_url
- class ProtocolError(ValueError):
- """Exception that indicates that a message violated the
- protocol. It is raised and caught internally to this file."""
- class TypeURIMismatch(ProtocolError):
- """A protocol error arising from type URIs mismatching
- """
- def __init__(self, expected, endpoint):
- ProtocolError.__init__(self, expected, endpoint)
- self.expected = expected
- self.endpoint = endpoint
- def __str__(self):
- s = '<%s.%s: Required type %s not found in %s for endpoint %s>' % (
- self.__class__.__module__, self.__class__.__name__,
- self.expected, self.endpoint.type_uris, self.endpoint)
- return s
- class ServerError(Exception):
- """Exception that is raised when the server returns a 400 response
- code to a direct request."""
- def __init__(self, error_text, error_code, message):
- Exception.__init__(self, error_text)
- self.error_text = error_text
- self.error_code = error_code
- self.message = message
- def fromMessage(cls, message):
- """Generate a ServerError instance, extracting the error text
- and the error code from the message."""
- error_text = message.getArg(
- OPENID_NS, 'error', '<no error message supplied>')
- error_code = message.getArg(OPENID_NS, 'error_code')
- return cls(error_text, error_code, message)
- fromMessage = classmethod(fromMessage)
- class GenericConsumer(object):
- """This is the implementation of the common logic for OpenID
- consumers. It is unaware of the application in which it is
- running.
- @ivar negotiator: An object that controls the kind of associations
- that the consumer makes. It defaults to
- C{L{openid.association.default_negotiator}}. Assign a
- different negotiator to it if you have specific requirements
- for how associations are made.
- @type negotiator: C{L{openid.association.SessionNegotiator}}
- """
- # The name of the query parameter that gets added to the return_to
- # URL when using OpenID1. You can change this value if you want or
- # need a different name, but don't make it start with openid,
- # because it's not a standard protocol thing for OpenID1. For
- # OpenID2, the library will take care of the nonce using standard
- # OpenID query parameter names.
- openid1_nonce_query_arg_name = 'janrain_nonce'
- # Another query parameter that gets added to the return_to for
- # OpenID 1; if the user's session state is lost, use this claimed
- # identifier to do discovery when verifying the response.
- openid1_return_to_identifier_name = 'openid1_claimed_id'
- session_types = {
- 'DH-SHA1':DiffieHellmanSHA1ConsumerSession,
- 'DH-SHA256':DiffieHellmanSHA256ConsumerSession,
- 'no-encryption':PlainTextConsumerSession,
- }
- _discover = staticmethod(discover)
- def __init__(self, store):
- self.store = store
- self.negotiator = default_negotiator.copy()
- def begin(self, service_endpoint):
- """Create an AuthRequest object for the specified
- service_endpoint. This method will create an association if
- necessary."""
- if self.store is None:
- assoc = None
- else:
- assoc = self._getAssociation(service_endpoint)
- request = AuthRequest(service_endpoint, assoc)
- request.return_to_args[self.openid1_nonce_query_arg_name] = mkNonce()
- if request.message.isOpenID1():
- request.return_to_args[self.openid1_return_to_identifier_name] = \
- request.endpoint.claimed_id
- return request
- def complete(self, message, endpoint, return_to):
- """Process the OpenID message, using the specified endpoint
- and return_to URL as context. This method will handle any
- OpenID message that is sent to the return_to URL.
- """
- mode = message.getArg(OPENID_NS, 'mode', '<No mode set>')
- modeMethod = getattr(self, '_complete_' + mode,
- self._completeInvalid)
- return modeMethod(message, endpoint, return_to)
- def _complete_cancel(self, message, endpoint, _):
- return CancelResponse(endpoint)
- def _complete_error(self, message, endpoint, _):
- error = message.getArg(OPENID_NS, 'error')
- contact = message.getArg(OPENID_NS, 'contact')
- reference = message.getArg(OPENID_NS, 'reference')
- return FailureResponse(endpoint, error, contact=contact,
- reference=reference)
- def _complete_setup_needed(self, message, endpoint, _):
- if not message.isOpenID2():
- return self._completeInvalid(message, endpoint, _)
- user_setup_url = message.getArg(OPENID2_NS, 'user_setup_url')
- return SetupNeededResponse(endpoint, user_setup_url)
- def _complete_id_res(self, message, endpoint, return_to):
- try:
- self._checkSetupNeeded(message)
- except SetupNeededError, why:
- return SetupNeededResponse(endpoint, why.user_setup_url)
- else:
- try:
- return self._doIdRes(message, endpoint, return_to)
- except (ProtocolError, DiscoveryFailure), why:
- return FailureResponse(endpoint, why[0])
- def _completeInvalid(self, message, endpoint, _):
- mode = message.getArg(OPENID_NS, 'mode', '<No mode set>')
- return FailureResponse(endpoint,
- 'Invalid openid.mode: %r' % (mode,))
- def _checkReturnTo(self, message, return_to):
- """Check an OpenID message and its openid.return_to value
- against a return_to URL from an application. Return True on
- success, False on failure.
- """
- # Check the openid.return_to args against args in the original
- # message.
- try:
- self._verifyReturnToArgs(message.toPostArgs())
- except ProtocolError, why:
- oidutil.log("Verifying return_to arguments: %s" % (why[0],))
- return False
- # Check the return_to base URL against the one in the message.
- msg_return_to = message.getArg(OPENID_NS, 'return_to')
- # The URL scheme, authority, and path MUST be the same between
- # the two URLs.
- app_parts = urlparse(urinorm.urinorm(return_to))
- msg_parts = urlparse(urinorm.urinorm(msg_return_to))
- # (addressing scheme, network location, path) must be equal in
- # both URLs.
- for part in range(0, 3):
- if app_parts[part] != msg_parts[part]:
- return False
- return True
- _makeKVPost = staticmethod(makeKVPost)
- def _checkSetupNeeded(self, message):
- """Check an id_res message to see if it is a
- checkid_immediate cancel response.
- @raises SetupNeededError: if it is a checkid_immediate cancellation
- """
- # In OpenID 1, we check to see if this is a cancel from
- # immediate mode by the presence of the user_setup_url
- # parameter.
- if message.isOpenID1():
- user_setup_url = message.getArg(OPENID1_NS, 'user_setup_url')
- if user_setup_url is not None:
- raise SetupNeededError(user_setup_url)
- def _doIdRes(self, message, endpoint, return_to):
- """Handle id_res responses that are not cancellations of
- immediate mode requests.
- @param message: the response paramaters.
- @param endpoint: the discovered endpoint object. May be None.
- @raises ProtocolError: If the message contents are not
- well-formed according to the OpenID specification. This
- includes missing fields or not signing fields that should
- be signed.
- @raises DiscoveryFailure: If the subject of the id_res message
- does not match the supplied endpoint, and discovery on the
- identifier in the message fails (this should only happen
- when using OpenID 2)
- @returntype: L{Response}
- """
- # Checks for presence of appropriate fields (and checks
- # signed list fields)
- self._idResCheckForFields(message)
- if not self._checkReturnTo(message, return_to):
- raise ProtocolError(
- "return_to does not match return URL. Expected %r, got %r"
- % (return_to, message.getArg(OPENID_NS, 'return_to')))
- # Verify discovery information:
- endpoint = self._verifyDiscoveryResults(message, endpoint)
- oidutil.log("Received id_res response from %s using association %s" %
- (endpoint.server_url,
- message.getArg(OPENID_NS, 'assoc_handle')))
- self._idResCheckSignature(message, endpoint.server_url)
- # Will raise a ProtocolError if the nonce is bad
- self._idResCheckNonce(message, endpoint)
- signed_list_str = message.getArg(OPENID_NS, 'signed', no_default)
- signed_list = signed_list_str.split(',')
- signed_fields = ["openid." + s for s in signed_list]
- return SuccessResponse(endpoint, message, signed_fields)
- def _idResGetNonceOpenID1(self, message, endpoint):
- """Extract the nonce from an OpenID 1 response. Return the
- nonce from the BARE_NS since we independently check the
- return_to arguments are the same as those in the response
- message.
- See the openid1_nonce_query_arg_name class variable
- @returns: The nonce as a string or None
- """
- return message.getArg(BARE_NS, self.openid1_nonce_query_arg_name)
- def _idResCheckNonce(self, message, endpoint):
- if message.isOpenID1():
- # This indicates that the nonce was generated by the consumer
- nonce = self._idResGetNonceOpenID1(message, endpoint)
- server_url = ''
- else:
- nonce = message.getArg(OPENID2_NS, 'response_nonce')
- server_url = endpoint.server_url
- if nonce is None:
- raise ProtocolError('Nonce missing from response')
- try:
- timestamp, salt = splitNonce(nonce)
- except ValueError, why:
- raise ProtocolError('Malformed nonce: %s' % (why[0],))
- if (self.store is not None and
- not self.store.useNonce(server_url, timestamp, salt)):
- raise ProtocolError('Nonce already used or out of range')
- def _idResCheckSignature(self, message, server_url):
- assoc_handle = message.getArg(OPENID_NS, 'assoc_handle')
- if self.store is None:
- assoc = None
- else:
- assoc = self.store.getAssociation(server_url, assoc_handle)
- if assoc:
- if assoc.getExpiresIn() <= 0:
- # XXX: It might be a good idea sometimes to re-start the
- # authentication with a new association. Doing it
- # automatically opens the possibility for
- # denial-of-service by a server that just returns expired
- # associations (or really short-lived associations)
- raise ProtocolError(
- 'Association with %s expired' % (server_url,))
- if not assoc.checkMessageSignature(message):
- raise ProtocolError('Bad signature')
- else:
- # It's not an association we know about. Stateless mode is our
- # only possible path for recovery.
- # XXX - async framework will not want to block on this call to
- # _checkAuth.
- if not self._checkAuth(message, server_url):
- raise ProtocolError('Server denied check_authentication')
- def _idResCheckForFields(self, message):
- # XXX: this should be handled by the code that processes the
- # response (that is, if a field is missing, we should not have
- # to explicitly check that it's present, just make sure that
- # the fields are actually being used by the rest of the code
- # in tests). Although, which fields are signed does need to be
- # checked somewhere.
- basic_fields = ['return_to', 'assoc_handle', 'sig', 'signed']
- basic_sig_fields = ['return_to', 'identity']
- require_fields = {
- OPENID2_NS: basic_fields + ['op_endpoint'],
- OPENID1_NS: basic_fields + ['identity'],
- }
- require_sigs = {
- OPENID2_NS: basic_sig_fields + ['response_nonce',
- 'claimed_id',
- 'assoc_handle',
- 'op_endpoint',],
- OPENID1_NS: basic_sig_fields,
- }
- for field in require_fields[message.getOpenIDNamespace()]:
- if not message.hasKey(OPENID_NS, field):
- raise ProtocolError('Missing required field %r' % (field,))
- signed_list_str = message.getArg(OPENID_NS, 'signed', no_default)
- signed_list = signed_list_str.split(',')
- for field in require_sigs[message.getOpenIDNamespace()]:
- # Field is present and not in signed list
- if message.hasKey(OPENID_NS, field) and field not in signed_list:
- raise ProtocolError('"%s" not signed' % (field,))
- def _verifyReturnToArgs(query):
- """Verify that the arguments in the return_to URL are present in this
- response.
- """
- message = Message.fromPostArgs(query)
- return_to = message.getArg(OPENID_NS, 'return_to')
- if return_to is None:
- raise ProtocolError('Response has no return_to')
- parsed_url = urlparse(return_to)
- rt_query = parsed_url[4]
- parsed_args = cgi.parse_qsl(rt_query)
- for rt_key, rt_value in parsed_args:
- try:
- value = query[rt_key]
- if rt_value != value:
- format = ("parameter %s value %r does not match "
- "return_to's value %r")
- raise ProtocolError(format % (rt_key, value, rt_value))
- except KeyError:
- format = "return_to parameter %s absent from query %r"
- raise ProtocolError(format % (rt_key, query))
- # Make sure all non-OpenID arguments in the response are also
- # in the signed return_to.
- bare_args = message.getArgs(BARE_NS)
- for pair in bare_args.iteritems():
- if pair not in parsed_args:
- raise ProtocolError("Parameter %s not in return_to URL" % (pair[0],))
- _verifyReturnToArgs = staticmethod(_verifyReturnToArgs)
- def _verifyDiscoveryResults(self, resp_msg, endpoint=None):
- """
- Extract the information from an OpenID assertion message and
- verify it against the original
- @param endpoint: The endpoint that resulted from doing discovery
- @param resp_msg: The id_res message object
- @returns: the verified endpoint
- """
- if resp_msg.getOpenIDNamespace() == OPENID2_NS:
- return self._verifyDiscoveryResultsOpenID2(resp_msg, endpoint)
- else:
- return self._verifyDiscoveryResultsOpenID1(resp_msg, endpoint)
- def _verifyDiscoveryResultsOpenID2(self, resp_msg, endpoint):
- to_match = OpenIDServiceEndpoint()
- to_match.type_uris = [OPENID_2_0_TYPE]
- to_match.claimed_id = resp_msg.getArg(OPENID2_NS, 'claimed_id')
- to_match.local_id = resp_msg.getArg(OPENID2_NS, 'identity')
- # Raises a KeyError when the op_endpoint is not present
- to_match.server_url = resp_msg.getArg(
- OPENID2_NS, 'op_endpoint', no_default)
- # claimed_id and identifier must both be present or both
- # be absent
- if (to_match.claimed_id is None and
- to_match.local_id is not None):
- raise ProtocolError(
- 'openid.identity is present without openid.claimed_id')
- elif (to_match.claimed_id is not None and
- to_match.local_id is None):
- raise ProtocolError(
- 'openid.claimed_id is present without openid.identity')
- # This is a response without identifiers, so there's really no
- # checking that we can do, so return an endpoint that's for
- # the specified `openid.op_endpoint'
- elif to_match.claimed_id is None:
- return OpenIDServiceEndpoint.fromOPEndpointURL(to_match.server_url)
- # The claimed ID doesn't match, so we have to do discovery
- # again. This covers not using sessions, OP identifier
- # endpoints and responses that didn't match the original
- # request.
- if not endpoint:
- oidutil.log('No pre-discovered information supplied.')
- endpoint = self._discoverAndVerify(to_match.claimed_id, [to_match])
- else:
- # The claimed ID matches, so we use the endpoint that we
- # discovered in initiation. This should be the most common
- # case.
- try:
- self._verifyDiscoverySingle(endpoint, to_match)
- except ProtocolError, e:
- oidutil.log(
- "Error attempting to use stored discovery information: " +
- str(e))
- oidutil.log("Attempting discovery to verify endpoint")
- endpoint = self._discoverAndVerify(
- to_match.claimed_id, [to_match])
- # The endpoint we return should have the claimed ID from the
- # message we just verified, fragment and all.
- if endpoint.claimed_id != to_match.claimed_id:
- endpoint = copy.copy(endpoint)
- endpoint.claimed_id = to_match.claimed_id
- return endpoint
- def _verifyDiscoveryResultsOpenID1(self, resp_msg, endpoint):
- claimed_id = resp_msg.getArg(BARE_NS, self.openid1_return_to_identifier_name)
- if endpoint is None and claimed_id is None:
- raise RuntimeError(
- 'When using OpenID 1, the claimed ID must be supplied, '
- 'either by passing it through as a return_to parameter '
- 'or by using a session, and supplied to the GenericConsumer '
- 'as the argument to complete()')
- elif endpoint is not None and claimed_id is None:
- claimed_id = endpoint.claimed_id
- to_match = OpenIDServiceEndpoint()
- to_match.type_uris = [OPENID_1_1_TYPE]
- to_match.local_id = resp_msg.getArg(OPENID1_NS, 'identity')
- # Restore delegate information from the initiation phase
- to_match.claimed_id = claimed_id
- if to_match.local_id is None:
- raise ProtocolError('Missing required field openid.identity')
- to_match_1_0 = copy.copy(to_match)
- to_match_1_0.type_uris = [OPENID_1_0_TYPE]
- if endpoint is not None:
- try:
- try:
- self._verifyDiscoverySingle(endpoint, to_match)
- except TypeURIMismatch:
- self._verifyDiscoverySingle(endpoint, to_match_1_0)
- except ProtocolError, e:
- oidutil.log("Error attempting to use stored discovery information: " +
- str(e))
- oidutil.log("Attempting discovery to verify endpoint")
- else:
- return endpoint
- # Endpoint is either bad (failed verification) or None
- return self._discoverAndVerify(claimed_id, [to_match, to_match_1_0])
- def _verifyDiscoverySingle(self, endpoint, to_match):
- """Verify that the given endpoint matches the information
- extracted from the OpenID assertion, and raise an exception if
- there is a mismatch.
- @type endpoint: openid.consumer.discover.OpenIDServiceEndpoint
- @type to_match: openid.consumer.discover.OpenIDServiceEndpoint
- @rtype: NoneType
- @raises ProtocolError: when the endpoint does not match the
- discovered information.
- """
- # Every type URI that's in the to_match endpoint has to be
- # present in the discovered endpoint.
- for type_uri in to_match.type_uris:
- if not endpoint.usesExtension(type_uri):
- raise TypeURIMismatch(type_uri, endpoint)
- # Fragments do not influence discovery, so we can't compare a
- # claimed identifier with a fragment to discovered information.
- defragged_claimed_id, _ = urldefrag(to_match.claimed_id)
- if defragged_claimed_id != endpoint.claimed_id:
- raise ProtocolError(
- 'Claimed ID does not match (different subjects!), '
- 'Expected %s, got %s' %
- (defragged_claimed_id, endpoint.claimed_id))
- if to_match.getLocalID() != endpoint.getLocalID():
- raise ProtocolError('local_id mismatch. Expected %s, got %s' %
- (to_match.getLocalID(), endpoint.getLocalID()))
- # If the server URL is None, this must be an OpenID 1
- # response, because op_endpoint is a required parameter in
- # OpenID 2. In that case, we don't actually care what the
- # discovered server_url is, because signature checking or
- # check_auth should take care of that check for us.
- if to_match.server_url is None:
- assert to_match.preferredNamespace() == OPENID1_NS, (
- """The code calling this must ensure that OpenID 2
- responses have a non-none `openid.op_endpoint' and
- that it is set as the `server_url' attribute of the
- `to_match' endpoint.""")
- elif to_match.server_url != endpoint.server_url:
- raise ProtocolError('OP Endpoint mismatch. Expected %s, got %s' %
- (to_match.server_url, endpoint.server_url))
- def _discoverAndVerify(self, claimed_id, to_match_endpoints):
- """Given an endpoint object created from the information in an
- OpenID response, perform discovery and verify the discovery
- results, returning the matching endpoint that is the result of
- doing that discovery.
- @type to_match: openid.consumer.discover.OpenIDServiceEndpoint
- @param to_match: The endpoint whose information we're confirming
- @rtype: openid.consumer.discover.OpenIDServiceEndpoint
- @returns: The result of performing discovery on the claimed
- identifier in `to_match'
- @raises DiscoveryFailure: when discovery fails.
- """
- oidutil.log('Performing discovery on %s' % (claimed_id,))
- _, services = self._discover(claimed_id)
- if not services:
- raise DiscoveryFailure('No OpenID information found at %s' %
- (claimed_id,), None)
- return self._verifyDiscoveredServices(claimed_id, services,
- to_match_endpoints)
- def _verifyDiscoveredServices(self, claimed_id, services, to_match_endpoints):
- """See @L{_discoverAndVerify}"""
- # Search the services resulting from discovery to find one
- # that matches the information from the assertion
- failure_messages = []
- for endpoint in services:
- for to_match_endpoint in to_match_endpoints:
- try:
- self._verifyDiscoverySingle(
- endpoint, to_match_endpoint)
- except ProtocolError, why:
- failure_messages.append(str(why))
- else:
- # It matches, so discover verification has
- # succeeded. Return this endpoint.
- return endpoint
- else:
- oidutil.log('Discovery verification failure for %s' %
- (claimed_id,))
- for failure_message in failure_messages:
- oidutil.log(' * Endpoint mismatch: ' + failure_message)
- raise DiscoveryFailure(
- 'No matching endpoint found after discovering %s'
- % (claimed_id,), None)
- def _checkAuth(self, message, server_url):
- """Make a check_authentication request to verify this message.
- @returns: True if the request is valid.
- @rtype: bool
- """
- oidutil.log('Using OpenID check_authentication')
- request = self._createCheckAuthRequest(message)
- if request is None:
- return False
- try:
- response = self._makeKVPost(request, server_url)
- except (fetchers.HTTPFetchingError, ServerError), e:
- oidutil.log('check_authentication failed: %s' % (e[0],))
- return False
- else:
- return self._processCheckAuthResponse(response, server_url)
- def _createCheckAuthRequest(self, message):
- """Generate a check_authentication request message given an
- id_res message.
- """
- signed = message.getArg(OPENID_NS, 'signed')
- if signed:
- for k in signed.split(','):
- oidutil.log(k)
- val = message.getAliasedArg(k)
- # Signed value is missing
- if val is None:
- oidutil.log('Missing signed field %r' % (k,))
- return None
- check_auth_message = message.copy()
- check_auth_message.setArg(OPENID_NS, 'mode', 'check_authentication')
- return check_auth_message
- def _processCheckAuthResponse(self, response, server_url):
- """Process the response message from a check_authentication
- request, invalidating associations if requested.
- """
- is_valid = response.getArg(OPENID_NS, 'is_valid', 'false')
- invalidate_handle = response.getArg(OPENID_NS, 'invalidate_handle')
- if invalidate_handle is not None:
- oidutil.log(
- 'Received "invalidate_handle" from server %s' % (server_url,))
- if self.store is None:
- oidutil.log('Unexpectedly got invalidate_handle without '
- 'a store!')
- else:
- self.store.removeAssociation(server_url, invalidate_handle)
- if is_valid == 'true':
- return True
- else:
- oidutil.log('Server responds that checkAuth call is not valid')
- return False
- def _getAssociation(self, endpoint):
- """Get an association for the endpoint's server_url.
- First try seeing if we have a good association in the
- store. If we do not, then attempt to negotiate an association
- with the server.
- If we negotiate a good association, it will get stored.
- @returns: A valid association for the endpoint's server_url or None
- @rtype: openid.association.Association or NoneType
- """
- assoc = self.store.getAssociation(endpoint.server_url)
- if assoc is None or assoc.expiresIn <= 0:
- assoc = self._negotiateAssociation(endpoint)
- if assoc is not None:
- self.store.storeAssociation(endpoint.server_url, assoc)
- return assoc
- def _negotiateAssociation(self, endpoint):
- """Make association requests to the server, attempting to
- create a new association.
- @returns: a new association object
- @rtype: L{openid.association.Association}
- """
- # Get our preferred session/association type from the negotiatior.
- assoc_type, session_type = self.negotiator.getAllowedType()
- try:
- assoc = self._requestAssociation(
- endpoint, assoc_type, session_type)
- except ServerError, why:
- supportedTypes = self._extractSupportedAssociationType(why,
- endpoint,
- assoc_type)
- if supportedTypes is not None:
- assoc_type, session_type = supportedTypes
- # Attempt to create an association from the assoc_type
- # and session_type that the server told us it
- # supported.
- try:
- assoc = self._requestAssociation(
- endpoint, assoc_type, session_type)
- except ServerError, why:
- # Do not keep trying, since it rejected the
- # association type that it told us to use.
- oidutil.log('Server %s refused its suggested association '
- 'type: session_type=%s, assoc_type=%s'
- % (endpoint.server_url, session_type,
- assoc_type))
- return None
- else:
- return assoc
- else:
- return assoc
- def _extractSupportedAssociationType(self, server_error, endpoint,
- assoc_type):
- """Handle ServerErrors resulting from association requests.
- @returns: If server replied with an C{unsupported-type} error,
- return a tuple of supported C{association_type}, C{session_type}.
- Otherwise logs the error and returns None.
- @rtype: tuple or None
- """
- # Any error message whose code is not 'unsupported-type'
- # should be considered a total failure.
- if server_error.error_code != 'unsupported-type' or \
- server_error.message.isOpenID1():
- oidutil.log(
- 'Server error when requesting an association from %r: %s'
- % (endpoint.server_url, server_error.error_text))
- return None
- # The server didn't like the association/session type
- # that we sent, and it sent us back a message that
- # might tell us how to handle it.
- oidutil.log(
- 'Unsupported association type %s: %s' % (assoc_type,
- server_error.error_text,))
- # Extract the session_type and assoc_type from the
- # error message
- assoc_type = server_error.message.getArg(OPENID_NS, 'assoc_type')
- session_type = server_error.message.getArg(OPENID_NS, 'session_type')
- if assoc_type is None or session_type is None:
- oidutil.log('Server responded with unsupported association '
- 'session but did not supply a fallback.')
- return None
- elif not self.negotiator.isAllowed(assoc_type, session_type):
- fmt = ('Server sent unsupported session/association type: '
- 'session_type=%s, assoc_type=%s')
- oidutil.log(fmt % (session_type, assoc_type))
- return None
- else:
- return assoc_type, session_type
- def _requestAssociation(self, endpoint, assoc_type, session_type):
- """Make and process one association request to this endpoint's
- OP endpoint URL.
- @returns: An association object or None if the association
- processing failed.
- @raises ServerError: when the remote OpenID server returns an error.
- """
- assoc_session, args = self._createAssociateRequest(
- endpoint, assoc_type, session_type)
- try:
- response = self._makeKVPost(args, endpoint.server_url)
- except fetchers.HTTPFetchingError, why:
- oidutil.log('openid.associate request failed: %s' % (why[0],))
- return None
- try:
- assoc = self._extractAssociation(response, assoc_session)
- except KeyError, why:
- oidutil.log('Missing required parameter in response from %s: %s'
- % (endpoint.server_url, why[0]))
- return None
- except ProtocolError, why:
- oidutil.log('Protocol error parsing response from %s: %s' % (
- endpoint.server_url, why[0]))
- return None
- else:
- return assoc
- def _createAssociateRequest(self, endpoint, assoc_type, session_type):
- """Create an association request for the given assoc_type and
- session_type.
- @param endpoint: The endpoint whose server_url will be
- queried. The important bit about the endpoint is whether
- it's in compatiblity mode (OpenID 1.1)
- @param assoc_type: The association type that the request
- should ask for.
- @type assoc_type: str
- @param session_type: The session type that should be used in
- the association request. The session_type is used to
- create an association session object, and that session
- object is asked for any additional fields that it needs to
- add to the request.
- @type session_type: str
- @returns: a pair of the association session object and the
- request message that will be sent to the server.
- @rtype: (association session type (depends on session_type),
- openid.message.Message)
- """
- session_type_class = self.session_types[session_type]
- assoc_session = session_type_class()
- args = {
- 'mode': 'associate',
- 'assoc_type': assoc_type,
- }
- if not endpoint.compatibilityMode():
- args['ns'] = OPENID2_NS
- # Leave out the session type if we're in compatibility mode
- # *and* it's no-encryption.
- if (not endpoint.compatibilityMode() or
- assoc_session.session_type != 'no-encryption'):
- args['session_type'] = assoc_session.session_type
- args.update(assoc_session.getRequest())
- message = Message.fromOpenIDArgs(args)
- return assoc_session, message
- def _getOpenID1SessionType(self, assoc_response):
- """Given an association response message, extract the OpenID
- 1.X session type.
- This function mostly takes care of the 'no-encryption' default
- behavior in OpenID 1.
- If the association type is plain-text, this function will
- return 'no-encryption'
- @returns: The association type for this message
- @rtype: str
- @raises KeyError: when the session_type field is absent.
- """
- # If it's an OpenID 1 message, allow session_type to default
- # to None (which signifies "no-encryption")
- session_type = assoc_response.getArg(OPENID1_NS, 'session_type')
- # Handle the differences between no-encryption association
- # respones in OpenID 1 and 2:
- # no-encryption is not really a valid session type for
- # OpenID 1, but we'll accept it anyway, while issuing a
- # warning.
- if session_type == 'no-encryption':
- oidutil.log('WARNING: OpenID server sent "no-encryption"'
- 'for OpenID 1.X')
- # Missing or empty session type is the way to flag a
- # 'no-encryption' response. Change the session type to
- # 'no-encryption' so that it can be handled in the same
- # way as OpenID 2 'no-encryption' respones.
- elif session_type == '' or session_type is None:
- session_type = 'no-encryption'
- return session_type
- def _extractAssociation(self, assoc_response, assoc_session):
- """Attempt to extract an association from the response, given
- the association response message and the established
- association session.
- @param assoc_response: The association response message from
- the server
- @type assoc_response: openid.message.Message
- @param assoc_session: The association session object that was
- used when making the request
- @type assoc_session: depends on the session type of the request
- @raises ProtocolError: when data is malformed
- @raises KeyError: when a field is missing
- @rtype: openid.association.Association
- """
- # Extract the common fields from the response, raising an
- # exception if they are not found
- assoc_type = assoc_response.getArg(
- OPENID_NS, 'assoc_type', no_default)
- assoc_handle = assoc_response.getArg(
- OPENID_NS, 'assoc_handle', no_default)
- # expires_in is a base-10 string. The Python parsing will
- # accept literals that have whitespace around them and will
- # accept negative values. Neither of these are really in-spec,
- # but we think it's OK to accept them.
- expires_in_str = assoc_response.getArg(
- OPENID_NS, 'expires_in', no_default)
- try:
- expires_in = int(expires_in_str)
- except ValueError, why:
- raise ProtocolError('Invalid expires_in field: %s' % (why[0],))
- # OpenID 1 has funny association session behaviour.
- if assoc_response.isOpenID1():
- session_type = self._getOpenID1SessionType(assoc_response)
- else:
- session_type = assoc_response.getArg(
- OPENID2_NS, 'session_type', no_default)
- # Session type mismatch
- if assoc_session.session_type != session_type:
- if (assoc_response.isOpenID1() and
- session_type == 'no-encryption'):
- # In OpenID 1, any association request can result in a
- # 'no-encryption' association response. Setting
- # assoc_session to a new no-encryption session should
- # make the rest of this function work properly for
- # that case.
- assoc_session = PlainTextConsumerSession()
- else:
- # Any other mismatch, regardless of protocol version
- # results in the failure of the association session
- # altogether.
- fmt = 'Session type mismatch. Expected %r, got %r'
- message = fmt % (assoc_session.session_type, session_type)
- raise ProtocolError(message)
- # Make sure assoc_type is valid for session_type
- if assoc_type not in assoc_session.allowed_assoc_types:
- fmt = 'Unsupported assoc_type for session %s returned: %s'
- raise ProtocolError(fmt % (assoc_session.session_type, assoc_type))
- # Delegate to the association session to extract the secret
- # from the response, however is appropriate for that session
- # type.
- try:
- secret = assoc_session.extractSecret(assoc_response)
- except ValueError, why:
- fmt = 'Malformed response for %s session: %s'
- raise ProtocolError(fmt % (assoc_session.session_type, why[0]))
- return Association.fromExpiresIn(
- expires_in, assoc_handle, secret, assoc_type)
- class AuthRequest(object):
- """An object that holds the state necessary for generating an
- OpenID authentication request. This object holds the association
- with the server and the discovered information with which the
- request will be made.
- It is separate from the consumer because you may wish to add
- things to the request before sending it on its way to the
- server. It also has serialization options that let you encode the
- authentication request as a URL or as a form POST.
- """
- def __init__(self, endpoint, assoc):
- """
- Creates a new AuthRequest object. This just stores each
- argument in an appropriately named field.
- Users of this library should not create instances of this
- class. Instances of this class are created by the library
- when needed.
- """
- self.assoc = assoc
- self.endpoint = endpoint
- self.return_to_args = {}
- self.message = Message(endpoint.preferredNamespace())
- self._anonymous = False
- def setAnonymous(self, is_anonymous):
- """Set whether this request should be made anonymously. If a
- request is anonymous, the identifier will not be sent in the
- request. This is only useful if you are making another kind of
- request with an extension in this request.
- Anonymous requests are not allowed when the request is made
- with OpenID 1.
- @raises ValueError: when attempting to set an OpenID1 request
- as anonymous
- """
- if is_anonymous and self.message.isOpenID1():
- raise ValueError('OpenID 1 requests MUST include the '
- 'identifier in the request')
- else:
- self._anonymous = is_anonymous
- def addExtension(self, extension_request):
- """Add an extension to this checkid request.
- @param extension_request: An object that implements the
- extension interface for adding arguments to an OpenID
- message.
- """
- extension_request.toMessage(self.message)
- def addExtensionArg(self, namespace, key, value):
- """Add an extension argument to this OpenID authentication
- request.
- Use caution when adding arguments, because they will be
- URL-escaped and appended to the redirect URL, which can easily
- get quite long.
- @param namespace: The namespace for the extension. For
- example, the simple registration extension uses the
- namespace C{sreg}.
- @type namespace: str
- @param key: The key within the extension namespace. For
- example, the nickname field in the simple registration
- extension's key is C{nickname}.
- @type key: str
- @param value: The value to provide to the server for this
- argument.
- @type value: str
- """
- self.message.setArg(namespace, key, value)
- def getMessage(self, realm, return_to=None, immediate=False):
- """Produce a L{openid.message.Message} representing this request.
- @param realm: The URL (or URL pattern) that identifies your
- web site to the user when she is authorizing it.
- @type realm: str
- @param return_to: The URL that the OpenID provider will send the
- user back to after attempting to verify her identity.
- Not specifying a return_to URL means that the user will not
- be returned to the site issuing the request upon its
- completion.
- @type return_to: str
- @param immediate: If True, the OpenID provider is to send back
- a response immediately, useful for behind-the-scenes
- authentication attempts. Otherwise the OpenID provider
- may engage the user before providing a response. This is
- the default case, as the user may need to provide
- credentials or approve the request before a positive
- response can be sent.
- @type immediate: bool
- @returntype: L{openid.message.Message}
- """
- if return_to:
- return_to = oidutil.appendArgs(return_to, self.return_to_args)
- elif immediate:
- raise ValueError(
- '"return_to" is mandatory when using "checkid_immediate"')
- elif self.message.isOpenID1():
- raise ValueError('"return_to" is mandatory for OpenID 1 requests')
- elif self.return_to_args:
- raise ValueError('extra "return_to" arguments were specified, '
- 'but no return_to was specified')
- if immediate:
- mode = 'checkid_immediate'
- else:
- mode = 'checkid_setup'
- message = self.message.copy()
- if message.isOpenID1():
- realm_key = 'trust_root'
- else:
- realm_key = 'realm'
- message.updateArgs(OPENID_NS,
- {
- realm_key:realm,
- 'mode':mode,
- 'return_to':return_to,
- })
- if not self._anonymous:
- if self.endpoint.isOPIdentifier():
- # This will never happen when we're in compatibility
- # mode, as long as isOPIdentifier() returns False
- # whenever preferredNamespace() returns OPENID1_NS.
- claimed_id = request_identity = IDENTIFIER_SELECT
- else:
- request_identity = self.endpoint.getLocalID()
- claimed_id = self.endpoint.claimed_id
- # This is true for both OpenID 1 and 2
- message.setArg(OPENID_NS, 'identity', request_identity)
- if message.isOpenID2():
- message.setArg(OPENID2_NS, 'claimed_id', claimed_id)
- if self.assoc:
- message.setArg(OPENID_NS, 'assoc_handle', self.assoc.handle)
- assoc_log_msg = 'with assocication %s' % (self.assoc.handle,)
- else:
- assoc_log_msg = 'using stateless mode.'
- oidutil.log("Generated %s request to %s %s" %
- (mode, self.endpoint.server_url, assoc_log_msg))
- return message
- def redirectURL(self, realm, return_to=None, immediate=False):
- """Returns a URL with an encoded OpenID request.
- The resulting URL is the OpenID provider's endpoint URL with
- parameters appended as query arguments. You should redirect
- the user agent to this URL.
- OpenID 2.0 endpoints also accept POST requests, see
- C{L{shouldSendRedirect}} and C{L{formMarkup}}.
- @param realm: The URL (or URL pattern) that identifies your
- web site to the user when she is authorizing it.
- @type realm: str
- @param return_to: The URL that the OpenID provider will send the
- user back to after attempting to verify her identity.
- Not specifying a return_to URL means that the user will not
- be returned to the site issuing the request upon its
- completion.
- @type return_to: str
- @param immediate: If True, the OpenID provider is to send back
- a response immediately, useful for behind-the-scenes
- authentication attempts. Otherwise the OpenID provider
- may engage the user before providing a response. This is
- the default case, as the user may need to provide
- credentials or approve the request before a positive
- response can be sent.
- @type immediate: bool
- @returns: The URL to redirect the user agent to.
- @returntype: str
- """
- message = self.getMessage(realm, return_to, immediate)
- return message.toURL(self.endpoint.server_url)
- def formMarkup(self, realm, return_to=None, immediate=False,
- form_tag_attrs=None):
- """Get html for a form to submit this request to the IDP.
- @param form_tag_attrs: Dictionary of attributes to be added to
- the form tag. 'accept-charset' and 'enctype' have defaults
- that can be overridden. If a value is supplied for
- 'action' or 'method', it will be replaced.
- @type form_tag_attrs: {unicode: unicode}
- """
- message = self.getMessage(realm, return_to, immediate)
- return message.toFormMarkup(self.endpoint.server_url,
- form_tag_attrs)
- def htmlMarkup(self, realm, return_to=None, immediate=False,
- form_tag_attrs=None):
- """Get an autosubmitting HTML page that submits this request to the
- IDP. This is just a wrapper for formMarkup.
- @see: formMarkup
- @returns: str
- """
- return oidutil.autoSubmitHTML(self.formMarkup(realm,
- return_to,
- immediate,
- form_tag_attrs))
- def shouldSendRedirect(self):
- """Should this OpenID authentication request be sent as a HTTP
- redirect or as a POST (form submission)?
- @rtype: bool
- """
- return self.endpoint.compatibilityMode()
- FAILURE = 'failure'
- SUCCESS = 'success'
- CANCEL = 'cancel'
- SETUP_NEEDED = 'setup_needed'
- class Response(object):
- status = None
- def setEndpoint(self, endpoint):
- self.endpoint = endpoint
- if endpoint is None:
- self.identity_url = None
- else:
- self.identity_url = endpoint.claimed_id
- def getDisplayIdentifier(self):
- """Return the display identifier for this response.
- The display identifier is related to the Claimed Identifier, but the
- two are not always identical. The display identifier is something the
- user should recognize as what they entered, whereas the response's
- claimed identifier (in the L{identity_url} attribute) may have extra
- information for better persistence.
- URLs will be stripped of their fragments for display. XRIs will
- display the human-readable identifier (i-name) instead of the
- persistent identifier (i-number).
- Use the display identifier in your user interface. Use
- L{identity_url} for querying your database or authorization server.
- """
- if self.endpoint is not None:
- return self.endpoint.getDisplayIdentifier()
- return None
- class SuccessResponse(Response):
- """A response with a status of SUCCESS. Indicates that this request is a
- successful acknowledgement from the OpenID server that the
- supplied URL is, indeed controlled by the requesting agent.
- @ivar identity_url: The identity URL that has been authenticated; the Claimed Identifier.
- See also L{getDisplayIdentifier}.
- @ivar endpoint: The endpoint that authenticated the identifier. You
- may access other discovered information related to this endpoint,
- such as the CanonicalID of an XRI, through this object.
- @type endpoint: L{OpenIDServiceEndpoint<openid.consumer.discover.OpenIDServiceEndpoint>}
- @ivar signed_fields: The arguments in the server's response that
- were signed and verified.
- @cvar status: SUCCESS
- """
- status = SUCCESS
- def __init__(self, endpoint, message, signed_fields=None):
- # Don't use setEndpoint, because endpoint should never be None
- # for a successfull transaction.
- self.endpoint = endpoint
- self.identity_url = endpoint.claimed_id
- self.message = message
- if signed_fields is None:
- signed_fields = []
- self.signed_fields = signed_fields
- def isOpenID1(self):
- """Was this authentication response an OpenID 1 authentication
- response?
- """
- return self.message.isOpenID1()
- def isSigned(self, ns_uri, ns_key):
- """Return whether a particular key is signed, regardless of
- its namespace alias
- """
- return self.message.getKey(ns_uri, ns_key) in self.signed_fields
- def getSigned(self, ns_uri, ns_key, default=None):
- """Return the specified signed field if available,
- otherwise return default
- """
- if self.isSigned(ns_uri, ns_key):
- return self.message.getArg(ns_uri, ns_key, default)
- else:
- return default
- def getSignedNS(self, ns_uri):
- """Get signed arguments from the response message. Return a
- dict of all arguments in the specified namespace. If any of
- the arguments are not signed, return None.
- """
- msg_args = self.message.getArgs(ns_uri)
- for key in msg_args.iterkeys():
- if not self.isSigned(ns_uri, key):
- oidutil.log("SuccessResponse.getSignedNS: (%s, %s) not signed."
- % (ns_uri, key))
- return None
- return msg_args
- def extensionResponse(self, namespace_uri, require_signed):
- """Return response arguments in the specified namespace.
- @param namespace_uri: The namespace URI of the arguments to be
- returned.
- @param require_signed: True if the arguments should be among
- those signed in the response, False if you don't care.
- If require_signed is True and the arguments are not signed,
- return None.
- """
- if require_signed:
- return self.getSignedNS(namespace_uri)
- else:
- return self.message.getArgs(namespace_uri)
- def getReturnTo(self):
- """Get the openid.return_to argument from this response.
- This is useful for verifying that this request was initiated
- by this consumer.
- @returns: The return_to URL supplied to the server on the
- initial request, or C{None} if the response did not contain
- an C{openid.return_to} argument.
- @returntype: str
- """
- return self.getSigned(OPENID_NS, 'return_to')
- def __eq__(self, other):
- return (
- (self.endpoint == other.endpoint) and
- (self.identity_url == other.identity_url) and
- (self.message == other.message) and
- (self.signed_fields == other.signed_fields) and
- (self.status == other.status))
- def __ne__(self, other):
- return not (self == other)
- def __repr__(self):
- return '<%s.%s id=%r signed=%r>' % (
- self.__class__.__module__,
- self.__class__.__name__,
- self.identity_url, self.signed_fields)
- class FailureResponse(Response):
- """A response with a status of FAILURE. Indicates that the OpenID
- protocol has failed. This could be locally or remotely triggered.
- @ivar identity_url: The identity URL for which authenitcation was
- attempted, if it can be determined. Otherwise, None.
- @ivar message: A message indicating why the request failed, if one
- is supplied. otherwise, None.
- @cvar status: FAILURE
- """
- status = FAILURE
- def __init__(self, endpoint, message=None, contact=None,
- reference=None):
- self.setEndpoint(endpoint)
- self.message = message
- self.contact = contact
- self.reference = reference
- def __repr__(self):
- return "<%s.%s id=%r message=%r>" % (
- self.__class__.__module__, self.__class__.__name__,
- self.identity_url, self.message)
- class CancelResponse(Response):
- """A response with a status of CANCEL. Indicates that the user
- cancelled the OpenID authentication request.
- @ivar identity_url: The identity URL for which authenitcation was
- attempted, if it can be determined. Otherwise, None.
- @cvar status: CANCEL
- """
- status = CANCEL
- def __init__(self, endpoint):
- self.setEndpoint(endpoint)
- class SetupNeededResponse(Response):
- """A response with a status of SETUP_NEEDED. Indicates that the
- request was in immediate mode, and the server is unable to
- authenticate the user without further interaction.
- @ivar identity_url: The identity URL for which authenitcation was
- attempted.
- @ivar setup_url: A URL that can be used to send the user to the
- server to set up for authentication. The user should be
- redirected in to the setup_url, either in the current window
- or in a new browser window. C{None} in OpenID 2.0.
- @cvar status: SETUP_NEEDED
- """
- status = SETUP_NEEDED
- def __init__(self, endpoint, setup_url=None):
- self.setEndpoint(endpoint)
- self.setup_url = setup_url
|