~gary/python-openid/python-openid-2.2.1-patched

« back to all changes in this revision

Viewing changes to openid/consumer/consumer.py

  • Committer: Launchpad Patch Queue Manager
  • Date: 2007-11-30 02:46:28 UTC
  • mfrom: (1.1.1 pyopenid-2.0)
  • Revision ID: launchpad@pqm.canonical.com-20071130024628-qktwsew3383iawmq
[rs=SteveA] upgrade to python-openid-2.0.1

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# -*- test-case-name: openid.test.consumer -*-
2
 
"""
 
1
# -*- test-case-name: openid.test.test_consumer -*-
 
2
"""OpenID support for Relying Parties (aka Consumers).
 
3
 
3
4
This module documents the main interface with the OpenID consumer
4
5
library.  The only part of the library which has to be used and isn't
5
6
documented in full here is the store required to create an
18
19
        1. The user enters their OpenID into a field on the consumer's
19
20
           site, and hits a login button.
20
21
 
21
 
        2. The consumer site discovers the user's OpenID server using
22
 
           the YADIS protocol.
 
22
        2. The consumer site discovers the user's OpenID provider using
 
23
           the Yadis protocol.
23
24
 
24
25
        3. The consumer site sends the browser a redirect to the
25
 
           identity server.  This is the authentication request as
 
26
           OpenID provider.  This is the authentication request as
26
27
           described in the OpenID specification.
27
28
 
28
 
        4. The identity server's site sends the browser a redirect
 
29
        4. The OpenID provider's site sends the browser a redirect
29
30
           back to the consumer site.  This redirect contains the
30
 
           server's response to the authentication request.
 
31
           provider's response to the authentication request.
31
32
 
32
33
    The most important part of the flow to note is the consumer's site
33
34
    must handle two separate HTTP requests in order to perform the
60
61
    appropriate for the action the site wants to take.
61
62
 
62
63
 
63
 
STORES AND DUMB MODE
64
 
====================
65
 
 
66
 
    OpenID is a protocol that works best when the consumer site is
67
 
    able to store some state.  This is the normal mode of operation
68
 
    for the protocol, and is sometimes referred to as smart mode.
69
 
    There is also a fallback mode, known as dumb mode, which is
70
 
    available when the consumer site is not able to store state.  This
71
 
    mode should be avoided when possible, as it leaves the
72
 
    implementation more vulnerable to replay attacks.
73
 
 
74
 
    The mode the library works in for normal operation is determined
75
 
    by the store that it is given.  The store is an abstraction that
76
 
    handles the data that the consumer needs to manage between http
77
 
    requests in order to operate efficiently and securely.
 
64
SESSIONS, STORES, AND STATELESS MODE
 
65
====================================
 
66
 
 
67
    The C{L{Consumer}} object keeps track of two types of state:
 
68
 
 
69
        1. State of the user's current authentication attempt.  Things like
 
70
           the identity URL, the list of endpoints discovered for that
 
71
           URL, and in case where some endpoints are unreachable, the list
 
72
           of endpoints already tried.  This state needs to be held from
 
73
           Consumer.begin() to Consumer.complete(), but it is only applicable
 
74
           to a single session with a single user agent, and at the end of
 
75
           the authentication process (i.e. when an OP replies with either
 
76
           C{id_res} or C{cancel}) it may be discarded.
 
77
 
 
78
        2. State of relationships with servers, i.e. shared secrets
 
79
           (associations) with servers and nonces seen on signed messages.
 
80
           This information should persist from one session to the next and
 
81
           should not be bound to a particular user-agent.
 
82
 
 
83
 
 
84
    These two types of storage are reflected in the first two arguments of
 
85
    Consumer's constructor, C{session} and C{store}.  C{session} is a
 
86
    dict-like object and we hope your web framework provides you with one
 
87
    of these bound to the user agent.  C{store} is an instance of
 
88
    L{openid.store.interface.OpenIDStore}.
 
89
 
 
90
    Since the store does hold secrets shared between your application and the
 
91
    OpenID provider, you should be careful about how you use it in a shared
 
92
    hosting environment.  If the filesystem or database permissions of your
 
93
    web host allow strangers to read from them, do not store your data there!
 
94
    If you have no safe place to store your data, construct your consumer
 
95
    with C{None} for the store, and it will operate only in stateless mode.
 
96
    Stateless mode may be slower, put more load on the OpenID provider, and
 
97
    trusts the provider to keep you safe from replay attacks.
 
98
 
78
99
 
79
100
    Several store implementation are provided, and the interface is
80
101
    fully documented so that custom stores can be used as well.  See
84
105
    in several different ways, including several SQL databases and
85
106
    normal files on disk.
86
107
 
87
 
    There is an additional concrete store provided that puts the
88
 
    system in dumb mode.  This is not recommended, as it removes the
89
 
    library's ability to stop replay attacks reliably.  It still uses
90
 
    time-based checking to make replay attacks only possible within a
91
 
    small window, but they remain possible within that window.  This
92
 
    store should only be used if the consumer site has no way to
93
 
    retain data between requests at all.
94
 
 
95
108
 
96
109
IMMEDIATE MODE
97
110
==============
98
111
 
99
112
    In the flow described above, the user may need to confirm to the
100
 
    identity server that it's ok to authorize his or her identity.
101
 
    The server may draw pages asking for information from the user
 
113
    OpenID provider that it's ok to disclose his or her identity.
 
114
    The provider may draw pages asking for information from the user
102
115
    before it redirects the browser back to the consumer's site.  This
103
116
    is generally transparent to the consumer site, so it is typically
104
117
    ignored as an implementation detail.
108
121
    put the library in immediate mode.  In immediate mode, there is an
109
122
    extra response possible from the server, which is essentially the
110
123
    server reporting that it doesn't have enough information to answer
111
 
    the question yet.  In addition to saying that, the identity server
112
 
    provides a URL to which the user can be sent to provide the needed
113
 
    information and let the server finish handling the original
114
 
    request.
 
124
    the question yet.
115
125
 
116
126
 
117
127
USING THIS LIBRARY
125
135
    is entered in that field and the form is submitted, it should make
126
136
    a request to the your site which includes that OpenID URL.
127
137
 
128
 
    First, the application should instantiate the C{L{Consumer}} class
129
 
    using the store of choice.  If the application has any sort of
130
 
    session framework that provides per-client state management, a
131
 
    dict-like object to access the session should be passed as the
132
 
    optional second parameter.  The library just expects the session
133
 
    object to support a C{dict}-like interface, if it is provided.
 
138
    First, the application should L{instantiate a Consumer<Consumer.__init__>}
 
139
    with a session for per-user state and store for shared state.
 
140
    using the store of choice.
134
141
 
135
 
    Next, the application should call the 'begin' method on the
 
142
    Next, the application should call the 'C{L{begin<Consumer.begin>}}' method on the
136
143
    C{L{Consumer}} instance.  This method takes the OpenID URL.  The
137
144
    C{L{begin<Consumer.begin>}} method returns an C{L{AuthRequest}}
138
145
    object.
141
148
    C{L{redirectURL<AuthRequest.redirectURL>}} method on the
142
149
    C{L{AuthRequest}} object.  The parameter C{return_to} is the URL
143
150
    that the OpenID server will send the user back to after attempting
144
 
    to verify his or her identity.  The C{trust_root} parameter is the
 
151
    to verify his or her identity.  The C{realm} parameter is the
145
152
    URL (or URL pattern) that identifies your web site to the user
146
153
    when he or she is authorizing it.  Send a redirect to the
147
154
    resulting URL to the user's browser.
148
155
 
149
156
    That's the first half of the authentication process.  The second
150
 
    half of the process is done after the user's ID server sends the
 
157
    half of the process is done after the user's OpenID Provider sends the
151
158
    user's browser a redirect back to your site to complete their
152
159
    login.
153
160
 
155
162
    given as the C{return_to} URL to the
156
163
    C{L{redirectURL<AuthRequest.redirectURL>}} call made
157
164
    above.  The request will have several query parameters added to
158
 
    the URL by the identity server as the information necessary to
 
165
    the URL by the OpenID provider as the information necessary to
159
166
    finish the request.
160
167
 
161
 
    Get an C{L{Consumer}} instance, and call its
162
 
    C{L{complete<Consumer.complete>}} method, passing in all the
163
 
    received query arguments.
 
168
    Get an C{L{Consumer}} instance with the same session and store as
 
169
    before and call its C{L{complete<Consumer.complete>}} method,
 
170
    passing in all the received query arguments.
164
171
 
165
172
    There are multiple possible return types possible from that
166
173
    method. These indicate the whether or not the login was
181
188
    objects.
182
189
"""
183
190
 
184
 
import string
185
 
import time
186
 
import urllib
187
191
import cgi
 
192
import copy
188
193
from urlparse import urlparse
189
194
 
190
 
from urljr import fetchers
 
195
from openid import fetchers
191
196
 
192
 
from openid.consumer.discover import discover as openIDDiscover
193
 
from openid.consumer.discover import discoverXRI
194
 
from openid.consumer.discover import yadis_available, DiscoveryFailure
 
197
from openid.consumer.discover import discover, OpenIDServiceEndpoint, \
 
198
     DiscoveryFailure, OPENID_1_0_TYPE, OPENID_1_1_TYPE, OPENID_2_0_TYPE
 
199
from openid.message import Message, OPENID_NS, OPENID2_NS, OPENID1_NS, \
 
200
     IDENTIFIER_SELECT, no_default
195
201
from openid import cryptutil
196
 
from openid import kvform
197
202
from openid import oidutil
198
 
from openid.association import Association
 
203
from openid.association import Association, default_negotiator, \
 
204
     SessionNegotiator
199
205
from openid.dh import DiffieHellman
 
206
from openid.store.nonce import mkNonce, split as splitNonce
 
207
from openid.yadis.manager import Discovery
 
208
 
200
209
 
201
210
__all__ = ['AuthRequest', 'Consumer', 'SuccessResponse',
202
211
           'SetupNeededResponse', 'CancelResponse', 'FailureResponse',
203
212
           'SUCCESS', 'FAILURE', 'CANCEL', 'SETUP_NEEDED',
204
213
           ]
205
214
 
206
 
if yadis_available:
207
 
    from yadis.manager import Discovery
208
 
    from yadis import xri
 
215
def makeKVPost(request_message, server_url):
 
216
    """Make a Direct Request to an OpenID Provider and return the
 
217
    result as a Message object.
 
218
 
 
219
    @raises openid.fetchers.HTTPFetchingError: if an error is
 
220
        encountered in making the HTTP post.
 
221
 
 
222
    @rtype: L{openid.message.Message}
 
