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

« back to all changes in this revision

Viewing changes to openid/server/server.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.server -*-
 
1
# -*- test-case-name: openid.test.test_server -*-
2
2
"""OpenID server protocol and logic.
3
3
 
4
4
Overview
35
35
    Other types of requests relate to establishing associations between client
36
36
    and server and verifying the authenticity of previous communications.
37
37
    L{Server} contains all the logic and data necessary to respond to
38
 
    such requests; just pass it to L{Server.handleRequest}.
 
38
    such requests; just pass the request to L{Server.handleRequest}.
39
39
 
40
40
 
41
41
OpenID Extensions
42
42
=================
43
43
 
44
44
    Do you want to provide other information for your users
45
 
    in addition to authentication?  Version 1.2 of the OpenID
 
45
    in addition to authentication?  Version 2.0 of the OpenID
46
46
    protocol allows consumers to add extensions to their requests.
47
47
    For example, with sites using the U{Simple Registration
48
48
    Extension<http://www.openidenabled.com/openid/simple-registration-extension/>},
61
61
        # when request is a checkid_* request
62
62
        response = request.answer(True)
63
63
        # this will a signed 'openid.sreg.timezone' parameter to the response
64
 
        response.addField('sreg', 'timezone', 'America/Los_Angeles')
 
64
        # as well as a namespace declaration for the openid.sreg namespace
 
65
        response.fields.setArg('http://openid.net/sreg/1.0', 'timezone', 'America/Los_Angeles')
65
66
 
66
67
 
67
68
Stores
84
85
    in version 1.2 of this library.  If your store has entries created from
85
86
    version 1.0 code, you should empty it.
86
87
 
 
88
    FIXME: add notes on 1.2 -> 2.0 upgrade here.
87
89
 
88
90
@group Requests: OpenIDRequest, AssociateRequest, CheckIDRequest,
89
91
    CheckAuthRequest
95
97
@group Response Encodings: ENCODE_KVFORM, ENCODE_URL
96
98
"""
97
99
 
98
 
import time
 
100
import time, warnings
99
101
from copy import deepcopy
100
102
 
101
103
from openid import cryptutil
102
 
from openid import kvform
103
104
from openid import oidutil
104
105
from openid.dh import DiffieHellman
 
106
from openid.store.nonce import mkNonce
105
107
from openid.server.trustroot import TrustRoot
106
 
from openid.association import Association
 
108
from openid.association import Association, default_negotiator, getSecretSize
 
109
from openid.message import Message, OPENID_NS, OPENID1_NS, \
 
110
     OPENID2_NS, IDENTIFIER_SELECT
107
111
 
108
112
HTTP_OK = 200
109
113
HTTP_REDIRECT = 302
110
114
HTTP_ERROR = 400
111
115
 
112
116
BROWSER_REQUEST_MODES = ['checkid_setup', 'checkid_immediate']
113
 
OPENID_PREFIX = 'openid.'
114
117
 
115
118
ENCODE_KVFORM = ('kvform',)
116
119
ENCODE_URL = ('URL/redirect',)
117
120
 
 
121
UNUSED = None
 
122
 
118
123
class OpenIDRequest(object):
119
124
    """I represent an incoming OpenID request.
120
125
 
132
137
 
133
138
    @ivar assoc_handle: The X{association handle} the response was signed with.
134
139
    @type assoc_handle: str
135
 
    @ivar sig: The signature to check.
136
 
    @type sig: str
137
 
    @ivar signed: The ordered list of signed items you want to check.
138
 
    @type signed: list of pairs
 
140
    @ivar signed: The message with the signature which wants checking.
 
141
    @type signed: L{Message}
139
142
 
140
143
    @ivar invalidate_handle: An X{association handle} the client is asking
141
144
        about the validity of.  Optional, may be C{None}.
146
149
    """
147
150
    mode = "check_authentication"
148
151
 
 
152
    required_fields = ["identity", "return_to", "response_nonce"]
149
153
 
150
 
    def __init__(self, assoc_handle, sig, signed, invalidate_handle=None):
 
154
    def __init__(self, assoc_handle, signed, invalidate_handle=None):
151
155
        """Construct me.
152
156
 
153
157
        These parameters are assigned directly as class attributes, see
154
158
        my L{class documentation<CheckAuthRequest>} for their descriptions.
155
159
 
156
160
        @type assoc_handle: str
157
 
        @type sig: str
158
 
        @type signed: list of pairs
 
161
        @type signed: L{Message}
159
162
        @type invalidate_handle: str
160
163
        """
161
164
        self.assoc_handle = assoc_handle
162
 
        self.sig = sig
163
165
        self.signed = signed
164
166
        self.invalidate_handle = invalidate_handle
165
 
 
166
 
 
167
 
    def fromQuery(klass, query):
168
 
        """Construct me from a web query.
169
 
 
170
 
        @param query: The query parameters as a dictionary with each
171
 
            key mapping to one value.
172
 
        @type query: dict
 
167
        self.namespace = OPENID2_NS
 
168
 
 
169
 
 
170
    def fromMessage(klass, message, op_endpoint=UNUSED):
 
171
        """Construct me from an OpenID Message.
 
172
 
 
173
        @param message: An OpenID check_authentication Message
 
174
        @type message: L{openid.message.Message}
173
175
 
174
176
        @returntype: L{CheckAuthRequest}
175
177
        """
176
178
        self = klass.__new__(klass)
177
 
        try:
178
 
            self.assoc_handle = query[OPENID_PREFIX + 'assoc_handle']
179
 
            self.sig = query[OPENID_PREFIX + 'sig']
180
 
            signed_list = query[OPENID_PREFIX + 'signed']
181
 
        except KeyError, e:
182
 
            raise ProtocolError(query,
183
 
                                text="%s request missing required parameter %s"
184
 
                                " from query %s" %
185
 
                                (self.mode, e.args[0], query))
186
 
 
187
 
        self.invalidate_handle = query.get(OPENID_PREFIX + 'invalidate_handle')
188
 
 
189
 
        signed_list = signed_list.split(',')
190
 
        signed_pairs = []
191
 
        for field in signed_list:
192
 
            try:
193
 
                if field == 'mode':
194
 
                    # XXX KLUDGE HAX WEB PROTOCoL BR0KENNN
195
 
                    # openid.mode is currently check_authentication because
196
 
                    # that's the mode of this request.  But the signature
197
 
                    # was made on something with a different openid.mode.
198
 
                    # http://article.gmane.org/gmane.comp.web.openid.general/537
199
 
                    value = "id_res"
200
 
                else:
201
 
                    value = query[OPENID_PREFIX + field]
202
 
            except KeyError, e:
203
 
                raise ProtocolError(
204
 
                    query,
205
 
                    text="Couldn't find signed field %r in query %s"
206
 
                    % (field, query))
207
 
            else:
208
 
                signed_pairs.append((field, value))
209
 
 
210
 
        self.signed = signed_pairs
 
179
        self.message = message
 
180
        self.namespace = message.getOpenIDNamespace()
 
181
        self.assoc_handle = message.getArg(OPENID_NS, 'assoc_handle')
 
182
        self.sig = message.getArg(OPENID_NS, 'sig')
 
183
 
 
184
        if (self.assoc_handle is None or
 
185
            self.sig is None):
 
186
            fmt = "%s request missing required parameter from message %s"
 
187
            raise ProtocolError(
 
188
                message, text=fmt % (self.mode, message))
 
189
 
 
190
        self.invalidate_handle = message.getArg(OPENID_NS, 'invalidate_handle')
 
191
 
 
192
        self.signed = message.copy()
 
193
        # openid.mode is currently check_authentication because
 
194
        # that's the mode of this request.  But the signature
 
195
        # was made on something with a different openid.mode.
 
196
        # http://article.gmane.org/gmane.comp.web.openid.general/537
 
197
        if self.signed.hasKey(OPENID_NS, "mode"):
 
198
            self.signed.setArg(OPENID_NS, "mode", "id_res")
 
199
 
211
200
        return self
212
201
 
213
 
    fromQuery = classmethod(fromQuery)
 
202
    fromMessage = classmethod(fromMessage)
214
203
 
215
204
 
216
205
    def answer(self, signatory):
226
215
           appropriate X{C{invalidate_handle}}) field.
227
216
        @returntype: L{OpenIDResponse}
228
217
        """