223
    """
 
224
    # XXX: TESTME
 
225
    resp = fetchers.fetch(server_url, body=request_message.toURLEncoded())
 
226
 
 
227
    response_message = Message.fromKVForm(resp.body)
 
228
    if resp.status == 400:
 
229
        raise ServerError.fromMessage(response_message)
 
230
 
 
231
    elif resp.status != 200:
 
232
        fmt = 'bad status code from server %s: %s'
 
233
        error_message = fmt % (server_url, resp.status)
 
234
        raise fetchers.HTTPFetchingError(error_message)
 
235
 
 
236
    return response_message
 
237
 
209
238
 
210
239
class Consumer(object):
211
240
    """An OpenID consumer implementation that performs discovery and
228
257
 
229
258
    _token = 'last_token'
230
259
 
231
 
    def __init__(self, session, store):
 
260
    _discover = staticmethod(discover)
 
261
 
 
262
    def __init__(self, session, store, consumer_class=None):
232
263
        """Initialize a Consumer instance.
233
264
 
234
265
        You should create a new instance of the Consumer object with
247
278
        @see: L{openid.store}
248
279
        """
249
280
        self.session = session
250
 
        self.consumer = GenericConsumer(store)
 
281
        if consumer_class is None:
 
282
            consumer_class = GenericConsumer
 
283
        self.consumer = consumer_class(store)
251
284
        self._token_key = self.session_key_prefix + self._token
252
285
 
253
 
    def begin(self, user_url):
 
286
    def begin(self, user_url, anonymous=False):
254
287
        """Start the OpenID authentication process. See steps 1-2 in
255
288
        the overview at the top of this file.
256
289
 
261
294
            normalizing and resolving any redirects the server might
262
295
            issue.
263
296
 
264
 
        @type user_url: str
 
297
        @type user_url: unicode
 
298
 
 
299
        @param anonymous: Whether to make an anonymous request of the OpenID
 
300
            provider.  Such a request does not ask for an authorization
 
301
            assertion for an OpenID identifier, but may be used with
 
302
            extensions to pass other data.  e.g. "I don't care who you are,
 
303
            but I'd like to know your time zone."
 
304
 
 
305
        @type anonymous: bool
265
306
 
266
307
        @returns: An object containing the discovered information will
267
308
            be returned, with a method for building a redirect URL to
278
319
            is available, L{openid.consumer.discover.DiscoveryFailure} is
279
320
            an alias for C{yadis.discover.DiscoveryFailure}.
280
321
        """
281
 
        if yadis_available and xri.identifierScheme(user_url) == "XRI":
282
 
            discoverMethod = discoverXRI
283
 
            openid_url = user_url
284
 
        else:
285
 
            discoverMethod = openIDDiscover
286
 
            openid_url = oidutil.normalizeUrl(user_url)
287
 
 
288
 
        if yadis_available:
289
 
            try:
290
 
                disco = Discovery(self.session,
291
 
                                  openid_url,
292
 
                                  self.session_key_prefix)
293
 
                service = disco.getNextService(discoverMethod)
294
 
            except fetchers.HTTPFetchingError, e:
295
 
                raise DiscoveryFailure('Error fetching XRDS document', e)
296
 
        else:
297
 
            # XXX - Untested branch!
298
 
            _, services = openIDDiscover(user_url)
299
 
            if not services:
300
 
                service = None
301
 
            else:
302
 
                service = services[0]
 
322
        disco = Discovery(self.session, user_url, self.session_key_prefix)
 
323
        try:
 
324
            service = disco.getNextService(self._discover)
 
325
        except fetchers.HTTPFetchingError, why:
 
326
            raise DiscoveryFailure(
 
327
                'Error fetching XRDS document: %s' % (why[0],), None)
303
328
 
304
329
        if service is None:
305
330
            raise DiscoveryFailure(
306
 
                'No usable OpenID services found for %s' % (openid_url,), None)
 
331
                'No usable OpenID services found for %s' % (user_url,), None)
307
332
        else:
308
 
            return self.beginWithoutDiscovery(service)
 
333
            return self.beginWithoutDiscovery(service, anonymous)
309
334
 
310
 
    def beginWithoutDiscovery(self, service):
 
335
    def beginWithoutDiscovery(self, service, anonymous=False):
311
336
        """Start OpenID verification without doing OpenID server
312
337
        discovery. This method is used internally by Consumer.begin
313
338
        after discovery is performed, and exists to provide an
330
355
        """
331
356
        auth_req = self.consumer.begin(service)
332
357
        self.session[self._token_key] = auth_req.endpoint
 
358
 
 
359
        try:
 
360
            auth_req.setAnonymous(anonymous)
 
361
        except ValueError, why:
 
362
            raise ProtocolError(str(why))
 
363
 
333
364
        return auth_req
334
365
 
335
 
    def complete(self, query):
 
366
    def complete(self, query, return_to=None):
336
367
        """Called to interpret the server's response to an OpenID
337
368
        request. It is called in step 4 of the flow described in the
338
369
        consumer overview.
340
371
        @param query: A dictionary of the query parameters for this
341
372
            HTTP request.
342
373
 
 
374
        @param return_to: The return URL used to invoke the
 
375
            application.  Extract the URL from your application's web
 
376
            request framework and specify it here to have it checked
 
377
            against the openid.return_to value in the response.  If
 
378
            the return_to URL check fails, the status of the
 
379
            completion will be FAILURE.
 
380
 
343
381
        @returns: a subclass of Response. The type of response is
344
382
            indicated by the status attribute, which will be one of
345
383
            SUCCESS, CANCEL, FAILURE, or SETUP_NEEDED.
354
392
        if endpoint is None:
355
393
            response = FailureResponse(None, 'No session state found')
356
394
        else:
357
 
            response = self.consumer.complete(query, endpoint)
 
395
            message = Message.fromPostArgs(query)
 
396
            response = self.consumer.complete(message, endpoint, return_to)
358
397
            del self.session[self._token_key]
359
398
 
360
399
        if (response.status in ['success', 'cancel'] and
361
 
            yadis_available and
362
400
            response.identity_url is not None):
363
401
 
364
402
            disco = Discovery(self.session,
370
408
 
371
409
        return response
372
410
 
373
 
class DiffieHellmanConsumerSession(object):
 
411
    def setAssociationPreference(self, association_preferences):
 
412
        """Set the order in which association types/sessions should be
 
413
        attempted. For instance, to only allow HMAC-SHA256
 
414
        associations created with a DH-SHA256 association session:
 
415
 
 
416
        >>> consumer.setAssociationPreference([('HMAC-SHA256', 'DH-SHA256')])
 
417
 
 
418
        Any association type/association type pair that is not in this
 
419
        list will not be attempted at all.
 
420
 
 
421
        @param association_preferences: The list of allowed
 
422
            (association type, association session type) pairs that
 
423
            should be allowed for this consumer to use, in order from
 
424
            most preferred to least preferred.
 
425
        @type association_preferences: [(str, str)]
 
426
 
 
427
        @returns: None
 
428
 
 
429
        @see: C{L{openid.association.SessionNegotiator}}
 
430
        """
 
431
        self.consumer.negotiator = SessionNegotiator(association_preferences)
 
432
 
 
433
class DiffieHellmanSHA1ConsumerSession(object):
374
434
    session_type = 'DH-SHA1'
 
435
    hash_func = staticmethod(cryptutil.sha1)
 
436
    secret_size = 20
 
437
    allowed_assoc_types = ['HMAC-SHA1']
375
438
 
376
439
    def __init__(self, dh=None):
377
440
        if dh is None:
382
445
    def getRequest(self):
383
446
        cpub = cryptutil.longToBase64(self.dh.public)
384
447
 
385
 
        args = {'openid.dh_consumer_public': cpub}
 
448
        args = {'dh_consumer_public': cpub}
386
449
 
387
450
        if not self.dh.usingDefaultValues():
388
451
            args.update({
389
 
                'openid.dh_modulus': cryptutil.longToBase64(self.dh.modulus),
390
 
                'openid.dh_gen': cryptutil.longToBase64(self.dh.generator),
 
452
                'dh_modulus': cryptutil.longToBase64(self.dh.modulus),
 
453
                'dh_gen': cryptutil.longToBase64(self.dh.generator),
391
454
                })
392
455
 
393
456
        return args
394
457
 
395
458
    def extractSecret(self, response):
396
 
        spub = cryptutil.base64ToLong(response['dh_server_public'])
397
 
        enc_mac_key = oidutil.fromBase64(response['enc_mac_key'])
398
 
        return self.dh.xorSecret(spub, enc_mac_key)
 
459
        dh_server_public64 = response.getArg(
 
460
            OPENID_NS, 'dh_server_public', no_default)
 
461
        enc_mac_key64 = response.getArg(OPENID_NS, 'enc_mac_key', no_default)
 
462
        dh_server_public = cryptutil.base64ToLong(dh_server_public64)
 
463
        enc_mac_key = oidutil.fromBase64(enc_mac_key64)
 
464
        return self.dh.xorSecret(dh_server_public, enc_mac_key, self.hash_func)
 
465
 
 
466
class DiffieHellmanSHA256ConsumerSession(DiffieHellmanSHA1ConsumerSession):
 
467
    session_type = 'DH-SHA256'
 
468
    hash_func = staticmethod(cryptutil.sha256)
 
469
    secret_size = 32
 
470
    allowed_assoc_types = ['HMAC-SHA256']
399
471
 
400
472
class PlainTextConsumerSession(object):
401
 
    session_type = None
 
473
    session_type = 'no-encryption'
 
474
    allowed_assoc_types = ['HMAC-SHA1', 'HMAC-SHA256']
402
475
 
403
476
    def getRequest(self):
404
477
        return {}
405
478
 
406
479
    def extractSecret(self, response):
407
 
        return oidutil.fromBase64(response['mac_key'])
 
480
        mac_key64 = response.getArg(OPENID_NS, 'mac_key', no_default)
 
481
        return oidutil.fromBase64(mac_key64)
 
482
 
 
483
class SetupNeededError(Exception):
 
484
    """Internally-used exception that indicates that an immediate-mode
 
485
    request cancelled."""
 
486
    def __init__(self, user_setup_url=None):
 
487
        Exception.__init__(self, user_setup_url)
 
488
        self.user_setup_url = user_setup_url
 
489
 
 
490
class ProtocolError(ValueError):
 
491
    """Exception that indicates that a message violated the
 
492
    protocol. It is raised and caught internally to this file."""
 
493
 
 
494
class TypeURIMismatch(ProtocolError):
 
495
    """A protocol error arising from type URIs mismatching
 
496
    """
 
497
 
 
498
class ServerError(Exception):
 
499
    """Exception that is raised when the server returns a 400 response
 
500
    code to a direct request."""
 
501
 
 
502
    def __init__(self, error_text, error_code, message):
 
503
        Exception.__init__(self, error_text)
 
504
        self.error_text = error_text
 
505
        self.error_code = error_code
 
506
        self.message = message
 
507
 
 
508
    def fromMessage(cls, message):
 
509
        """Generate a ServerError instance, extracting the error text
 
510
        and the error code from the message."""
 
511
        error_text = message.getArg(
 
512
            OPENID_NS, 'error', '<no error message supplied>')
 
513
        error_code = message.getArg(OPENID_NS, 'error_code')
 
514
        return cls(error_text, error_code, message)
 
515
 
 
516
    fromMessage = classmethod(fromMessage)
408
517
 
409
518
class GenericConsumer(object):
410
519
    """This is the implementation of the common logic for OpenID
411
520
    consumers. It is unaware of the application in which it is
412
521
    running.
 
522
 
 
523
    @ivar negotiator: An object that controls the kind of associations
 
524
        that the consumer makes. It defaults to
 
525
        C{L{openid.association.default_negotiator}}. Assign a
 
526
        different negotiator to it if you have specific requirements
 
527
        for how associations are made.
 
528
    @type negotiator: C{L{openid.association.SessionNegotiator}}
413
529
    """
414
530
 
415
 
    NONCE_LEN = 8
416
 
    NONCE_CHRS = string.ascii_letters + string.digits
 
531
    # The name of the query parameter that gets added to the return_to
 
532
    # URL when using OpenID1. You can change this value if you want or
 
533
    # need a different name, but don't make it start with openid,
 
534
    # because it's not a standard protocol thing for OpenID1. For
 
535
    # OpenID2, the library will take care of the nonce using standard
 
536
    # OpenID query parameter names.
 
537
    openid1_nonce_query_arg_name = 'janrain_nonce'
 
538
 
 
539
    session_types = {
 
540
        'DH-SHA1':DiffieHellmanSHA1ConsumerSession,
 
541
        'DH-SHA256':DiffieHellmanSHA256ConsumerSession,
 
542
        'no-encryption':PlainTextConsumerSession,
 
543
        }
 
544
 
 
545
    _discover = staticmethod(discover)
417
546
 
418
547
    def __init__(self, store):
419
548
        self.store = store
 
549
        self.negotiator = default_negotiator.copy()
420
550
 
421
551
    def begin(self, service_endpoint):
422
 
        nonce = self._createNonce()
423
 
        assoc = self._getAssociation(service_endpoint.server_url)
 
552
        """Create an AuthRequest object for the specified
 
553
        service_endpoint. This method will create an association if
 
554
        necessary."""
 
555
        if self.store is None:
 
556
            assoc = None
 
557
        else:
 
558
            assoc = self._getAssociation(service_endpoint)
 
559
 
424
560
        request = AuthRequest(service_endpoint, assoc)
425
 
        request.return_to_args['nonce'] = nonce
 
561
        request.return_to_args[self.openid1_nonce_query_arg_name] = mkNonce()
426
562
        return request
427
563
 
428
 
    def complete(self, query, endpoint):
429
 
        mode = query.get('openid.mode', '<no mode specified>')
 
564
    def complete(self, message, endpoint, return_to=None):
 
565
        """Process the OpenID message, using the specified endpoint
 
566
        and return_to URL as context. This method will handle any
 
567
        OpenID message that is sent to the return_to URL.
 
568
        """
 
569
        mode = message.getArg(OPENID_NS, 'mode', '<No mode set>')
430
570
 
431
 
        if isinstance(mode, list):
432
 
            raise TypeError("query dict must have one value for each key, "
433
 
                            "not lists of values.  Query is %r" % (query,))
 
571
        if return_to is not None:
 
572
            if not self._checkReturnTo(message, return_to):
 
573
                return FailureResponse(endpoint,
 
574
                                       "openid.return_to does not match return URL")
434
575
 
435
576
        if mode == 'cancel':
436
577
            return CancelResponse(endpoint)
437
578
        elif mode == 'error':
438
 
            error = query.get('openid.error')
439
 
            return FailureResponse(endpoint, error)
 
579
            error = message.getArg(OPENID_NS, 'error')
 
580
            contact = message.getArg(OPENID_NS, 'contact')
 
581
            reference = message.getArg(OPENID_NS, 'reference')
 
582
 
 
583
            return FailureResponse(endpoint, error, contact=contact,
 
584
                                   reference=reference)
 
585
        elif message.isOpenID2() and mode == 'setup_needed':
 
586
            return SetupNeededResponse(endpoint)
 
587
 
440
588
        elif mode == 'id_res':
441
 
            if endpoint.identity_url is None:
442
 
                return FailureResponse(endpoint, 'No session state found')
443
589
            try:
444
 
                response = self._doIdRes(query, endpoint)
445
 
            except fetchers.HTTPFetchingError, why:
446
 
                message = 'HTTP request failed: %s' % (str(why),)
447
 
                return FailureResponse(endpoint, message)
 
590
                self._checkSetupNeeded(message)
 
591
            except SetupNeededError, why:
 
592
                return SetupNeededResponse(endpoint, why.user_setup_url)
448
593
            else:
449
 
                if response.status == 'success':
450
 
                    return self._checkNonce(response, query.get('nonce'))
451
 
                else:
452
 
                    return response
 
594
                try:
 
595
                    return self._doIdRes(message, endpoint)
 
596
                except (ProtocolError, DiscoveryFailure), why:
 
597
                    return FailureResponse(endpoint, why[0])
453
598
        else:
454
599
            return FailureResponse(endpoint,
455
600
                                   'Invalid openid.mode: %r' % (mode,))
456
601
 
457
 
    def _checkNonce(self, response, nonce):
458
 
        parsed_url = urlparse(response.getReturnTo())
 
602
    def _checkReturnTo(self, message, return_to):
 
603
        """Check an OpenID message and its openid.return_to value
 
604
        against a return_to URL from an application.  Return True on
 
605
        success, False on failure.
 
606
        """
 
607
        # Check the openid.return_to args against args in the original
 
608
        # message.
 
609
        try:
 
610
            self._verifyReturnToArgs(message.toPostArgs())
 
611
        except ProtocolError, why:
 
612
            oidutil.log("Verifying return_to arguments: %s" % (why[0],))
 
613
            return False
 
614
 
 
615
        # Check the return_to base URL against the one in the message.
 
616
        msg_return_to = message.getArg(OPENID_NS, 'return_to')
 
617
 
 
618
        # The URL scheme, authority, and path MUST be the same between
 
619
        # the two URLs.
 
620
        app_parts = urlparse(return_to)
 
621
        msg_parts = urlparse(msg_return_to)
 
622
 
 
623
        # (addressing scheme, network location, path) must be equal in
 
624
        # both URLs.
 
625
        for part in range(0, 3):
 
626
            if app_parts[part] != msg_parts[part]:
 
627
                return False
 
628
 
 
629
        return True
 
630
 
 
631
    _makeKVPost = staticmethod(makeKVPost)
 
632
 
 
633
    def _checkSetupNeeded(self, message):
 
634
        """Check an id_res message to see if it is a
 
635
        checkid_immediate cancel response.
 
636
 
 
637
        @raises SetupNeededError: if it is a checkid_immediate cancellation
 
638
        """
 
639
        # In OpenID 1, we check to see if this is a cancel from
 
640
        # immediate mode by the presence of the user_setup_url
 
641
        # parameter.
 
642
        if message.isOpenID1():
 
643
            user_setup_url = message.getArg(OPENID1_NS, 'user_setup_url')
 
644
            if user_setup_url is not None:
 
645
                raise SetupNeededError(user_setup_url)
 
646
 
 
647
    def _doIdRes(self, message, endpoint):
 
648
        """Handle id_res responses that are not cancellations of
 
649
        immediate mode requests.
 
650
 
 
651
        @param message: the response paramaters.
 
652
        @param endpoint: the discovered endpoint object. May be None.
 
653
 
 
654
        @raises ProtocolError: If the message contents are not
 
655
            well-formed according to the OpenID specification. This
 
656
            includes missing fields or not signing fields that should
 
657
            be signed.
 
658
 
 
659
        @raises DiscoveryFailure: If the subject of the id_res message
 
660
            does not match the supplied endpoint, and discovery on the
 
661
            identifier in the message fails (this should only happen
 
662
            when using OpenID 2)
 
663
 
 
664
        @returntype: L{Response}
 
665
        """
 
666
        signed_list_str = message.getArg(OPENID_NS, 'signed')
 
667
        if signed_list_str is None:
 
668
            raise ProtocolError("Response missing signed list")
 
669
 
 
670
        signed_list = signed_list_str.split(',')
 
671
 
 
672
        # Checks for presence of appropriate fields (and checks
 
673
        # signed list fields)
 
674
        self._idResCheckForFields(message, signed_list)
 
675
 
 
676
        # Verify discovery information:
 
677
        endpoint = self._verifyDiscoveryResults(message, endpoint)
 
678
 
 
679
        self._idResCheckSignature(message, endpoint.server_url)
 
680
 
 
681
        response_identity = message.getArg(OPENID_NS, 'identity')
 
682
 
 
683
        # Will raise a ProtocolError if the nonce is bad
 
684
        self._idResCheckNonce(message, endpoint)
 
685
 
 
686
        signed_fields = ["openid." + s for s in signed_list]
 
687
        return SuccessResponse(endpoint, message, signed_fields)
 
688
 
 
689
    def _idResGetNonceOpenID1(self, message, endpoint):
 
690
        """Extract the nonce from an OpenID 1 response
 
691
 
 
692
        See the openid1_nonce_query_arg_name class variable
 
693
 
 
694
        @returns: The nonce as a string or None
 