229
 
        is_valid = signatory.verify(self.assoc_handle, self.sig, self.signed)
 
218
        is_valid = signatory.verify(self.assoc_handle, self.signed)
230
219
        # Now invalidate that assoc_handle so it this checkAuth message cannot
231
220
        # be replayed.
232
221
        signatory.invalidate(self.assoc_handle, dumb=True)
233
222
        response = OpenIDResponse(self)
234
 
        response.fields['is_valid'] = (is_valid and "true") or "false"
 
223
        valid_str = (is_valid and "true") or "false"
 
224
        response.fields.setArg(OPENID_NS, 'is_valid', valid_str)
235
225
 
236
226
        if self.invalidate_handle:
237
227
            assoc = signatory.getAssociation(self.invalidate_handle, dumb=False)
238
228
            if not assoc:
239
 
                response.fields['invalidate_handle'] = self.invalidate_handle
 
229
                response.fields.setArg(
 
230
                    OPENID_NS, 'invalidate_handle', self.invalidate_handle)
240
231
        return response
241
232
 
242
233
 
257
248
 
258
249
    @cvar session_type: The session_type for this association
259
250
        session. There is no type defined for plain-text in the OpenID
260
 
        specification, so we use 'plaintext'.
 
251
        specification, so we use 'no-encryption'.
261
252
    @type session_type: str
262
253
 