695
        """
 
696
        return_to = message.getArg(OPENID1_NS, 'return_to', None)
 
697
        if return_to is None:
 
698
            return None
 
699
 
 
700
        parsed_url = urlparse(return_to)
459
701
        query = parsed_url[4]
460
702
        for k, v in cgi.parse_qsl(query):
461
 
            if k == 'nonce':
462
 
                if v != nonce:
463
 
                    return FailureResponse(response, 'Nonce mismatch')
464
 
                else:
465
 
                    break
466
 
        else:
467
 
            return FailureResponse(response, 'Nonce missing from return_to: %r'
468
 
                                   % (response.getReturnTo()))
469
 
 
470
 
        # The nonce matches the signed nonce in the openid.return_to
471
 
        # response parameter
472
 
        if not self.store.useNonce(nonce):
473
 
            return FailureResponse(response,
474
 
                                   'Nonce missing from store')
475
 
 
476
 
        # If the nonce check succeeded, return the original success
477
 
        # response
478
 
        return response
479
 
 
480
 
    def _createNonce(self):
481
 
        nonce = cryptutil.randomString(self.NONCE_LEN, self.NONCE_CHRS)
482
 
        self.store.storeNonce(nonce)
483
 
        return nonce
484
 
 
485
 
    def _makeKVPost(self, args, server_url):
486
 
        mode = args['openid.mode']
487
 
        body = urllib.urlencode(args)
488
 
 
489
 
        resp = fetchers.fetch(server_url, body=body)
490
 
        if resp is None:
491
 
            fmt = 'openid.mode=%s: failed to fetch URL: %s'
492
 
            oidutil.log(fmt % (mode, server_url))
493
 
            return None
494
 
 
495
 
        response = kvform.kvToDict(resp.body)
496
 
        if resp.status == 400:
497
 
            server_error = response.get('error', '<no message from server>')
498
 
            fmt = 'openid.mode=%s: error returned from server %s: %s'
499
 
            oidutil.log(fmt % (mode, server_url, server_error))
500
 
            return None
501
 
        elif resp.status != 200:
502
 
            fmt = 'openid.mode=%s: bad status code from server %s: %s'
503
 
            oidutil.log(fmt % (mode, server_url, resp.status))
504
 
            return None
505
 
 
506
 
        return response
507
 
 
508
 
    def _doIdRes(self, query, endpoint):
509
 
        """Handle id_res responses.
510
 
 
511
 
        @param query: the response paramaters.
512
 
        @param consumer_id: The normalized Claimed Identifier.
513
 
        @param server_id: The Delegate Identifier.
514
 
        @param server_url: OpenID server endpoint URL.
515
 
 
516
 
        @returntype: L{Response}
517
 
        """
518
 
        user_setup_url = query.get('openid.user_setup_url')
519
 
        if user_setup_url is not None:
520
 
            return SetupNeededResponse(endpoint, user_setup_url)
521
 
 
522
 
        return_to = query.get('openid.return_to')
523
 
        server_id2 = query.get('openid.identity')
524
 
        assoc_handle = query.get('openid.assoc_handle')
525
 
 
526
 
        if return_to is None or server_id2 is None or assoc_handle is None:
527
 
            return FailureResponse(endpoint, 'Missing required field')
528
 
 
529
 
        if endpoint.getServerID() != server_id2:
530
 
            return FailureResponse(endpoint, 'Server ID (delegate) mismatch')
531
 
 
532
 
        signed = query.get('openid.signed')
533
 
 
534
 
        assoc = self.store.getAssociation(endpoint.server_url, assoc_handle)
535
 
 
536
 
        if assoc is None:
537
 
            # It's not an association we know about.  Dumb mode is our
 
703
            if k == self.openid1_nonce_query_arg_name:
 
704
                return v
 
705
 
 
706
        return None
 
707
 
 
708
    def _idResCheckNonce(self, message, endpoint):
 
709
        if message.isOpenID1():
 
710
            # This indicates that the nonce was generated by the consumer
 
711
            nonce = self._idResGetNonceOpenID1(message, endpoint)
 
712
            server_url = ''
 
713
        else:
 
714
            nonce = message.getArg(OPENID2_NS, 'response_nonce')
 
715
            server_url = endpoint.server_url
 
716
 
 
717
        if nonce is None:
 
718
            raise ProtocolError('Nonce missing from response')
 
719
 
 
720
        try:
 
721
            timestamp, salt = splitNonce(nonce)
 
722
        except ValueError, why:
 
723
            raise ProtocolError('Malformed nonce: %s' % (why[0],))
 
724
 
 
725
        if (self.store is not None and
 
726
            not self.store.useNonce(server_url, timestamp, salt)):
 
727
            raise ProtocolError('Nonce already used or out of range')
 
728
 
 
729
    def _idResCheckSignature(self, message, server_url):
 
730
        assoc_handle = message.getArg(OPENID_NS, 'assoc_handle')
 
731
        if self.store is None:
 
732
            assoc = None
 
733
        else:
 
734
            assoc = self.store.getAssociation(server_url, assoc_handle)
 
735
 
 
736
        if assoc:
 
737
            if assoc.getExpiresIn() <= 0:
 
738
                # XXX: It might be a good idea sometimes to re-start the
 
739
                # authentication with a new association. Doing it
 
740
                # automatically opens the possibility for
 
741
                # denial-of-service by a server that just returns expired
 
742
                # associations (or really short-lived associations)
 
743
                raise ProtocolError(
 
744
                    'Association with %s expired' % (server_url,))
 
745
 
 
746
            if not assoc.checkMessageSignature(message):
 
747
                raise ProtocolError('Bad signature')
 
748
 
 
749
        else:
 
750
            # It's not an association we know about.  Stateless mode is our
538
751
            # only possible path for recovery.
539
 
            if self._checkAuth(query, endpoint.server_url):
540
 
                return SuccessResponse.fromQuery(endpoint, query, signed)
 
752
            # XXX - async framework will not want to block on this call to
 
753
            # _checkAuth.
 
754
            if not self._checkAuth(message, server_url):
 
755
                raise ProtocolError('Server denied check_authentication')
 
756
 
 
757
    def _idResCheckForFields(self, message, signed_list):
 
758
        # XXX: this should be handled by the code that processes the
 
759
        # response (that is, if a field is missing, we should not have
 
760
        # to explicitly check that it's present, just make sure that
 
761
        # the fields are actually being used by the rest of the code
 
762
        # in tests). Although, which fields are signed does need to be
 
763
        # checked somewhere.
 
764
        basic_fields = ['return_to', 'assoc_handle', 'sig']
 
765
        basic_sig_fields = ['return_to', 'identity']
 
766
 
 
767
        require_fields = {
 
768
            OPENID2_NS: basic_fields + ['op_endpoint'],
 
769
            OPENID1_NS: basic_fields + ['identity'],
 
770
            }
 
771
 
 
772
        require_sigs = {
 
773
            OPENID2_NS: basic_sig_fields + ['response_nonce',
 
774
                                            'claimed_id',
 
775
                                            'assoc_handle',],
 
776
            OPENID1_NS: basic_sig_fields,
 
777
            }
 
778
 
 
779
        for field in require_fields[message.getOpenIDNamespace()]:
 
780
            if not message.hasKey(OPENID_NS, field):
 
781
                raise ProtocolError('Missing required field %r' % (field,))
 
782
 
 
783
        for field in require_sigs[message.getOpenIDNamespace()]:
 
784
            # Field is present and not in signed list
 
785
            if message.hasKey(OPENID_NS, field) and field not in signed_list:
 
786
                raise ProtocolError('"%s" not signed' % (field,))
 
787
 
 
788
 
 
789
    def _verifyReturnToArgs(query):
 
790
        """Verify that the arguments in the return_to URL are present in this
 
791
        response.
 
792
        """
 
793
        message = Message.fromPostArgs(query)
 
794
        return_to = message.getArg(OPENID_NS, 'return_to')
 
795
 
 
796
        # XXX: this should be checked by _idResCheckForFields
 
797
        if not return_to:
 
798
            raise ProtocolError("no openid.return_to in query %r" % (query,))
 
799
        parsed_url = urlparse(return_to)
 
800
        rt_query = parsed_url[4]
 
801
        for rt_key, rt_value in cgi.parse_qsl(rt_query):
 
802
            try:
 
803
                value = query[rt_key]
 
804
                if rt_value != value:
 
805
                    format = ("parameter %s value %r does not match "
 
806
                              "return_to's value %r")
 
807
                    raise ProtocolError(format % (rt_key, value, rt_value))
 
808
            except KeyError:
 
809
                format = "return_to parameter %s absent from query %r"
 
810
                raise ProtocolError(format % (rt_key, query))
 
811
 
 
812
    _verifyReturnToArgs = staticmethod(_verifyReturnToArgs)
 
813
 
 
814
    def _verifyDiscoveryResults(self, resp_msg, endpoint=None):
 
815
        """
 
816
        Extract the information from an OpenID assertion message and
 
817
        verify it against the original
 
818
 
 
819
        @param endpoint: The endpoint that resulted from doing discovery
 
820
        @param resp_msg: The id_res message object
 
821
        """
 
822
        if resp_msg.getOpenIDNamespace() == OPENID2_NS:
 
823
            return self._verifyDiscoveryResultsOpenID2(resp_msg, endpoint)
 
824
        else:
 
825
            return self._verifyDiscoveryResultsOpenID1(resp_msg, endpoint)
 
826
 
 
827
 
 
828
    def _verifyDiscoveryResultsOpenID2(self, resp_msg, endpoint):
 
829
        to_match = OpenIDServiceEndpoint()
 
830
        to_match.type_uris = [OPENID_2_0_TYPE]
 
831
        to_match.claimed_id = resp_msg.getArg(OPENID2_NS, 'claimed_id')
 
832
        to_match.local_id = resp_msg.getArg(OPENID2_NS, 'identity')
 
833
 
 
834
        # Raises a KeyError when the op_endpoint is not present
 
835
        to_match.server_url = resp_msg.getArg(
 
836
            OPENID2_NS, 'op_endpoint', no_default)
 
837
 
 
838
        # claimed_id and identifier must both be present or both
 
839
        # be absent
 
840
        if (to_match.claimed_id is None and
 
841
            to_match.local_id is not None):
 
842
            raise ProtocolError(
 
843
                'openid.identity is present without openid.claimed_id')
 
844
 
 
845
        elif (to_match.claimed_id is not None and
 
846
              to_match.local_id is None):
 
847
            raise ProtocolError(
 
848
                'openid.claimed_id is present without openid.identity')
 
849
 
 
850
        # This is a response without identifiers, so there's really no
 
851
        # checking that we can do, so return an endpoint that's for
 
852
        # the specified `openid.op_endpoint'
 
853
        elif to_match.claimed_id is None:
 
854
            return OpenIDServiceEndpoint.fromOPEndpointURL(to_match.server_url)
 
855
 
 
856
        # The claimed ID doesn't match, so we have to do discovery
 
857
        # again. This covers not using sessions, OP identifier
 
858
        # endpoints and responses that didn't match the original
 
859
        # request.
 
860
        elif not endpoint:
 
861
            oidutil.log('No pre-discovered information supplied.')
 
862
            return self._discoverAndVerify(to_match)
 
863
 
 
864
        elif to_match.claimed_id != endpoint.claimed_id:
 
865
            oidutil.log('Mismatched pre-discovered session data. '
 
866
                        'Claimed ID in session=%s, in assertion=%s' %
 
867
                        (endpoint.claimed_id, to_match.claimed_id))
 
868
            return self._discoverAndVerify(to_match)
 
869
 
 
870
        # The claimed ID matches, so we use the endpoint that we
 
871
        # discovered in initiation. This should be the most common
 
872
        # case.
 
873
        else:
 
874
            self._verifyDiscoverySingle(endpoint, to_match)
 
875
            return endpoint
 
876
 
 
877
    def _verifyDiscoveryResultsOpenID1(self, resp_msg, endpoint):
 
878
        if endpoint is None:
 
879
            raise RuntimeError(
 
880
                'When using OpenID 1, the claimed ID must be supplied, '
 
881
                'either by passing it through as a return_to parameter '
 
882
                'or by using a session, and supplied to the GenericConsumer '
 
883
                'as the argument to complete()')
 
884
 
 
885
        to_match = OpenIDServiceEndpoint()
 
886
        to_match.type_uris = [OPENID_1_1_TYPE]
 
887
        to_match.local_id = resp_msg.getArg(OPENID1_NS, 'identity')
 
888
        # Restore delegate information from the initiation phase
 
889
        to_match.claimed_id = endpoint.claimed_id
 
890
 
 
891
        if to_match.local_id is None:
 
892
            raise ProtocolError('Missing required field openid.identity')
 
893
 
 
894
        to_match_1_0 = copy.copy(to_match)
 
895
        to_match_1_0.type_uris = [OPENID_1_0_TYPE]
 
896
 
 
897
        try:
 
898
            self._verifyDiscoverySingle(endpoint, to_match)
 
899
        except TypeURIMismatch:
 
900
            self._verifyDiscoverySingle(endpoint, to_match_1_0)
 
901
 
 
902
        return endpoint
 
903
 
 
904
    def _verifyDiscoverySingle(self, endpoint, to_match):
 
905
        """Verify that the given endpoint matches the information
 
906
        extracted from the OpenID assertion, and raise an exception if
 
907
        there is a mismatch.
 
908
 
 
909
        @type endpoint: openid.consumer.discover.OpenIDServiceEndpoint
 
910
        @type to_match: openid.consumer.discover.OpenIDServiceEndpoint
 
911
 
 
912
        @rtype: NoneType
 
913
 
 
914
        @raises ProtocolError: when the endpoint does not match the
 
915
            discovered information.
 
916
        """
 
917
        # Every type URI that's in the to_match endpoint has to be
 
918
        # present in the discovered endpoint.
 
919
        for type_uri in to_match.type_uris:
 
920
            if not endpoint.usesExtension(type_uri):
 
921
                raise TypeURIMismatch(
 
922
                    'Required type %r not present' % (type_uri,))
 
923
 
 
924
        if to_match.claimed_id != endpoint.claimed_id:
 
925
            raise ProtocolError(
 
926
                'Claimed ID does not match (different subjects!), '
 
927
                'Expected %s, got %s' %
 
928
                (to_match.claimed_id, endpoint.claimed_id))
 
929
 
 
930
        if to_match.getLocalID() != endpoint.getLocalID():
 
931
            raise ProtocolError('local_id mismatch. Expected %s, got %s' %
 
932
                                (to_match.getLocalID(), endpoint.getLocalID()))
 
933
 
 
934
        # If the server URL is None, this must be an OpenID 1
 
935
        # response, because op_endpoint is a required parameter in
 
936
        # OpenID 2. In that case, we don't actually care what the
 
937
        # discovered server_url is, because signature checking or
 
938
        # check_auth should take care of that check for us.
 
939
        if to_match.server_url is None:
 
940
            assert to_match.preferredNamespace() == OPENID1_NS, (
 
941
                """The code calling this must ensure that OpenID 2
 
942
                responses have a non-none `openid.op_endpoint' and
 
943
                that it is set as the `server_url' attribute of the
 
944
                `to_match' endpoint.""")
 
945
 
 
946
        elif to_match.server_url != endpoint.server_url:
 
947
            raise ProtocolError('OP Endpoint mismatch. Expected %s, got %s' %
 
948
                                (to_match.server_url, endpoint.server_url))
 
949
 
 
950
    def _discoverAndVerify(self, to_match):
 
951
        """Given an endpoint object created from the information in an
 
952
        OpenID response, perform discovery and verify the discovery
 
953
        results, returning the matching endpoint that is the result of
 
954
        doing that discovery.
 
955
 
 
956
        @type to_match: openid.consumer.discover.OpenIDServiceEndpoint
 
957
        @param to_match: The endpoint whose information we're confirming
 
958
 
 
959
        @rtype: openid.consumer.discover.OpenIDServiceEndpoint
 
960
        @returns: The result of performing discovery on the claimed
 
961
            identifier in `to_match'
 
962
 
 
963
        @raises ProtocolError: when discovery fails.
 
964
        """
 
965
        oidutil.log('Performing discovery on %s' % (to_match.claimed_id,))
 
966
        _, services = self._discover(to_match.claimed_id)
 
967
        if not services:
 
968
            raise DiscoveryFailure('No OpenID information found at %s' %
 
969
                                   (to_match.claimed_id,), None)
 
970
 
 
971
        # Search the services resulting from discovery to find one
 
972
        # that matches the information from the assertion
 
973
        failure_messages = []
 
974
        for endpoint in services:
 
975
            try:
 
976
                self._verifyDiscoverySingle(endpoint, to_match)
 
977
            except ProtocolError, why:
 
978
                failure_messages.append(why[0])
541
979
            else:
542
 
                return FailureResponse(endpoint,
543
 
                                       'Server denied check_authentication')
544
 
 
545
 
        if assoc.expiresIn <= 0:
546
 
            # XXX: It might be a good idea sometimes to re-start the
547
 
            # authentication with a new association. Doing it
548
 
            # automatically opens the possibility for
549
 
            # denial-of-service by a server that just returns expired
550
 
            # associations (or really short-lived associations)
551
 
            msg = 'Association with %s expired' % (endpoint.server_url,)
552
 
            return FailureResponse(endpoint, msg)
553
 
 
554
 
        # Check the signature
555
 
        sig = query.get('openid.sig')
556
 
        if sig is None or signed is None:
557
 
            return FailureResponse(endpoint, 'Missing argument signature')
558
 
 
559
 
        signed_list = signed.split(',')
560
 
 
561
 
        # Fail if the identity field is present but not signed
562
 
        if endpoint.identity_url is not None and 'identity' not in signed_list:
563
 
            msg = '"openid.identity" not signed'
564
 
            return FailureResponse(endpoint, msg)
565
 
 
566
 
        v_sig = assoc.signDict(signed_list, query)
567
 
 
568
 
        if v_sig != sig:
569
 
            return FailureResponse(endpoint, 'Bad signature')
570
 
 
571
 
        return SuccessResponse.fromQuery(endpoint, query, signed)
572
 
 
573
 
    def _checkAuth(self, query, server_url):
574
 
        request = self._createCheckAuthRequest(query)
 
980
                # It matches, so discover verification has
 
981
                # succeeded. Return this endpoint.
 
982
                return endpoint
 
983
        else:
 
984
            oidutil.log('Discovery verification failure for %s' %
 
985
                        (to_match.claimed_id,))
 
986
            for failure_message in failure_messages:
 
987
                oidutil.log(' * Endpoint mismatch: ' + failure_message)
 
988
 
 
989
            raise DiscoveryFailure(
 
990
                'No matching endpoint found after discovering %s'
 
991
                % (to_match.claimed_id,), None)
 
992
 
 
993
    def _checkAuth(self, message, server_url):
 
994
        oidutil.log('Using OpenID check_authentication')
 
995
        request = self._createCheckAuthRequest(message)
575
996
        if request is None:
576
997
            return False
577
 
        response = self._makeKVPost(request, server_url)
578
 
        if response is None:
 
998
        try:
 
999
            response = self._makeKVPost(request, server_url)
 
1000
        except (fetchers.HTTPFetchingError, ServerError), e:
 
1001
            oidutil.log('check_authentication failed: %s' % (e[0],))
579
1002
            return False
580
 
        return self._processCheckAuthResponse(response, server_url)
581
 
 
582
 
    def _createCheckAuthRequest(self, query):
583
 
        signed = query.get('openid.signed')
584
 
        if signed is None:
585
 
            oidutil.log('No signature present; checkAuth aborted')
586
 
            return None
587
 
 
 
1003
        else:
 
1004
            return self._processCheckAuthResponse(response, server_url)
 
1005
 
 
1006
    def _createCheckAuthRequest(self, message):
 
1007
        """Generate a check_authentication request message given an
 
1008
        id_res message.
 
1009
        """
588
1010
        # Arguments that are always passed to the server and not
589
1011
        # included in the signature.
590
1012
        whitelist = ['assoc_handle', 'sig', 'signed', 'invalidate_handle']
591
 
        signed = signed.split(',') + whitelist
592
 
 
593
 
        check_args = dict([(k, v) for k, v in query.iteritems()
594
 
                           if k.startswith('openid.') and k[7:] in signed])
595
 
 
596
 
        check_args['openid.mode'] = 'check_authentication'
597
 
        return check_args
 
1013
 
 
1014
        check_args = {}
 
1015
        for k in whitelist:
 
1016
            val = message.getArg(OPENID_NS, k)
 
1017
            if val is not None:
 
1018
                check_args[k] = val
 
1019
 
 
1020
        signed = message.getArg(OPENID_NS, 'signed')
 
1021
        if signed:
 
1022
            for k in signed.split(','):
 
1023
                if k == 'ns':
 
1024
                    check_args['ns'] = message.getOpenIDNamespace()
 
1025
                    continue
 
1026
 
 
1027
                val = message.getAliasedArg(k)
 
1028
 
 
1029
                # Signed value is missing
 
1030
                if val is None:
 
1031
                    oidutil.log('Missing signed field %r' % (k,))
 
1032
                    return None
 
1033
 
 
1034
                check_args[k] = val
 
1035
 
 
1036
        check_args['mode'] = 'check_authentication'
 
1037
        return Message.fromOpenIDArgs(check_args)
598
1038
 
599
1039
    def _processCheckAuthResponse(self, response, server_url):
600
 
        is_valid = response.get('is_valid', 'false')
 
1040
        """Process the response message from a check_authentication
 
1041
        request, invalidating associations if requested.
 
1042
        """
 
1043
        is_valid = response.getArg(OPENID_NS, 'is_valid', 'false')
601
1044
 
602
 
        invalidate_handle = response.get('invalidate_handle')
 
1045
        invalidate_handle = response.getArg(OPENID_NS, 'invalidate_handle')
603
1046
        if invalidate_handle is not None:
604
 
            self.store.removeAssociation(server_url, invalidate_handle)
 
1047
            oidutil.log(
 
1048
                'Received "invalidate_handle" from server %s' % (server_url,))
 
1049
            if self.store is None:
 
1050
                oidutil.log('Unexpectedly got invalidate_handle without '
 
1051
                            'a store!')
 
1052
            else:
 
1053
                self.store.removeAssociation(server_url, invalidate_handle)
605
1054
 
606
1055
        if is_valid == 'true':
607
1056
            return True
609
1058
            oidutil.log('Server responds that checkAuth call is not valid')
610
1059
            return False
611
1060
 
612
 
    def _getAssociation(self, server_url):
613
 
        if self.store.isDumb():
614
 
            return None
615
 
 
616
 
        assoc = self.store.getAssociation(server_url)
 
1061
    def _getAssociation(self, endpoint):
 
1062
        """Get an association for the endpoint's server_url.
 