263
254
    @see: U{OpenID Specs, Mode: associate
264
255
        <http://openid.net/specs.bml#mode-associate>}
265
256
    @see: AssociateRequest
266
257
    """
267
 
    session_type = 'plaintext'
 
258
    session_type = 'no-encryption'
 
259
    allowed_assoc_types = ['HMAC-SHA1', 'HMAC-SHA256']
268
260
 
269
 
    def fromQuery(cls, unused_request):
 
261
    def fromMessage(cls, unused_request):
270
262
        return cls()
271
263
 
272
 
    fromQuery = classmethod(fromQuery)
 
264
    fromMessage = classmethod(fromMessage)
273
265
 
274
266
    def answer(self, secret):
275
267
        return {'mac_key': oidutil.toBase64(secret)}
276
268
 
277
269
 
278
 
class DiffieHellmanServerSession(object):
 
270
class DiffieHellmanSHA1ServerSession(object):
279
271
    """An object that knows how to handle association requests with the
280
272
    Diffie-Hellman session type.
281
273
 
295
287
    @see: AssociateRequest
296
288
    """
297
289
    session_type = 'DH-SHA1'
 
290
    hash_func = staticmethod(cryptutil.sha1)
 
291
    allowed_assoc_types = ['HMAC-SHA1']
298
292
 
299
293
    def __init__(self, dh, consumer_pubkey):
300
294
        self.dh = dh
301
295
        self.consumer_pubkey = consumer_pubkey
302
296
 
303
 
    def fromQuery(cls, query):
 
297
    def fromMessage(cls, message):
304
298
        """
305
 
        @param query: The associate request's query parameters
306
 
        @type query: {str:str}
 
299
        @param message: The associate request message
 
300
        @type message: openid.message.Message
307
301
 
308
 
        @returntype: L{DiffieHellmanServerSession}
 
302
        @returntype: L{DiffieHellmanSHA1ServerSession}
309
303
 
310
304
        @raises ProtocolError: When parameters required to establish the
311
305
            session are missing.
312
306
        """
313
 
        dh_modulus = query.get('openid.dh_modulus')
314
 
        dh_gen = query.get('openid.dh_gen')
 
307
        dh_modulus = message.getArg(OPENID_NS, 'dh_modulus')
 
308
        dh_gen = message.getArg(OPENID_NS, 'dh_gen')
315
309
        if (dh_modulus is None and dh_gen is not None or
316
310
            dh_gen is None and dh_modulus is not None):
317
311
 
320
314
            else:
321
315
                missing = 'generator'
322
316
 
323
 
            raise ProtocolError('If non-default modulus or generator is '
 
317
            raise ProtocolError(message,
 
318
                                'If non-default modulus or generator is '
324
319
                                'supplied, both must be supplied. Missing %s'
325
320
                                % (missing,))
326
321
 
331
326
        else:
332
327
            dh = DiffieHellman.fromDefaults()
333
328
 
334
 
        consumer_pubkey = query.get('openid.dh_consumer_public')
 
329
        consumer_pubkey = message.getArg(OPENID_NS, 'dh_consumer_public')
335
330
        if consumer_pubkey is None:
336
 
            raise ProtocolError("Public key for DH-SHA1 session "
337
 
                                "not found in query %s" % (query,))
 
331
            raise ProtocolError(message, "Public key for DH-SHA1 session "
 
332
                                "not found in message %s" % (message,))
338
333
 
339
334
        consumer_pubkey = cryptutil.base64ToLong(consumer_pubkey)
340
335
 
341
336
        return cls(dh, consumer_pubkey)
342
337
 
343
 
    fromQuery = classmethod(fromQuery)
 
338
    fromMessage = classmethod(fromMessage)
344
339
 
345
340
    def answer(self, secret):
346
 
        mac_key = self.dh.xorSecret(self.consumer_pubkey, secret)
 
341
        mac_key = self.dh.xorSecret(self.consumer_pubkey,
 
342
                                    secret,
 
343
                                    self.hash_func)
347
344
        return {
348
345
            'dh_server_public': cryptutil.longToBase64(self.dh.public),
349
346
            'enc_mac_key': oidutil.toBase64(mac_key),
350
347
            }
351
348
 
 
349
class DiffieHellmanSHA256ServerSession(DiffieHellmanSHA1ServerSession):
 
350
    session_type = 'DH-SHA256'
 
351
    hash_func = staticmethod(cryptutil.sha256)
 
352
    allowed_assoc_types = ['HMAC-SHA256']
352
353
 
353
354
class AssociateRequest(OpenIDRequest):
354
355
    """A request to establish an X{association}.
368
369
    """
369
370
 
370
371
    mode = "associate"
371
 
    assoc_type = 'HMAC-SHA1'
372
372
 
373
373
    session_classes = {
374
 
        None: PlainTextServerSession,
375
 
        'DH-SHA1': DiffieHellmanServerSession,
 
374
        'no-encryption': PlainTextServerSession,
 
375
        'DH-SHA1': DiffieHellmanSHA1ServerSession,
 
376
        'DH-SHA256': DiffieHellmanSHA256ServerSession,
376
377
        }
377
378
 
378
 
    def __init__(self, session):
 
379
    def __init__(self, session, assoc_type):
379
380
        """Construct me.
380
381
 
381
382
        The session is assigned directly as a class attribute. See my
383
384
        """
384
385
        super(AssociateRequest, self).__init__()
385
386
        self.session = session
386
 
 
387
 
 
388
 
    def fromQuery(klass, query):
389
 
        """Construct me from a web query.
390
 
 
391
 
        @param query: The query parameters as a dictionary with each
392
 
            key mapping to one value.
393
 
        @type query: dict
 
387
        self.assoc_type = assoc_type
 
388
        self.namespace = OPENID2_NS
 
389
 
 
390
 
 
391
    def fromMessage(klass, message, op_endpoint=UNUSED):
 
392
        """Construct me from an OpenID Message.
 
393
 
 
394
        @param message: The OpenID associate request
 
395
        @type message: openid.message.Message
394
396
 
395
397
        @returntype: L{AssociateRequest}
396
398
        """
397
 
        session_type = query.get(OPENID_PREFIX + 'session_type')
 
399
        if message.isOpenID1():
 
400
            session_type = message.getArg(OPENID1_NS, 'session_type')
 
401
            if session_type == 'no-encryption':
 
402
                oidutil.log('Received OpenID 1 request with a no-encryption '
 
403
                            'assocaition session type. Continuing anyway.')
 
404
            elif not session_type:
 
405
                session_type = 'no-encryption'
 
406
        else:
 
407
            session_type = message.getArg(OPENID2_NS, 'session_type')
 
408
            if session_type is None:
 
409
                raise ProtocolError(message,
 
410
                                    text="session_type missing from request")
 
411
 
398
412
        try:
399
413
            session_class = klass.session_classes[session_type]
400
414
        except KeyError:
401
 
            raise ProtocolError(query,
 
415
            raise ProtocolError(message,
402
416
                                "Unknown session type %r" % (session_type,))
403
417
 
404
418
        try:
405
 
            session = session_class.fromQuery(query)
 
419
            session = session_class.fromMessage(message)
406
420
        except ValueError, why:
407
 
            raise ProtocolError(query, 'Error parsing %s session: %s' %
 
421
            raise ProtocolError(message, 'Error parsing %s session: %s' %
408
422
                                (session_class.session_type, why[0]))
409
423
 
410
 
        return klass(session)
411
 
 
412
 
    fromQuery = classmethod(fromQuery)
 
424
        assoc_type = message.getArg(OPENID_NS, 'assoc_type', 'HMAC-SHA1')
 
425
        if assoc_type not in session.allowed_assoc_types:
 
426
            fmt = 'Session type %s does not support association type %s'
 
427
            raise ProtocolError(message, fmt % (session_type, assoc_type))
 
428
 
 
429
        self = klass(session, assoc_type)
 
430
        self.message = message
 
431
        self.namespace = message.getOpenIDNamespace()
 
432
        return self
 
433
 
 
434
    fromMessage = classmethod(fromMessage)
413
435
 
414
436
    def answer(self, assoc):
415
437
        """Respond to this request with an X{association}.
422
444
        @returntype: L{OpenIDResponse}
423
445
        """
424
446
        response = OpenIDResponse(self)
425
 
        response.fields.update({
 
447
        response.fields.updateArgs(OPENID_NS, {
426
448
            'expires_in': '%d' % (assoc.getExpiresIn(),),
427
 
            'assoc_type': 'HMAC-SHA1',
 
449
            'assoc_type': self.assoc_type,
428
450
            'assoc_handle': assoc.handle,
429
451
            })
430
 
        response.fields.update(self.session.answer(assoc.secret))
431
 
        if self.session.session_type != 'plaintext':
432
 
            response.fields['session_type'] = self.session.session_type
433
 
 
434
 
        return response
435
 
 
 
452
        response.fields.updateArgs(OPENID_NS,
 
453
                                   self.session.answer(assoc.secret))
 
454
        if self.session.session_type != 'no-encryption':
 
455
            response.fields.setArg(
 
456
                OPENID_NS, 'session_type', self.session.session_type)
 
457
 
 
458
        return response
 
459
 
 
460
    def answerUnsupported(self, message, preferred_association_type=None,
 
461
                          preferred_session_type=None):
 
462
        """Respond to this request indicating that the association
 
463
        type or association session type is not supported."""
 
464
        if self.message.isOpenID1():
 
465
            raise ProtocolError(self.message)
 
466
 
 
467
        response = OpenIDResponse(self)
 
468
        response.fields.setArg(OPENID_NS, 'error_code', 'unsupported-type')
 
469
        response.fields.setArg(OPENID_NS, 'error', message)
 
470
 
 
471
        if preferred_association_type:
 
472
            response.fields.setArg(
 
473
                OPENID_NS, 'assoc_type', preferred_association_type)
 
474
 
 
475
        if preferred_session_type:
 
476
            response.fields.setArg(
 
477
                OPENID_NS, 'session_type', preferred_session_type)
 
478
 
 
479
        return response
436
480
 
437
481
class CheckIDRequest(OpenIDRequest):
438
482
    """A request to confirm the identity of a user.
446
490
    @ivar immediate: Is this an immediate-mode request?
447
491
    @type immediate: bool
448
492
 
449
 
    @ivar identity: The identity URL being checked.
 
493
    @ivar identity: The OP-local identifier being checked.
450
494
    @type identity: str
451
495
 
 
496
    @ivar claimed_id: The claimed identifier.  Not present in OpenID 1.x
 
497
        messages.
 
498
    @type claimed_id: str
 
499
 
452
500
    @ivar trust_root: "Are you Frank?" asks the checkid request.  "Who wants
453
501
        to know?"  C{trust_root}, that's who.  This URL identifies the party
454
502
        making the request, and the user will use that to make her decision
455
 
        about what answer she trusts them to have.
 
503
        about what answer she trusts them to have.  Referred to as "realm" in
 
504
        OpenID 2.0.
456
505
    @type trust_root: str
457
506
 
458
507
    @ivar return_to: The URL to send the user agent back to to reply to this
465
514
    """
466
515
 
467
516
    def __init__(self, identity, return_to, trust_root=None, immediate=False,
468
 
                 assoc_handle=None):
 
517
                 assoc_handle=None, op_endpoint=None):
469
518
        """Construct me.
470
519
 
471
520
        These parameters are assigned directly as class attributes, see
473
522
 
474
523
        @raises MalformedReturnURL: When the C{return_to} URL is not a URL.
475
524
        """
 
525
        self.namespace = OPENID2_NS
476
526
        self.assoc_handle = assoc_handle
477
527
        self.identity = identity
 
528
        self.claimed_id = identity
478
529
        self.return_to = return_to
479
530
        self.trust_root = trust_root or return_to
 
531
        self.op_endpoint = op_endpoint
 
532
        assert self.op_endpoint is not None
480
533
        if immediate:
481
534
            self.immediate = True
482
535
            self.mode = "checkid_immediate"
484
537
            self.immediate = False
485
538
            self.mode = "checkid_setup"
486
539
 
487
 
        if not TrustRoot.parse(self.return_to):
 
540
        if self.return_to is not None and \
 
541
               not TrustRoot.parse(self.return_to):
488
542
            raise MalformedReturnURL(None, self.return_to)
489
543
        if not self.trustRootValid():
490
544
            raise UntrustedReturnURL(None, self.return_to, self.trust_root)
491
545
 
492
546
 
493
 
    def fromQuery(klass, query):
494
 
        """Construct me from a web query.
 
547
    def fromMessage(klass, message, op_endpoint):
 
548
        """Construct me from an OpenID message.
495
549
 
496
550
        @raises ProtocolError: When not all required parameters are present
497
 
            in the query.
 
551
            in the message.
498
552
 
499
553
        @raises MalformedReturnURL: When the C{return_to} URL is not a URL.
500
554
 
501
555
        @raises UntrustedReturnURL: When the C{return_to} URL is outside
502
556
            the C{trust_root}.
503
557
 
504
 
        @param query: The query parameters as a dictionary with each
505
 
            key mapping to one value.
506
 
        @type query: dict
 
558
        @param message: An OpenID checkid_* request Message
 
559
        @type message: openid.message.Message
 
560
 
 
561
        @param op_endpoint: The endpoint URL of the server that this
 
562
            message was sent to.
 
563
        @type op_endpoint: str
507
564
 
508
565
        @returntype: L{CheckIDRequest}
509
566
        """
510
567
        self = klass.__new__(klass)
511
 
        mode = query[OPENID_PREFIX + 'mode']
 
568
        self.message = message
 
569
        self.namespace = message.getOpenIDNamespace()
 
570
        self.op_endpoint = op_endpoint
 
571
        mode = message.getArg(OPENID_NS, 'mode')
512
572
        if mode == "checkid_immediate":
513
573
            self.immediate = True
514
574
            self.mode = "checkid_immediate"
516
576
            self.immediate = False
517
577
            self.mode = "checkid_setup"
518
578
 
519
 
        required = [
520
 
            'identity',
521
 
            'return_to',
522
 
            ]
523
 
 
524
 
        for field in required:
525
 
            value = query.get(OPENID_PREFIX + field)
526
 
            if not value:
527
 
                raise ProtocolError(
528
 
                    query,
529
 
                    text="Missing required field %s from %r"
530
 
                    % (field, query))
531
 
            setattr(self, field, value)
 
579
        self.return_to = message.getArg(OPENID_NS, 'return_to')
 
580
        if self.namespace == OPENID1_NS and not self.return_to:
 
581
            fmt = "Missing required field 'return_to' from %r"
 
582
            raise ProtocolError(message, text=fmt % (message,))
 
583
 
 
584
        self.identity = message.getArg(OPENID_NS, 'identity')
 
585
        if self.identity and message.isOpenID2():
 
586
            self.claimed_id = message.getArg(OPENID_NS, 'claimed_id')
 
587
            if not self.claimed_id:
 
588
                s = ("OpenID 2.0 message contained openid.identity but not "
 
589
                     "claimed_id")
 
590
                raise ProtocolError(message, text=s)
 
591
 
 
592
        else:
 
593
            self.claimed_id = None
 
594
 
 
595
        if self.identity is None and self.namespace == OPENID1_NS:
 
596
            s = "OpenID 1 message did not contain openid.identity"
 
597
            raise ProtocolError(message, text=s)
532
598
 
533
599
        # There's a case for making self.trust_root be a TrustRoot
534
600
        # here.  But if TrustRoot isn't currently part of the "public" API,
535
601
        # I'm not sure it's worth doing.
536
 
        self.trust_root = query.get(OPENID_PREFIX + 'trust_root', self.return_to)
537
 
        self.assoc_handle = query.get(OPENID_PREFIX + 'assoc_handle')
 
602
        if self.namespace == OPENID1_NS:
 
603
            self.trust_root = message.getArg(
 
604
                OPENID_NS, 'trust_root', self.return_to)
 
605
        else:
 
606
            self.trust_root = message.getArg(
 
607
                OPENID_NS, 'realm', self.return_to)
 
608
 
 
609
            if self.return_to is self.trust_root is None:
 
610
                raise ProtocolError(message, "openid.realm required when " +
 
611
                                    "openid.return_to absent")
 
612
 
 
613
        self.assoc_handle = message.getArg(OPENID_NS, 'assoc_handle')
538
614
 
539
615
        # Using TrustRoot.parse here is a bit misleading, as we're not
540
616
        # parsing return_to as a trust root at all.  However, valid URLs
542
618
        # is a valid URL.  Not all trust roots are valid return_to URLs,
543
619
        # however (particularly ones with wildcards), so this is still a
544
620
        # little sketchy.
545
 
        if not TrustRoot.parse(self.return_to):
546
 
            raise MalformedReturnURL(query, self.return_to)
 
621
        if self.return_to is not None and \
 
622
               not TrustRoot.parse(self.return_to):
 
623
            raise MalformedReturnURL(message, self.return_to)
547
624
 
548
625
        # I first thought that checking to see if the return_to is within
549
626
        # the trust_root is premature here, a logic-not-decoding thing.  But
551
628
        # request with an invalid trust_root/return_to is broken regardless of
552
629
        # application, right?
553
630
        if not self.trustRootValid():
554
 
            raise UntrustedReturnURL(query, self.return_to, self.trust_root)
 
631
            raise UntrustedReturnURL(message, self.return_to, self.trust_root)
555
632
 
556
633
        return self
557
634
 
558
 
    fromQuery = classmethod(fromQuery)
559
 
 
 
635
    fromMessage = classmethod(fromMessage)
 
636
 
 
637
    def idSelect(self):
 
638
        """Is the identifier to be selected by the IDP?
 
639
 
 
640
        @returntype: bool
 
641
        """
 
642
        # So IDPs don't have to import the constant
 
643
        return self.identity == IDENTIFIER_SELECT
560
644
 
561
645
    def trustRootValid(self):
562
646
        """Is my return_to under my trust_root?
568
652
        tr = TrustRoot.parse(self.trust_root)
569
653
        if tr is None:
570
654
            raise MalformedTrustRoot(None, self.trust_root)
571
 
        return tr.validateURL(self.return_to)
572
 
 
573
 
 
574
 
    def answer(self, allow, server_url=None):
 
655
 
 
656
        if self.return_to is not None:
 
657
            return tr.validateURL(self.return_to)
 
658
        else:
 
659
            return True
 
660
 
 
661
    def answer(self, allow, server_url=None, identity=None, claimed_id=None):
575
662
        """Respond to this request.
576
663
 
577
664
        @param allow: Allow this user to claim this identity, and allow the
578
665
            consumer to have this information?
579
666
        @type allow: bool
580
667
 
581
 
        @param server_url: When an immediate mode request does not
582
 
            succeed, it gets back a URL where the request may be
583
 
            carried out in a not-so-immediate fashion.  Pass my URL
584
 
            in here (the fully qualified address of this server's
585
 
            endpoint, i.e.  C{http://example.com/server}), and I
586
 
            will use it as a base for the URL for a new request.
 
668
        @param server_url: DEPRECATED.  Passing C{op_endpoint} to the
 
669
            L{Server} constructor makes this optional.
 
670
 
 
671
            When an OpenID 1.x immediate mode request does not succeed,
 
672
            it gets back a URL where the request may be carried out
 
673
            in a not-so-immediate fashion.  Pass my URL in here (the
 
674
            fully qualified address of this server's endpoint, i.e.
 
675
            C{http://example.com/server}), and I will use it as a base for the
 
676
            URL for a new request.
587
677
 
588
678
            Optional for requests where C{CheckIDRequest.immediate} is C{False}
589
679
            or C{allow} is C{True}.
590
680
 
591
681
        @type server_url: str
592
682
 
 
683
        @param identity: The OP-local identifier to answer with.  Only for use
 
684
            when the relying party requested identifier selection.
 
685
        @type identity: str or None
 
686
 
 
687
        @param claimed_id: The claimed identifier to answer with, for use
 
688
            with identifier selection in the case where the claimed identifier
 
689
            and the OP-local identifier differ, i.e. when the claimed_id uses
 
690
            delegation.
 
691
 
 
692
            If C{identity} is provided but this is not, C{claimed_id} will
 
693
            default to the value of C{identity}.  When answering requests
 
694
            that did not ask for identifier selection, the response
 
695
            C{claimed_id} will default to that of the request.
 
696
 
 
697
            This parameter is new in OpenID 2.0.
 
698
        @type claimed_id: str or None
 
699
 
593
700
        @returntype: L{OpenIDResponse}
 
701
 
 
702
        @change: Version 2.0 deprecates C{server_url} and adds C{claimed_id}.
594
703
        """
595
 
        if allow or self.immediate:
 
704
        # FIXME: undocumented exceptions
 
705
        if not self.return_to:
 
706
            raise NoReturnToError
 
707
 
 
708
        if not server_url:
 
709
            if self.namespace != OPENID1_NS and not self.op_endpoint:
 
710
                # In other words, that warning I raised in Server.__init__?
 
711
                # You should pay attention to it now.
 
712
                raise RuntimeError("%s should be constructed with op_endpoint "
 
713
                                   "to respond to OpenID 2.0 messages." %
 
714
                                   (self,))
 
715
            server_url = self.op_endpoint
 
716
 
 
717
        if allow:
596
718
            mode = 'id_res'
 
719
        elif self.namespace == OPENID1_NS:
 
720
             if self.immediate:
 
721
                 mode = 'id_res'
 
722
             else:
 
723
                 mode = 'cancel'
597
724
        else:
598
 
            mode = 'cancel'
 
725
            if self.immediate:
 
726
                mode = 'setup_needed'
 
727
            else:
 
728
                mode = 'cancel'
599
729
 
600
730
        response = OpenIDResponse(self)
601
731
 
 
732
        if claimed_id and self.namespace == OPENID1_NS:
 
733
            raise VersionError("claimed_id is new in OpenID 2.0 and not "
 
734
                               "available for %s" % (self.namespace,))
 
735
 
 
736
        if identity and not claimed_id:
 
737
            claimed_id = identity
 
738
 
602
739
        if allow:
603
 
            response.addFields(None, {
 
740
            if self.identity == IDENTIFIER_SELECT:
 
741
                if not identity:
 
742
                    raise ValueError(
 
743
                        "This request uses IdP-driven identifier selection."
 
744
                        "You must supply an identifier in the response.")
 
745
                response_identity = identity
 
746
                response_claimed_id = claimed_id
 
747
 
 
748
            elif self.identity:
 
749
                if identity and (self.identity != identity):
 
750
                    raise ValueError(
 
751
                        "Request was for identity %r, cannot reply "
 
752
                        "with identity %r" % (self.identity, identity))
 
753
                response_identity = self.identity
 
754
                response_claimed_id = self.claimed_id
 
755
 
 
756
            else:
 
757
                if identity:
 
758
                    raise ValueError(
 
759
                        "This request specified no identity and you "
 
760
                        "supplied %r" % (identity,))
 
761
                response_identity = None
 
762
 
 
763
            if self.namespace == OPENID1_NS and response_identity is None:
 
764
                raise ValueError(
 
765
                    "Request was an OpenID 1 request, so response must "
 
766
                    "include an identifier."
 
767
                    )
 
768
 
 
769
            response.fields.updateArgs(OPENID_NS, {
604
770
                'mode': mode,
605
 
                'identity': self.identity,
 
771
                'op_endpoint': server_url,
606
772
                'return_to': self.return_to,
 
773
                'response_nonce': mkNonce(),
607
774
                })
 
775
 
 
776
            if response_identity is not None:
 
777
                response.fields.setArg(
 
778
                    OPENID_NS, 'identity', response_identity)
 
779
                if self.namespace == OPENID2_NS:
 
780
                    response.fields.setArg(
 
781
                        OPENID_NS, 'claimed_id', response_claimed_id)
608
782
        else:
609
 
            response.addField(None, 'mode', mode, False)
 
783
            response.fields.setArg(OPENID_NS, 'mode', mode)
610
784
            if self.immediate:
611
 
                if not server_url:
 
785
                if self.namespace == OPENID1_NS and not server_url:
612
786
                    raise ValueError("setup_url is required for allow=False "
613
 
                                     "in immediate mode.")
 
787
                                     "in OpenID 1.x immediate mode.")
614
788
                # Make a new request just like me, but with immediate=False.
615
789
                setup_request = self.__class__(
616
790
                    self.identity, self.return_to, self.trust_root,
617
 
                    immediate=False, assoc_handle=self.assoc_handle)
 
791
                    immediate=False, assoc_handle=self.assoc_handle,
 
792
                    op_endpoint=self.op_endpoint)
618
793
                setup_url = setup_request.encodeToURL(server_url)
619
 
                response.addField(None, 'user_setup_url', setup_url, False)
 
794
                response.fields.setArg(OPENID_NS, 'user_setup_url', setup_url)
620
795
 
621
796
        return response
622
797
 
629
804
 
630
805
        @returntype: str
631
806
        """
 
807
        if not self.return_to:
 
808
            raise NoReturnToError
 
809
 
632
810
        # Imported from the alternate reality where these classes are used
633
811
        # in both the client and server code, so Requests are Encodable too.
634
812
        # That's right, code imported from alternate realities all for the
635
813
        # love of you, id_res/user_setup_url.
636
814
        q = {'mode': self.mode,
637
815
             'identity': self.identity,
 
816
             'claimed_id': self.claimed_id,
638
817
             'return_to': self.return_to}
639
818
        if self.trust_root:
640
 
            q['trust_root'] = self.trust_root
 
819
            if self.namespace == OPENID1_NS:
 
820
                q['trust_root'] = self.trust_root
 
821
            else:
 
822
                q['realm'] = self.trust_root
641
823
        if self.assoc_handle:
642
824
            q['assoc_handle'] = self.assoc_handle
643
825
 
644
 
        q = dict([(OPENID_PREFIX + k, v) for k, v in q.iteritems()])
645
 
 
646
 
        return oidutil.appendArgs(server_url, q)
 
826
        response = Message(self.namespace)
 
827
        response.updateArgs(self.namespace, q)
 
828
        return response.toURL(server_url)
647
829
 
648
830
 
649
831
    def getCancelURL(self):
659
841
        @returntype: str
660
842
        @returns: The return_to URL with openid.mode = cancel.
661
843
        """
 
844
        if not self.return_to:
 
845
            raise NoReturnToError
 
846
 
662
847
        if self.immediate:
663
848
            raise ValueError("Cancel is not an appropriate response to "
664
849
                             "immediate mode requests.")
665
 
        return oidutil.appendArgs(self.return_to, {OPENID_PREFIX + 'mode':
666
 
                                                   'cancel'})
 
850
        response = Message(self.namespace)
 
851
        response.setArg(OPENID_NS, 'mode', 'cancel')
 
852
        return response.toURL(self.return_to)
667
853
 
668
854
 
669
855
    def __str__(self):
703
889
        @type request: L{OpenIDRequest}
704
890
        """
705
891
        self.request = request
706
 
        self.fields = {}
707
 
        self.signed = []
 
892
        self.fields = Message(request.namespace)
708
893
 
709
894
    def __str__(self):
710
895
        return "%s for %s: %s" % (
713
898
            self.fields)
714
899
 
715
900
 
716
 
    def addField(self, namespace, key, value, signed=True):
717
 
        """Add a field to this response.
718
 
 
719
 
        @param namespace: The extension namespace the field is in, with no
720
 
            leading "C{openid.}" e.g. "C{sreg}".
721
 
        @type namespace: str
722
 
 
723
 
        @param key: The field's name, e.g. "C{fullname}".
724
 
        @type key: str
725
 
 
726
 
        @param value: The field's value.
727
 
        @type value: str
728
 
 
729
 
        @param signed: Whether this field should be signed.
730
 
        @type signed: bool
731
 
        """
732
 
        if namespace:
733
 
            key = '%s.%s' % (namespace, key)
734
 
        self.fields[key] = value
735
 
        if signed and key not in self.signed:
736
 
            self.signed.append(key)
737
 
 
738
 
 
739
 
    def addFields(self, namespace, fields, signed=True):
740
 
        """Add a number of fields to this response.
741
 
 
742
 
        @param namespace: The extension namespace the field is in, with no
743
 
            leading "C{openid.}" e.g. "C{sreg}".
744
 
        @type namespace: str
745
 
 
746
 
        @param fields: A dictionary with the fields to add.
747
 
            e.g. C{{"fullname": "Frank the Goat"}}
748
 
 
749
 
        @param signed: Whether these fields should be signed.
750
 
        @type signed: bool
751
 
        """
752
 
        for key, value in fields.iteritems():
753
 
            self.addField(namespace, key, value, signed)
754
 
 
755
 
 
756
 
    def update(self, namespace, other):
757
 
        """Update my fields with those from another L{OpenIDResponse}.
758
 
 
759
 
        The idea here is that if you write an OpenID extension, it
760
 
        could produce a Response object with C{fields} and C{signed}
761
 
        attributes, and you could merge it with me using this method
762
 
        before I am signed and sent.
763
 
 
764
 
        All entries in C{other.fields} will have their keys prefixed
765
 
        with C{namespace} and added to my fields.  All elements of
766
 
        C{other.signed} will be prefixed with C{namespace} and added
767
 
        to my C{signed} list.
768
 
 
769
 
        @param namespace: The extension namespace the field is in, with no
770
 
            leading "C{openid.}" e.g. "C{sreg}".
771
 
        @type namespace: str
772
 
 
773
 
        @param other: A response object to update from.
774
 
        @type other: L{OpenIDResponse}
775
 
        """
776
 
        if namespace:
777
 
            namespaced_fields = dict([('%s.%s' % (namespace, k), v) for k, v
778
 
                                      in other.fields.iteritems()])
779
 
            namespaced_signed = ['%s.%s' % (namespace, k) for k
780
 
                                 in other.signed]
781
 
        else:
782
 
            namespaced_fields = other.fields
783
 
            namespaced_signed = other.signed
784
 
        self.fields.update(namespaced_fields)
785
 
        self.signed.extend(namespaced_signed)
786
 
 
787
 
 
788
901
    def needsSigning(self):
789
902
        """Does this response require signing?
790
903
 
791
904
        @returntype: bool
792
905
        """
793
 
        return (
794
 
            (self.request.mode in ['checkid_setup', 'checkid_immediate'])
795
 
            and self.signed
796
 
            )
 
906
        return self.fields.getArg(OPENID_NS, 'mode') == 'id_res'
797
907
 
798
908
 
799
909
    # implements IEncodable
817
927
        @returns: A URL to direct the user agent back to.
818
928
        @returntype: str
819
929
        """
820
 
        fields = dict(
821
 
            [(OPENID_PREFIX + k, v.encode('UTF8')) for k, v in self.fields.iteritems()])
822
 
        return oidutil.appendArgs(self.request.return_to, fields)
 
930
        return self.fields.toURL(self.request.return_to)
 
931
 
 
932
 
 
933
    def addExtension(self, extension_response):
 
934
        """
 
935
        Add an extension response to this response message.
 
936
 
 
937
        @param extension_response: An object that implements the
 
938
            extension interface for adding arguments to an OpenID
 
939
            message.
 
940
        @type extension_response: L{openid.extension}
 
941
 
 
942
        @returntype: None
 
943
        """
 
944
        extension_response.toMessage(self.fields)
823
945
 
824
946
 
825
947
    def encodeToKVForm(self):
833
955
 
834
956
        @returntype: str
835
957
        """
836
 
        return kvform.dictToKV(self.fields)
837
 
 
838
 
 
839
 
    def __str__(self):
840
 
        return "%s for %s: signed%s %s" % (
841
 
            self.__class__.__name__,
842
 
            self.request.__class__.__name__,
843
 
            self.signed, self.fields)
 
958
        return self.fields.toKVForm()
844
959
 
845
960
 
846
961
 
908
1023
        self.store = store
909
1024
 
910
1025
 
911
 
    def verify(self, assoc_handle, sig, signed_pairs):
 
1026
    def verify(self, assoc_handle, message):
912
1027
        """Verify that the signature for some data is valid.
913
1028
 
914
1029
        @param assoc_handle: The handle of the association used to sign the
915
1030
            data.
916
1031
        @type assoc_handle: str
917
1032
 
918
 
        @param sig: The base-64 encoded signature to check.
919
 
        @type sig: str
920
 
 
921
 
        @param signed_pairs: The data to check, an ordered list of key-value
922
 
            pairs.  The keys should be as they are in the request's C{signed}
923
 
            list, without any C{"openid."} prefix.
924
 
        @type signed_pairs: list of pairs
 
1033
        @param message: The signed message to verify
 
1034
        @type message: openid.message.Message
925
1035
 
926
1036
        @returns: C{True} if the signature is valid, C{False} if not.
927
1037
        @returntype: bool
928
1038
        """
929
1039
        assoc = self.getAssociation(assoc_handle, dumb=True)
930
1040
        if not assoc:
931
 
            oidutil.log("failed to get assoc with handle %r to verify sig %r"
932
 
                        % (assoc_handle, sig))
933
 
            return False
934
 
 
935
 
        # Not using Association.checkSignature here is intentional;
936
 
        # Association should not know things like "the list of signed pairs is
937
 
        # in the request's 'signed' parameter and it is comma-separated."
938
 
        expected_sig = oidutil.toBase64(assoc.sign(signed_pairs))
939
 
 
940
 
        return sig == expected_sig
 
1041
            oidutil.log("failed to get assoc with handle %r to verify "
 
1042
                        "message %r"
 
1043
                        % (assoc_handle, message))
 
1044
            return False
 
1045
 
 
1046
        try:
 
1047
            valid = assoc.checkMessageSignature(message)
 
1048
        except ValueError, ex:
 
1049
            oidutil.log("Error in verifying %s with %s: %s" % (message,
 
1050
                                                               assoc,
 
1051
                                                               ex))
 
1052
            return False
 
1053
        return valid
941
1054
 
942
1055
 
943
1056
    def sign(self, response):
957
1070
        assoc_handle = response.request.assoc_handle
958
1071
        if assoc_handle:
959
1072
            # normal mode
960
 
            assoc = self.getAssociation(assoc_handle, dumb=False)
961
 
            if not assoc:
 
1073
            # disabling expiration check because even if the association
 
1074
            # is expired, we still need to know some properties of the
 
1075
            # association so that we may preserve those properties when
 
1076
            # creating the fallback association.
 
1077
            assoc = self.getAssociation(assoc_handle, dumb=False,
 
1078
                                        checkExpiration=False)
 
1079
 
 
1080
            if not assoc or assoc.expiresIn <= 0:
962
1081
                # fall back to dumb mode
963
 
                signed_response.fields['invalidate_handle'] = assoc_handle
964
 
                assoc = self.createAssociation(dumb=True)
 
1082
                signed_response.fields.setArg(
 
1083
                    OPENID_NS, 'invalidate_handle', assoc_handle)
 
1084
                assoc_type = assoc and assoc.assoc_type or 'HMAC-SHA1'
 
1085
                if assoc and assoc.expiresIn <= 0:
 
1086
                    # now do the clean-up that the disabled checkExpiration
 
1087
                    # code didn't get to do.
 
1088
                    self.invalidate(assoc_handle, dumb=False)
 
1089
                assoc = self.createAssociation(dumb=True, assoc_type=assoc_type)
965
1090
        else:
966
1091
            # dumb mode.
967
1092
            assoc = self.createAssociation(dumb=True)
968
1093
 
969
 
        signed_response.fields['assoc_handle'] = assoc.handle
970
 
        assoc.addSignature(signed_response.signed, signed_response.fields,
971
 
                           prefix='')
 
1094
        signed_response.fields = assoc.signMessage(signed_response.fields)
972
1095
        return signed_response
973
1096
 
974
1097
 
985
1108
        @returns: the new association.
986
1109
        @returntype: L{openid.association.Association}
987
1110
        """
988
 
        secret = cryptutil.getBytes(20)
 
1111
        secret = cryptutil.getBytes(getSecretSize(assoc_type))
989
1112
        uniq = oidutil.toBase64(cryptutil.getBytes(4))
990
1113
        handle = '{%s}{%x}{%s}' % (assoc_type, int(time.time()), uniq)
991
1114
 
1000
1123
        return assoc
1001
1124
 
1002
1125
 
1003
 
    def getAssociation(self, assoc_handle, dumb):
 
1126
    def getAssociation(self, assoc_handle, dumb, checkExpiration=True):
1004
1127
        """Get the association with the specified handle.
1005
1128
 
1006
1129
        @type assoc_handle: str
1030
1153
            oidutil.log("requested %sdumb key %r is expired (by %s seconds)" %
1031
1154
                        ((not dumb) and 'not-' or '',
1032
1155
                         assoc_handle, assoc.expiresIn))
1033
 
            self.store.removeAssociation(key, assoc_handle)
1034
 
            assoc = None
 
1156
            if checkExpiration:
 
1157
                self.store.removeAssociation(key, assoc_handle)
 
1158
                assoc = None
1035
1159
        return assoc
1036
1160
 
1037
1161
 
1116
1240
                raise ValueError(
1117
1241
                    "Must have a store to sign this request: %s" %
1118
1242
                    (response,), response)
1119
 
            if 'sig' in response.fields:
 
1243
            if response.fields.hasKey(OPENID_NS, 'sig'):
1120
1244
                raise AlreadySigned(response)
1121
1245
            response = self.signatory.sign(response)
1122
1246
        return super(SigningEncoder, self).encode(response)
1128
1252
    """
1129
1253
 
1130
1254
    _handlers = {
1131
 
        'checkid_setup': CheckIDRequest.fromQuery,
1132
 
        'checkid_immediate': CheckIDRequest.fromQuery,
1133
 
        'check_authentication': CheckAuthRequest.fromQuery,
1134
 
        'associate': AssociateRequest.fromQuery,
 
1255
        'checkid_setup': CheckIDRequest.fromMessage,
 
1256
        'checkid_immediate': CheckIDRequest.fromMessage,
 
1257
        'check_authentication': CheckAuthRequest.fromMessage,
 
1258
        'associate': AssociateRequest.fromMessage,
1135
1259
        }
1136
1260
 
 
1261
    def __init__(self, server):
 
1262
        """Construct a Decoder.
 
1263
 
 
1264
        @param server: The server which I am decoding requests for.
 
1265
            (Necessary because some replies reference their server.)
 
1266
        @type server: L{Server}
 
1267
        """
 
1268
        self.server = server
1137
1269
 
1138
1270
    def decode(self, query):
1139
1271
        """I transform query parameters into an L{OpenIDRequest}.
1152
1284
        """
1153
1285
        if not query:
1154
1286
            return None
1155
 
        myquery = dict(filter(lambda (k, v): k.startswith(OPENID_PREFIX),
1156
 
                              query.iteritems()))
1157
 
        if not myquery:
1158
 
            return None
1159
 
 
1160
 
        mode = myquery.get(OPENID_PREFIX + 'mode')
1161
 
        if isinstance(mode, list):
1162
 
            raise TypeError("query dict must have one value for each key, "
1163
 
                            "not lists of values.  Query is %r" % (query,))
1164
 
 
 
1287
 
 
1288
        message = Message.fromPostArgs(query)
 
1289
 
 
1290
        mode = message.getArg(OPENID_NS, 'mode')
1165
1291
        if not mode:
1166
 
            raise ProtocolError(
1167
 
                query,
1168
 
                text="No %smode value in query %r" % (
1169
 
                OPENID_PREFIX, query))
 
1292
            fmt = "No mode value in message %s"
 
1293
            raise ProtocolError(message, text=fmt % (message,))
 
1294
 
1170
1295
        handler = self._handlers.get(mode, self.defaultDecoder)
1171
 
        return handler(query)
1172
 
 
1173
 
 
1174
 
    def defaultDecoder(self, query):
 
1296
        return handler(message, self.server.op_endpoint)
 
1297
 
 
1298
 
 
1299
    def defaultDecoder(self, message, server):
1175
1300
        """Called to decode queries when no handler for that mode is found.
1176
1301
 
1177
1302
        @raises ProtocolError: This implementation always raises
1178
1303
            L{ProtocolError}.
1179
1304
        """
1180
 
        mode = query[OPENID_PREFIX + 'mode']
1181
 
        raise ProtocolError(
1182
 
            query,
1183
 
            text="No decoder for mode %r" % (mode,))
 
1305
        mode = message.getArg(OPENID_NS, 'mode')
 
1306
        fmt = "No decoder for mode %r"
 
1307
        raise ProtocolError(message, text=fmt % (mode,))
1184
1308
 
1185
1309
 
1186
1310
 
1201
1325
 
1202
1326
    Example::
1203
1327
 
1204
 
        oserver = Server(FileOpenIDStore(data_path))
 
1328
        oserver = Server(FileOpenIDStore(data_path), "http://example.com/op")
1205
1329
        request = oserver.decodeRequest(query)
1206
 
        if request.mode in ["checkid_immediate", "checkid_setup"]:
 
1330
        if request.mode in ['checkid_immediate', 'checkid_setup']:
1207
1331
            if self.isAuthorized(request.identity, request.trust_root):
1208
1332
                response = request.answer(True)
1209
1333
            elif request.immediate:
1210
 
                response = request.answer(False, self.base_url)
 
1334
                response = request.answer(False)
1211
1335
            else:
1212
1336
                self.showDecidePage(request)
1213
1337
                return
1224
1348
 
1225
1349
    @ivar encoder: I'm using this to encode things.
1226
1350
    @type encoder: L{Encoder}
 
1351
 
 
1352
    @ivar op_endpoint: My URL.
 
1353
    @type op_endpoint: str
 
1354
 
 
1355
    @ivar negotiator: I use this to determine which kinds of
 
1356
        associations I can make and how.
 
1357
    @type negotiator: L{openid.association.SessionNegotiator}
1227
1358
    """
1228
1359
 
1229
1360
    signatoryClass = Signatory
1230
1361
    encoderClass = SigningEncoder
1231
1362
    decoderClass = Decoder
1232
1363
 
1233
 
    def __init__(self, store):
 
1364
    def __init__(self, store, op_endpoint=None):
1234
1365
        """A new L{Server}.
1235
1366
 
1236
1367
        @param store: The back-end where my associations are stored.
1237
1368
        @type store: L{openid.store.interface.OpenIDStore}
 
1369
 
 
1370
        @param op_endpoint: My URL, the fully qualified address of this
 
1371
            server's endpoint, i.e. C{http://example.com/server}
 
1372
        @type op_endpoint: str
 
1373
 
 
1374
        @change: C{op_endpoint} is new in library version 2.0.  It
 
1375
            currently defaults to C{None} for compatibility with
 
1376
            earlier versions of the library, but you must provide it
 
1377
            if you want to respond to any version 2 OpenID requests.
1238
1378
        """
1239
1379
        self.store = store
1240
1380
        self.signatory = self.signatoryClass(self.store)
1241
1381
        self.encoder = self.encoderClass(self.signatory)
1242
 
        self.decoder = self.decoderClass()
 
1382
        self.decoder = self.decoderClass(self)
 
1383
        self.negotiator = default_negotiator.copy()
 
1384
 
 
1385
        if not op_endpoint:
 
1386
            warnings.warn("%s.%s constructor requires op_endpoint parameter "
 
1387
                          "for OpenID 2.0 servers" %
 
1388
                          (self.__class__.__module__, self.__class__.__name__),
 
1389
                          stacklevel=2)
 
1390
        self.op_endpoint = op_endpoint
1243
1391
 
1244
1392
 
1245
1393
    def handleRequest(self, request):
1252
1400
 
1253
1401
        @raises NotImplementedError: When I do not have a handler defined
1254
1402
            for that type of request.
 
1403
 
 
1404
        @returntype: L{OpenIDResponse}
1255
1405
        """
1256
1406
        handler = getattr(self, 'openid_' + request.mode, None)
1257
1407
        if handler is not None:
1263
1413
 
1264
1414
 
1265
1415
    def openid_check_authentication(self, request):
1266
 
        """Handle and respond to {check_authentication} requests.
 
1416
        """Handle and respond to C{check_authentication} requests.
1267
1417
 
1268
1418
        @returntype: L{OpenIDResponse}
1269
1419
        """
1271
1421
 
1272
1422
 
1273
1423
    def openid_associate(self, request):
1274
 
        """Handle and respond to {associate} requests.
 
1424
        """Handle and respond to C{associate} requests.
1275
1425
 
1276
1426
        @returntype: L{OpenIDResponse}
1277
1427
        """
1278
 
        assoc = self.signatory.createAssociation(dumb=False)
1279
 
        return request.answer(assoc)
 
1428
        # XXX: TESTME
 
1429
        assoc_type = request.assoc_type
 
1430
        session_type = request.session.session_type
 
1431
        if self.negotiator.isAllowed(assoc_type, session_type):
 
1432
            assoc = self.signatory.createAssociation(dumb=False,
 
1433
                                                     assoc_type=assoc_type)
 
1434
            return request.answer(assoc)
 
1435
        else:
 
1436
            message = ('Association type %r is not supported with '
 
1437
                       'session type %r' % (assoc_type, session_type))
 
1438
            (preferred_assoc_type, preferred_session_type) = \
 
1439
                                   self.negotiator.getAllowedType()
 
1440
            return request.answerUnsupported(
 
1441
                message,
 
1442
                preferred_assoc_type,
 
1443
                preferred_session_type)
1280
1444
 
1281
1445
 
1282
1446
    def decodeRequest(self, query):
1309
1473
 
1310
1474
        @returntype: L{WebResponse}
1311
1475
 
1312
 
        @see: L{Encoder.encode}
 
1476
        @see: L{SigningEncoder.encode}
1313
1477
        """
1314
1478
        return self.encoder.encode(response)
1315
1479
 
1318
1482
class ProtocolError(Exception):
1319
1483
    """A message did not conform to the OpenID protocol.
1320
1484
 
1321
 
    @ivar query: The query that is failing to be a valid OpenID request.
1322
 
    @type query: dict
 
1485
    @ivar message: The query that is failing to be a valid OpenID request.
 
1486
    @type message: openid.message.Message
1323
1487
    """
1324
1488
 
1325
 
    def __init__(self, query, text=None):
 
1489
    def __init__(self, message, text=None, reference=None, contact=None):
1326
1490
        """When an error occurs.
1327
1491
 
1328
 
        @param query: The query that is failing to be a valid OpenID request.
1329
 
        @type query: dict
 
1492
        @param message: The message that is failing to be a valid
 
1493
            OpenID request.
 
1494
        @type message: openid.message.Message
1330
1495
 
1331
1496
        @param text: A message about the encountered error.  Set as C{args[0]}.
1332
1497
        @type text: str
1333
1498
        """
1334
 
        self.query = query
 
1499
        self.openid_message = message
 
1500
        self.reference = reference
 
1501
        self.contact = contact
 
1502
        assert type(message) not in [str, unicode]
1335
1503
        Exception.__init__(self, text)
1336
1504
 
1337
1505
 
 
1506
    def getReturnTo(self):
 
1507
        """Get the return_to argument from the request, if any.
 
1508
 
 
1509
        @returntype: str
 
1510
        """
 
1511
        if self.openid_message is None:
 
1512
            return False
 
1513
        else:
 
1514
            return self.openid_message.getArg(OPENID_NS, 'return_to')
 
1515
 
1338
1516
    def hasReturnTo(self):
1339
1517
        """Did this request have a return_to parameter?
1340
1518
 
1341
1519
        @returntype: bool
1342
1520
        """
1343
 
        if self.query is None:
1344
 
            return False
1345
 
        else:
1346
 
            return (OPENID_PREFIX + 'return_to') in self.query
1347
 
 
 
1521
        return self.getReturnTo() is not None
 
1522
 
 
1523
    def toMessage(self):
 
1524
        """Generate a Message object for sending to the relying party,
 
1525
        after encoding.
 
1526
        """
 
1527
        namespace = self.openid_message.getOpenIDNamespace()
 
1528
        reply = Message(namespace)
 
1529
        reply.setArg(OPENID_NS, 'mode', 'error')
 
1530
        reply.setArg(OPENID_NS, 'error', str(self))
 
1531
 
 
1532
        if self.contact is not None:
 
1533
            reply.setArg(OPENID_NS, 'contact', str(self.contact))
 
1534
 
 
1535
        if self.reference is not None:
 
1536
            reply.setArg(OPENID_NS, 'reference', str(self.reference))
 
1537
 
 
1538
        return reply
1348
1539
 
1349
1540
    # implements IEncodable
1350
1541
 
1351
1542
    def encodeToURL(self):
1352
 
        """Encode a response as a URL for the user agent to GET.
1353
 
 
1354
 
        You will generally use this URL with a HTTP redirect.
1355
 
 
1356
 
        @returns: A URL to direct the user agent back to.
1357
 
        @returntype: str
1358
 
        """
1359
 
        return_to = self.query.get(OPENID_PREFIX + 'return_to')
1360
 
        if not return_to:
1361
 
            raise ValueError("I have no return_to URL.")
1362
 
        return oidutil.appendArgs(return_to, {
1363
 
            'openid.mode': 'error',
1364
 
            'openid.error': str(self),
1365
 
            })
1366
 
 
 
1543
        return self.toMessage().toURL(self.getReturnTo())
1367
1544
 
1368
1545
    def encodeToKVForm(self):
1369
 
        """Encode a response in key-value colon/newline format.
1370
 
 
1371
 
        This is a machine-readable format used to respond to messages which
1372
 
        came directly from the consumer and not through the user agent.
1373
 
 
1374
 
        @see: OpenID Specs,
1375
 
           U{Key-Value Colon/Newline format<http://openid.net/specs.bml#keyvalue>}
1376
 
 
1377
 
        @returntype: str
1378
 
        """
1379
 
        return kvform.dictToKV({
1380
 
            'mode': 'error',
1381
 
            'error': str(self),
1382
 
            })
1383
 
 
 
1546
        return self.toMessage().toKVForm()
1384
1547
 
1385
1548
    def whichEncoding(self):
1386
1549
        """How should I be encoded?
1392
1555
        if self.hasReturnTo():
1393
1556
            return ENCODE_URL
1394
1557
 
1395
 
        if self.query is None:
 
1558
        if self.openid_message is None:
1396
1559
            return None
1397
1560
 
1398
 
        mode = self.query.get('openid.mode')
 
1561
        mode = self.openid_message.getArg(OPENID_NS, 'mode')
1399
1562
        if mode:
1400
1563
            if mode not in BROWSER_REQUEST_MODES:
1401
1564
                return ENCODE_KVFORM
1415
1578
 
1416
1579
 
1417
1580
 
 
1581
class VersionError(Exception):
 
1582
    """Raised when an operation was attempted that is not compatible with
 
1583
    the protocol version being used."""
 
1584
 
 
1585
 
 
1586
 
 
1587
class NoReturnToError(Exception):
 
1588
    """Raised when a response to a request cannot be generated because
 
1589
    the request contains no return_to URL.
 
1590
    """
 
1591
    pass
 
1592
 
 
1593
 
 
1594
 
1418
1595
class EncodingError(Exception):
1419
1596
    """Could not encode this as a protocol message.
1420
1597
 
1438
1615
class UntrustedReturnURL(ProtocolError):
1439
1616
    """A return_to is outside the trust_root."""
1440
1617
 
1441
 
    def __init__(self, query, return_to, trust_root):
1442
 
        ProtocolError.__init__(self, query)
 
1618
    def __init__(self, message, return_to, trust_root):
 
1619
        ProtocolError.__init__(self, message)
1443
1620
        self.return_to = return_to
1444
1621
        self.trust_root = trust_root
1445
1622
 
1450
1627
 
1451
1628
class MalformedReturnURL(ProtocolError):
1452
1629
    """The return_to URL doesn't look like a valid URL."""
1453
 
    def __init__(self, query, return_to):
 
1630
    def __init__(self, openid_message, return_to):
1454
1631
        self.return_to = return_to
1455
 
        ProtocolError.__init__(self, query)
 
1632
        ProtocolError.__init__(self, openid_message)
1456
1633
 
1457
1634
 
1458
1635