1063
 
 
1064
        First try seeing if we have a good association in the
 
1065
        store. If we do not, then attempt to negotiate an association
 
1066
        with the server.
 
1067
 
 
1068
        If we negotiate a good association, it will get stored.
 
1069
 
 
1070
        @returns: A valid association for the endpoint's server_url or None
 
1071
        @rtype: openid.association.Association or NoneType
 
1072
        """
 
1073
        assoc = self.store.getAssociation(endpoint.server_url)
617
1074
 
618
1075
        if assoc is None or assoc.expiresIn <= 0:
619
 
            assoc_session, args = self._createAssociateRequest(server_url)
620
 
            try:
621
 
                response = self._makeKVPost(args, server_url)
622
 
            except fetchers.HTTPFetchingError, why:
623
 
                oidutil.log('openid.associate request failed: %s' %
624
 
                            (str(why),))
625
 
                assoc = None
 
1076
            assoc = self._negotiateAssociation(endpoint)
 
1077
            if assoc is not None:
 
1078
                self.store.storeAssociation(endpoint.server_url, assoc)
 
1079
 
 
1080
        return assoc
 
1081
 
 
1082
    def _negotiateAssociation(self, endpoint):
 
1083
        """Make association requests to the server, attempting to
 
1084
        create a new association.
 
1085
 
 
1086
        @returns: a new association object
 
1087
 
 
1088
        @rtype: openid.association.Association
 
1089
 
 
1090
        @raises Exception: errors that the fetcher might raise. These are
 
1091
            intended to be propagated up to the library's entrance point.
 
1092
        """
 
1093
        # Get our preferred session/association type from the negotiatior.
 
1094
        assoc_type, session_type = self.negotiator.getAllowedType()
 
1095
 
 
1096
        try:
 
1097
            assoc = self._requestAssociation(
 
1098
                endpoint, assoc_type, session_type)
 
1099
        except ServerError, why:
 
1100
            # Any error message whose code is not 'unsupported-type'
 
1101
            # should be considered a total failure.
 
1102
            if why.error_code != 'unsupported-type' or \
 
1103
                   why.message.isOpenID1():
 
1104
                oidutil.log(
 
1105
                    'Server error when requesting an association from %r: %s'
 
1106
                    % (endpoint.server_url, why.error_text))
 
1107
                return None
 
1108
 
 
1109
            # The server didn't like the association/session type
 
1110
            # that we sent, and it sent us back a message that
 
1111
            # might tell us how to handle it.
 
1112
            oidutil.log(
 
1113
                'Unsupported association type %s: %s' % (assoc_type,
 
1114
                                                         why.error_text,))
 
1115
 
 
1116
            # Extract the session_type and assoc_type from the
 
1117
            # error message
 
1118
            assoc_type = why.message.getArg(OPENID_NS, 'assoc_type')
 
1119
            session_type = why.message.getArg(OPENID_NS, 'session_type')
 
1120
 
 
1121
            if assoc_type is None or session_type is None:
 
1122
                oidutil.log('Server responded with unsupported association '
 
1123
                            'session but did not supply a fallback.')
 
1124
                return None
 
1125
            elif not self.negotiator.isAllowed(assoc_type, session_type):
 
1126
                fmt = ('Server sent unsupported session/association type: '
 
1127
                       'session_type=%s, assoc_type=%s')
 
1128
                oidutil.log(fmt % (session_type, assoc_type))
 
1129
                return None
626
1130
            else:
627
 
                assoc = self._parseAssociation(
628
 
                    response, assoc_session, server_url)
629
 
 
630
 
        return assoc
631
 
 
632
 
    def _createAssociateRequest(self, server_url):
633
 
        proto = urlparse(server_url)[0]
634
 
        if proto == 'https':
635
 
            session_type = PlainTextConsumerSession
636
 
        else:
637
 
            session_type = DiffieHellmanConsumerSession
638
 
 
639
 
        assoc_session = session_type()
 
1131
                # Attempt to create an association from the assoc_type
 
1132
                # and session_type that the server told us it
 
1133
                # supported.
 
1134
                try:
 
1135
                    assoc = self._requestAssociation(
 
1136
                        endpoint, assoc_type, session_type)
 
1137
                except ServerError, why:
 
1138
                    # Do not keep trying, since it rejected the
 
1139
                    # association type that it told us to use.
 
1140
                    oidutil.log('Server %s refused its suggested association '
 
1141
                                'type: session_type=%s, assoc_type=%s'
 
1142
                                % (endpoint.server_url, session_type,
 
1143
                                   assoc_type))
 
1144
                    return None
 
1145
                else:
 
1146
                    return assoc
 
1147
        else:
 
1148
            return assoc
 
1149
 
 
1150
    def _requestAssociation(self, endpoint, assoc_type, session_type):
 
1151
        """Make and process one association request to this endpoint's
 
1152
        OP endpoint URL.
 
1153
 
 
1154
        @returns: An association object or None if the association
 
1155
            processing failed.
 
1156
 
 
1157
        @raises ServerError: when the remote OpenID server returns an error.
 
1158
        """
 
1159
        assoc_session, args = self._createAssociateRequest(
 
1160
            endpoint, assoc_type, session_type)
 
1161
 
 
1162
        try:
 
1163
            response = self._makeKVPost(args, endpoint.server_url)
 
1164
        except fetchers.HTTPFetchingError, why:
 
1165
            oidutil.log('openid.associate request failed: %s' % (why[0],))
 
1166
            return None
 
1167
 
 
1168
        try:
 
1169
            assoc = self._extractAssociation(response, assoc_session)
 
1170
        except KeyError, why:
 
1171
            oidutil.log('Missing required parameter in response from %s: %s'
 
1172
                        % (endpoint.server_url, why[0]))
 
1173
            return None
 
1174
        except ProtocolError, why:
 
1175
            oidutil.log('Protocol error parsing response from %s: %s' % (
 
1176
                endpoint.server_url, why[0]))
 
1177
            return None
 
1178
        else:
 
1179
            return assoc
 
1180
 
 
1181
    def _createAssociateRequest(self, endpoint, assoc_type, session_type):
 
1182
        """Create an association request for the given assoc_type and
 
1183
        session_type.
 
1184
 
 
1185
        @param endpoint: The endpoint whose server_url will be
 
1186
            queried. The important bit about the endpoint is whether
 
1187
            it's in compatiblity mode (OpenID 1.1)
 
1188
 
 
1189
        @param assoc_type: The association type that the request
 
1190
            should ask for.
 
1191
        @type assoc_type: str
 
1192
 
 
1193
        @param session_type: The session type that should be used in
 
1194
            the association request. The session_type is used to
 
1195
            create an association session object, and that session
 
1196
            object is asked for any additional fields that it needs to
 
1197
            add to the request.
 
1198
        @type session_type: str
 
1199
 
 
1200
        @returns: a pair of the association session object and the
 
1201
            request message that will be sent to the server.
 
1202
        @rtype: (association session type (depends on session_type),
 
1203
                 openid.message.Message)
 
1204
        """
 
1205
        session_type_class = self.session_types[session_type]
 
1206
        assoc_session = session_type_class()
640
1207
 
641
1208
        args = {
642
 
            'openid.mode': 'associate',
643
 
            'openid.assoc_type':'HMAC-SHA1',
 
1209
            'mode': 'associate',
 
1210
            'assoc_type': assoc_type,
644
1211
            }
645
1212
 
646
 
        if assoc_session.session_type is not None:
647
 
            args['openid.session_type'] = assoc_session.session_type
 
1213
        if not endpoint.compatibilityMode():
 
1214
            args['ns'] = OPENID2_NS
 
1215
 
 
1216
        # Leave out the session type if we're in compatibility mode
 
1217
        # *and* it's no-encryption.
 
1218
        if (not endpoint.compatibilityMode() or
 
1219
            assoc_session.session_type != 'no-encryption'):
 
1220
            args['session_type'] = assoc_session.session_type
648
1221
 
649
1222
        args.update(assoc_session.getRequest())
650
 
        return assoc_session, args
651
 
 
652
 
    def _parseAssociation(self, results, assoc_session, server_url):
653
 
        try:
654
 
            assoc_type = results['assoc_type']
655
 
            assoc_handle = results['assoc_handle']
656
 
            expires_in_str = results['expires_in']
657
 
        except KeyError, e:
658
 
            fmt = 'Getting association: missing key in response from %s: %s'
659
 
            oidutil.log(fmt % (server_url, e[0]))
660
 
            return None
661
 
 
662
 
        if assoc_type != 'HMAC-SHA1':
663
 
            fmt = 'Unsupported assoc_type returned from server %s: %s'
664
 
            oidutil.log(fmt % (server_url, assoc_type))
665
 
            return None
666
 
 
 
1223
        message = Message.fromOpenIDArgs(args)
 
1224
        return assoc_session, message
 
1225
 
 
1226
    def _getOpenID1SessionType(self, assoc_response):
 
1227
        """Given an association response message, extract the OpenID
 
1228
        1.X session type.
 
1229
 
 
1230
        This function mostly takes care of the 'no-encryption' default
 
1231
        behavior in OpenID 1.
 
1232
 
 
1233
        If the association type is plain-text, this function will
 
1234
        return 'no-encryption'
 
1235
 
 
1236
        @returns: The association type for this message
 
1237
        @rtype: str
 
1238
 
 
1239
        @raises KeyError: when the session_type field is absent.
 
1240
        """
 
1241
        # If it's an OpenID 1 message, allow session_type to default
 
1242
        # to None (which signifies "no-encryption")
 
1243
        session_type = assoc_response.getArg(OPENID1_NS, 'session_type')
 
1244
 
 
1245
        # Handle the differences between no-encryption association
 
1246
        # respones in OpenID 1 and 2:
 
1247
 
 
1248
        # no-encryption is not really a valid session type for
 
1249
        # OpenID 1, but we'll accept it anyway, while issuing a
 
1250
        # warning.
 
1251
        if session_type == 'no-encryption':
 
1252
            oidutil.log('WARNING: OpenID server sent "no-encryption"'
 
1253
                        'for OpenID 1.X')
 
1254
 
 
1255
        # Missing or empty session type is the way to flag a
 
1256
        # 'no-encryption' response. Change the session type to
 
1257
        # 'no-encryption' so that it can be handled in the same
 
1258
        # way as OpenID 2 'no-encryption' respones.
 
1259
        elif session_type == '' or session_type is None:
 
1260
            session_type = 'no-encryption'
 
1261
 
 
1262
        return session_type
 
1263
 
 
1264
    def _extractAssociation(self, assoc_response, assoc_session):
 
1265
        """Attempt to extract an association from the response, given
 
1266
        the association response message and the established
 
1267
        association session.
 
1268
 
 
1269
        @param assoc_response: The association response message from
 
1270
            the server
 
1271
        @type assoc_response: openid.message.Message
 
1272
 
 
1273
        @param assoc_session: The association session object that was
 
1274
            used when making the request
 
1275
        @type assoc_session: depends on the session type of the request
 
1276
 
 
1277
        @raises ProtocolError: when data is malformed
 
1278
        @raises KeyError: when a field is missing
 
1279
 
 
1280
        @rtype: openid.association.Association
 
1281
        """
 
1282
        # Extract the common fields from the response, raising an
 
1283
        # exception if they are not found
 
1284
        assoc_type = assoc_response.getArg(
 
1285
            OPENID_NS, 'assoc_type', no_default)
 
1286
        assoc_handle = assoc_response.getArg(
 
1287
            OPENID_NS, 'assoc_handle', no_default)
 
1288
 
 
1289
        # expires_in is a base-10 string. The Python parsing will
 
1290
        # accept literals that have whitespace around them and will
 
1291
        # accept negative values. Neither of these are really in-spec,
 
1292
        # but we think it's OK to accept them.
 
1293
        expires_in_str = assoc_response.getArg(
 
1294
            OPENID_NS, 'expires_in', no_default)
667
1295
        try:
668
1296
            expires_in = int(expires_in_str)
669
 
        except ValueError, e:
670
 
            fmt = 'Getting Association: invalid expires_in field: %s'
671
 
            oidutil.log(fmt % (e[0],))
672
 
            return None
673
 
 
674
 
        session_type = results.get('session_type')
675
 
        if session_type != assoc_session.session_type:
676
 
            if session_type is None:
677
 
                oidutil.log('Falling back to plain text association '
678
 
                            'session from %s' % assoc_session.session_type)
 
1297
        except ValueError, why:
 
1298
            raise ProtocolError('Invalid expires_in field: %s' % (why[0],))
 
1299
 
 
1300
        # OpenID 1 has funny association session behaviour.
 
1301
        if assoc_response.isOpenID1():
 
1302
            session_type = self._getOpenID1SessionType(assoc_response)
 
1303
        else:
 
1304
            session_type = assoc_response.getArg(
 
1305
                OPENID2_NS, 'session_type', no_default)
 
1306
 
 
1307
        # Session type mismatch
 
1308
        if assoc_session.session_type != session_type:
 
1309
            if (assoc_response.isOpenID1() and
 
1310
                session_type == 'no-encryption'):
 
1311
                # In OpenID 1, any association request can result in a
 
1312
                # 'no-encryption' association response. Setting
 
1313
                # assoc_session to a new no-encryption session should
 
1314
                # make the rest of this function work properly for
 
1315
                # that case.
679
1316
                assoc_session = PlainTextConsumerSession()
680
1317
            else:
681
 
                oidutil.log('Session type mismatch. Expected %r, got %r' %
682
 
                            (assoc_session.session_type, session_type))
683
 
                return None
684
 
 
 
1318
                # Any other mismatch, regardless of protocol version
 
1319
                # results in the failure of the association session
 
1320
                # altogether.
 
1321
                fmt = 'Session type mismatch. Expected %r, got %r'
 
1322
                message = fmt % (assoc_session.session_type, session_type)
 
1323
                raise ProtocolError(message)
 
1324
 
 
1325
        # Make sure assoc_type is valid for session_type
 
1326
        if assoc_type not in assoc_session.allowed_assoc_types:
 
1327
            fmt = 'Unsupported assoc_type for session %s returned: %s'
 
1328
            raise ProtocolError(fmt % (assoc_session.session_type, assoc_type))
 
1329
 
 
1330
        # Delegate to the association session to extract the secret
 
1331
        # from the response, however is appropriate for that session
 
1332
        # type.
685
1333
        try:
686
 
            secret = assoc_session.extractSecret(results)
 
1334
            secret = assoc_session.extractSecret(assoc_response)
687
1335
        except ValueError, why:
688
 
            oidutil.log('Malformed response for %s session: %s' % (
689
 
                assoc_session.session_type, why[0]))
690
 
            return None
691
 
        except KeyError, why:
692
 
            fmt = 'Getting association: missing key in response from %s: %s'
693
 
            oidutil.log(fmt % (server_url, why[0]))
694
 
            return None
 
1336
            fmt = 'Malformed response for %s session: %s'
 
1337
            raise ProtocolError(fmt % (assoc_session.session_type, why[0]))
695
1338
 
696
 
        assoc = Association.fromExpiresIn(
 
1339
        return Association.fromExpiresIn(
697
1340
            expires_in, assoc_handle, secret, assoc_type)
698
 
        self.store.storeAssociation(server_url, assoc)
699
 
 
700
 
        return assoc
701
1341
 
702
1342
class AuthRequest(object):
 
1343
    """An object that holds the state necessary for generating an
 
1344
    OpenID authentication request. This object holds the association
 
1345
    with the server and the discovered information with which the
 
1346
    request will be made.
 
1347
 
 
1348
    It is separate from the consumer because you may wish to add
 
1349
    things to the request before sending it on its way to the
 
1350
    server. It also has serialization options that let you encode the
 
1351
    authentication request as a URL or as a form POST.
 
1352
    """
 
1353
 
703
1354
    def __init__(self, endpoint, assoc):
704
1355
        """
705
1356
        Creates a new AuthRequest object.  This just stores each
711
1362
        """
712
1363
        self.assoc = assoc
713
1364
        self.endpoint = endpoint
714
 
        self.extra_args = {}
715
1365
        self.return_to_args = {}
 
1366
        self.message = Message()
 
1367
        self.message.setOpenIDNamespace(endpoint.preferredNamespace())
 
1368
        self._anonymous = False
 
1369
 
 
1370
    def setAnonymous(self, is_anonymous):
 
1371
        """Set whether this request should be made anonymously. If a
 
1372
        request is anonymous, the identifier will not be sent in the
 
1373
        request. This is only useful if you are making another kind of
 
1374
        request with an extension in this request.
 
1375
 
 
1376
        Anonymous requests are not allowed when the request is made
 
1377
        with OpenID 1.
 
1378
 
 
1379
        @raises ValueError: when attempting to set an OpenID1 request
 
1380
            as anonymous
 
1381
        """
 
1382
        if is_anonymous and self.message.isOpenID1():
 
1383
            raise ValueError('OpenID 1 requests MUST include the '
 
1384
                             'identifier in the request')
 
1385
        else:
 
1386
            self._anonymous = is_anonymous
 
1387
 
 
1388
    def addExtension(self, extension_request):
 
1389
        """Add an extension to this checkid request.
 
1390
 
 
1391
        @param extension_request: An object that implements the
 
1392
            extension interface for adding arguments to an OpenID
 
1393
            message.
 
1394
        """
 
1395
        extension_request.toMessage(self.message)
716
1396
 
717
1397
    def addExtensionArg(self, namespace, key, value):
718
1398
        """Add an extension argument to this OpenID authentication
739
1419
 
740
1420
        @type value: str
741
1421
        """
742
 
        arg_name = '.'.join(['openid', namespace, key])
743
 
        self.extra_args[arg_name] = value
744
 
 
745
 
    def redirectURL(self, trust_root, return_to, immediate=False):
 
1422
        self.message.setArg(namespace, key, value)
 
1423
 
 
1424
    def getMessage(self, realm, return_to=None, immediate=False):
 
1425
        """Produce a L{openid.message.Message} representing this request.
 
1426
 
 
1427
        @param realm: The URL (or URL pattern) that identifies your
 
1428
            web site to the user when she is authorizing it.
 
1429
 
 
1430
        @type realm: str
 
1431
 
 
1432
        @param return_to: The URL that the OpenID provider will send the
 
1433
            user back to after attempting to verify her identity.
 
1434
 
 
1435
            Not specifying a return_to URL means that the user will not
 
1436
            be returned to the site issuing the request upon its
 
1437
            completion.
 
1438
 
 
1439
        @type return_to: str
 
1440
 
 
1441
        @param immediate: If True, the OpenID provider is to send back
 
1442
            a response immediately, useful for behind-the-scenes
 
1443
            authentication attempts.  Otherwise the OpenID provider
 
1444
            may engage the user before providing a response.  This is
 
1445
            the default case, as the user may need to provide
 
1446
            credentials or approve the request before a positive
 
1447
            response can be sent.
 
1448
 
 
1449
        @type immediate: bool
 
1450
 
 
1451
        @returntype: L{openid.message.Message}
 
1452
        """
 
1453
        if return_to:
 
1454
            return_to = oidutil.appendArgs(return_to, self.return_to_args)
 
1455
        elif immediate:
 
1456
            raise ValueError(
 
1457
                '"return_to" is mandatory when using "checkid_immediate"')
 
1458
        elif self.message.isOpenID1():
 
1459
            raise ValueError('"return_to" is mandatory for OpenID 1 requests')
 
1460
        elif self.return_to_args:
 
1461
            raise ValueError('extra "return_to" arguments were specified, '
 
1462
                             'but no return_to was specified')
 
1463
 
746
1464
        if immediate:
747
1465
            mode = 'checkid_immediate'
748
1466
        else:
749
1467
            mode = 'checkid_setup'
750
1468
 
751
 
        return_to = oidutil.appendArgs(return_to, self.return_to_args)
752
 
 
753
 
        redir_args = {
754
 
            'openid.mode': mode,
755
 
            'openid.identity': self.endpoint.getServerID(),
756
 
            'openid.return_to': return_to,
757
 
            'openid.trust_root': trust_root,
758
 
            }
 
1469
        message = self.message.copy()
 
1470
        if message.isOpenID1():
 
1471
            realm_key = 'trust_root'
 
1472
        else:
 
1473
            realm_key = 'realm'
 
1474
 
 
1475
        message.updateArgs(OPENID_NS,
 
1476
            {
 
1477
            realm_key:realm,
 
1478
            'mode':mode,
 
1479
            'return_to':return_to,
 
1480
            })
 
1481
 
 
1482
        if not self._anonymous:
 
1483
            if self.endpoint.isOPIdentifier():
 
1484
                # This will never happen when we're in compatibility
 
1485
                # mode, as long as isOPIdentifier() returns False
 
1486
                # whenever preferredNamespace() returns OPENID1_NS.
 
1487
                claimed_id = request_identity = IDENTIFIER_SELECT
 
1488
            else:
 
1489
                request_identity = self.endpoint.getLocalID()
 
1490
                claimed_id = self.endpoint.claimed_id
 
1491
 
 
1492
            # This is true for both OpenID 1 and 2
 
1493
            message.setArg(OPENID_NS, 'identity', request_identity)
 
1494
 
 
1495
            if message.isOpenID2():
 
1496
                message.setArg(OPENID2_NS, 'claimed_id', claimed_id)
759
1497
 
760
1498
        if self.assoc:
761
 
            redir_args['openid.assoc_handle'] = self.assoc.handle
762
 
 
763
 
        redir_args.update(self.extra_args)
764
 
        return oidutil.appendArgs(self.endpoint.server_url, redir_args)
 
1499
            message.setArg(OPENID_NS, 'assoc_handle', self.assoc.handle)
 
1500
 
 
1501
        return message
 
1502
 
 
1503
    def redirectURL(self, realm, return_to=None, immediate=False):
 
1504
        """Returns a URL with an encoded OpenID request.
 
1505
 
 
1506
        The resulting URL is the OpenID provider's endpoint URL with
 
1507
        parameters appended as query arguments.  You should redirect
 
1508
        the user agent to this URL.
 
1509
 
 
1510
        OpenID 2.0 endpoints also accept POST requests, see
 
1511
        C{L{shouldSendRedirect}} and C{L{formMarkup}}.
 
1512
 
 
1513
        @param realm: The URL (or URL pattern) that identifies your
 
1514
            web site to the user when she is authorizing it.
 
1515
 
 
1516
        @type realm: str
 
1517
 
 
1518
        @param return_to: The URL that the OpenID provider will send the
 
1519
            user back to after attempting to verify her identity.
 
1520
 
 
1521
            Not specifying a return_to URL means that the user will not
 
1522
            be returned to the site issuing the request upon its
 
1523
            completion.
 
1524
 
 
1525
        @type return_to: str
 
1526
 
 
1527
        @param immediate: If True, the OpenID provider is to send back
 
1528
            a response immediately, useful for behind-the-scenes
 
1529
            authentication attempts.  Otherwise the OpenID provider
 
1530
            may engage the user before providing a response.  This is
 
1531
            the default case, as the user may need to provide
 
1532
            credentials or approve the request before a positive
 
1533
            response can be sent.
 
1534
 
 
1535
        @type immediate: bool
 
1536
 
 
1537
        @returns: The URL to redirect the user agent to.
 
1538
 
 
1539
        @returntype: str
 
1540
        """
 
1541
        message = self.getMessage(realm, return_to, immediate)
 
1542
        return message.toURL(self.endpoint.server_url)
 
1543
 
 
1544
    def formMarkup(self, realm, return_to=None, immediate=False,
 
1545
            form_tag_attrs=None):
 
1546
        """Get html for a form to submit this request to the IDP.
 
1547
 
 
1548
        @param form_tag_attrs: Dictionary of attributes to be added to
 
1549
            the form tag. 'accept-charset' and 'enctype' have defaults
 
1550
            that can be overridden. If a value is supplied for
 
1551
            'action' or 'method', it will be replaced.
 
1552
        @type form_tag_attrs: {unicode: unicode}
 
1553
        """
 
1554
        message = self.getMessage(realm, return_to, immediate)
 
1555
        return message.toFormMarkup(self.endpoint.server_url,
 
1556
                    form_tag_attrs)
 
1557
 
 
1558
    def shouldSendRedirect(self):
 
1559
        """Should this OpenID authentication request be sent as a HTTP
 
1560
        redirect or as a POST (form submission)?
 
1561
 
 
1562
        @rtype: bool
 
1563
        """
 
1564
        return self.endpoint.compatibilityMode()
765
1565
 
766
1566
FAILURE = 'failure'
767
1567
SUCCESS = 'success'
771
1571
class Response(object):
772
1572
    status = None
773
1573
 
 
1574
    def setEndpoint(self, endpoint):
 
1575
        self.endpoint = endpoint
 
1576
        if endpoint is None:
 
1577
            self.identity_url = None
 
1578
        else:
 
1579
            self.identity_url = endpoint.claimed_id
 
1580
 
774
1581
class SuccessResponse(Response):
775
1582
    """A response with a status of SUCCESS. Indicates that this request is a
776
1583
    successful acknowledgement from the OpenID server that the
783
1590
        such as the CanonicalID of an XRI, through this object.
784
1591
    @type endpoint: L{OpenIDServiceEndpoint<openid.consumer.discover.OpenIDServiceEndpoint>}
785
1592
 
786
 
    @ivar signed_args: The arguments in the server's response that
 
1593
    @ivar signed_fields: The arguments in the server's response that
787
1594
        were signed and verified.
788
1595
 
789
1596
    @cvar status: SUCCESS
791
1598
 
792
1599
    status = SUCCESS
793
1600
 
794
 
    def __init__(self, endpoint, signed_args):
 
1601
    def __init__(self, endpoint, message, signed_fields=None):
 
1602
        # Don't use setEndpoint, because endpoint should never be None
 
1603
        # for a successfull transaction.
795
1604
        self.endpoint = endpoint
796
 
        self.identity_url = endpoint.identity_url
797
 
        self.signed_args = signed_args
798
 
 
799
 
    def fromQuery(cls, endpoint, query, signed):
800
 
        signed_args = {}
801
 
        for field_name in signed.split(','):
802
 
            field_name = 'openid.' + field_name
803
 
            signed_args[field_name] = query.get(field_name, '')
804
 
        return cls(endpoint, signed_args)
805
 
 
806
 
    fromQuery = classmethod(fromQuery)
807
 
 
808
 
    def extensionResponse(self, prefix):
809
 
        """extract signed extension data from the server's response.
810
 
 
811
 
        @param prefix: The extension namespace from which to extract
812
 
            the extension data.
813
 
        """
814
 
        response = {}
815
 
        prefix = 'openid.%s.' % (prefix,)
816
 
        prefix_len = len(prefix)
817
 
        for k, v in self.signed_args.iteritems():
818
 
            if k.startswith(prefix):
819
 
                response_key = k[prefix_len:]
820
 
                response[response_key] = v
821
 
 
822
 
        return response
 
1605
        self.identity_url = endpoint.claimed_id
 
1606
 
 
1607
        self.message = message
 
1608
 
 
1609
        if signed_fields is None:
 
1610
            signed_fields = []
 
1611
        self.signed_fields = signed_fields
 
1612
 
 
1613
    def isOpenID1(self):
 
1614
        """Was this authentication response an OpenID 1 authentication
 
1615
        response?
 
1616
        """
 
1617
        return self.message.isOpenID1()
 
1618
 
 
1619
    def isSigned(self, ns_uri, ns_key):
 
1620
        """Return whether a particular key is signed, regardless of
 
1621
        its namespace alias
 
1622
        """
 
1623
        return self.message.getKey(ns_uri, ns_key) in self.signed_fields
 
1624
 
 
1625
    def getSigned(self, ns_uri, ns_key, default=None):
 
1626
        """Return the specified signed field if available,
 
1627
        otherwise return default
 
1628
        """
 
1629
        if self.isSigned(ns_uri, ns_key):
 
1630
            return self.message.getArg(ns_uri, ns_key, default)
 
1631
        else:
 
1632
            return default
 
1633
 
 
1634
    def getSignedNS(self, ns_uri):
 
1635
        """Get signed arguments from the response message.  Return a
 
1636
        dict of all arguments in the specified namespace.  If any of
 
1637
        the arguments are not signed, return None.
 
1638
        """
 
1639
        msg_args = self.message.getArgs(ns_uri)
 
1640
 
 
1641
        for key in msg_args.iterkeys():
 
1642
            if not self.isSigned(ns_uri, key):
 
1643
                return None
 
1644
 
 
1645
        return msg_args
 
1646
 
 
1647
    def extensionResponse(self, namespace_uri, require_signed):
 
1648
        """Return response arguments in the specified namespace.
 
1649
 
 
1650
        @param namespace_uri: The namespace URI of the arguments to be
 
1651
        returned.
 
1652
 
 
1653
        @param require_signed: True if the arguments should be among
 
1654
        those signed in the response, False if you don't care.
 
1655
 
 
1656
        If require_signed is True and the arguments are not signed,
 
1657
        return None.
 
1658
        """
 
1659
        if require_signed:
 
1660
            return self.getSignedNS(namespace_uri)
 
1661
        else:
 
1662
            return self.message.getArgs(namespace_uri)
823
1663
 
824
1664
    def getReturnTo(self):
825
1665
        """Get the openid.return_to argument from this response.
833
1673
 
834
1674
        @returntype: str
835
1675
        """
836
 
        return self.signed_args.get('openid.return_to', None)
 
1676
        return self.getSigned(OPENID_NS, 'return_to')
837
1677
 
838
1678
 
839
1679
 
852
1692
 
853
1693
    status = FAILURE
854
1694
 
855
 
    def __init__(self, endpoint, message=None):
856
 
        self.endpoint = endpoint
857
 
        if endpoint is not None:
858
 
            self.identity_url = endpoint.identity_url
859
 
        else:
860
 
            self.identity_url = None
 
1695
    def __init__(self, endpoint, message=None, contact=None,
 
1696
                 reference=None):
 
1697
        self.setEndpoint(endpoint)
861
1698
        self.message = message
862
 
 
 
1699
        self.contact = contact
 
1700
        self.reference = reference
863
1701
 
864
1702
    def __repr__(self):
865
1703
        return "<%s.%s id=%r message=%r>" % (
880
1718
    status = CANCEL
881
1719
 
882
1720
    def __init__(self, endpoint):
883
 
        self.endpoint = endpoint
884
 
        self.identity_url = endpoint.identity_url
 
1721
        self.setEndpoint(endpoint)
885
1722
 
886
1723
class SetupNeededResponse(Response):
887
1724
    """A response with a status of SETUP_NEEDED. Indicates that the
894
1731
    @ivar setup_url: A URL that can be used to send the user to the
895
1732
        server to set up for authentication. The user should be
896
1733
        redirected in to the setup_url, either in the current window
897
 
        or in a new browser window.
 
1734
        or in a new browser window.  C{None} in OpenID 2.0.
898
1735
 
899
1736
    @cvar status: SETUP_NEEDED
900
1737
    """
902
1739
    status = SETUP_NEEDED
903
1740
 
904
1741
    def __init__(self, endpoint, setup_url=None):
905
 
        self.endpoint = endpoint
906
 
        self.identity_url = endpoint.identity_url
 
1742
        self.setEndpoint(endpoint)
907
1743
        self.setup_url = setup_url