~canonical-isd-hackers/canonical-identity-provider/sst-changes

« back to all changes in this revision

Viewing changes to identityprovider/views/server.py

  • Committer: Danny Tamez
  • Date: 2010-04-21 15:29:24 UTC
  • Revision ID: danny.tamez@canonical.com-20100421152924-lq1m92tstk2iz75a
Canonical SSO Provider (Open Source) - Initial Commit

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Copyright 2010 Canonical Ltd.  This software is licensed under the
 
2
# GNU Affero General Public License version 3 (see the file LICENSE).
 
3
 
 
4
import logging
 
5
import re
 
6
import urllib
 
7
import urlparse
 
8
 
 
9
from datetime import datetime, timedelta
 
10
 
 
11
from openid.extensions import pape
 
12
from openid.extensions.sreg import SRegRequest, SRegResponse
 
13
from openid.message import IDENTIFIER_SELECT, registerNamespaceAlias
 
14
from openid.server.server import (CheckIDRequest, ENCODE_URL, ProtocolError,
 
15
    Server)
 
16
from openid.server.trustroot import TrustRoot
 
17
from openid.urinorm import urinorm
 
18
from openid.yadis.constants import YADIS_HEADER_NAME
 
19
 
 
20
import django.contrib.auth as auth
 
21
 
 
22
from django.conf import settings
 
23
from django.contrib.auth.decorators import login_required
 
24
from django.http import (Http404, HttpResponse, HttpResponseBadRequest,
 
25
    HttpResponseRedirect)
 
26
from django.template import RequestContext
 
27
from django.shortcuts import get_object_or_404, render_to_response
 
28
from django.utils.decorators import decorator_from_middleware
 
29
 
 
30
import identityprovider.signed as signed
 
31
 
 
32
from identityprovider.const import (LAUNCHPAD_TEAMS_NS,
 
33
                       PERSON_VISIBILITY_PUBLIC,
 
34
                       SREG_DATA_FIELDS_ORDER)
 
35
from identityprovider.forms import PreAuthorizeForm
 
36
from identityprovider.middleware.xrds import XRDSMiddleware
 
37
from identityprovider.models import (Account, DjangoOpenIDStore,
 
38
                        OpenIDAuthorization, OpenIDRPSummary,
 
39
                        Person)
 
40
from identityprovider.models.authtoken import create_token
 
41
from identityprovider.views import ui, utils
 
42
from identityprovider.const import SREG_LABELS
 
43
from identityprovider.middleware.csrf import csrf_exempt
 
44
 
 
45
accept_xrds = decorator_from_middleware(XRDSMiddleware)
 
46
registerNamespaceAlias(LAUNCHPAD_TEAMS_NS, 'lp')
 
47
logger = logging.getLogger('sso')
 
48
 
 
49
 
 
50
@csrf_exempt
 
51
@accept_xrds
 
52
def openid_provider(request):
 
53
    openid_server = _get_openid_server()
 
54
    querydict = dict(request.REQUEST.items())
 
55
    logger.debug("querydict = " + str(querydict))
 
56
    try:
 
57
        orequest = openid_server.decodeRequest(querydict)
 
58
        return _process_openid_request(request, orequest, openid_server)
 
59
    except ProtocolError, e:
 
60
        return _handle_openid_error(e)
 
61
 
 
62
 
 
63
def _process_openid_request(request, orequest, openid_server):
 
64
    if not orequest:
 
65
        context = RequestContext(request)
 
66
        return render_to_response('server_info.html', context)
 
67
    elif orequest.mode in ("checkid_immediate", "checkid_setup"):
 
68
        if (utils.get_rpconfig(orequest.trust_root) is None and
 
69
            getattr(settings, 'SSO_RESTRICT_RP', True)):
 
70
            # This is an untrusted RP.  We don't play with these for now.
 
71
            logger.debug("Untrusted RP: %s" % orequest.trust_root)
 
72
            token = create_token(16)
 
73
            request.session[token] = signed.dumps(orequest,
 
74
                                                  settings.SECRET_KEY)
 
75
            return HttpResponseRedirect('/%s/+untrusted' % token)
 
76
        return _handle_user_response(request, orequest)
 
77
    else:
 
78
        oresponse = openid_server.handleRequest(orequest)
 
79
        return _django_response(request, oresponse)
 
80
 
 
81
 
 
82
def _handle_openid_error(error):
 
83
    if error.whichEncoding() == ENCODE_URL:
 
84
        url = error.encodeToURL()
 
85
        return HttpResponseRedirect(url)
 
86
    else:
 
87
        response = HttpResponse(error.encodeToKVForm())
 
88
        response['Content-Type'] = 'text/plain;charset=utf-8'
 
89
        return response
 
90
 
 
91
 
 
92
def _handle_user_response(request, orequest):
 
93
    response = None
 
94
    if orequest.immediate:
 
95
        # We don't currently handle check_immediate requests
 
96
        oresponse = orequest.answer(False, settings.SSO_PROVIDER_URL)
 
97
        response = _django_response(request, oresponse)
 
98
    elif not _is_valid_openid_for_this_site(orequest.identity):
 
99
        context = RequestContext(request, {
 
100
            'trust_root': orequest.trust_root,
 
101
            'identifier': orequest.identity,
 
102
            'root_url': settings.SSO_ROOT_URL,
 
103
            'continue_url': orequest.answer(False).encodeToURL(),
 
104
        })
 
105
        response = render_to_response('invalid_identifier.html', context)
 
106
    elif _openid_is_authorized(request, orequest):
 
107
        if orequest.idSelect():
 
108
            oresponse = orequest.answer(
 
109
                True, identity=request.user.openid_identity_url)
 
110
        else:
 
111
            oresponse = orequest.answer(True)
 
112
        _add_sreg(request.user, orequest, oresponse)
 
113
        _check_team_membership(request.user, orequest, oresponse)
 
114
        response = _django_response(request, oresponse, True)
 
115
    elif (request.user.is_authenticated() and not
 
116
          _is_identity_owner(request.user, orequest)):
 
117
        oresponse = orequest.answer(False)
 
118
        response = _django_response(request, oresponse, True)
 
119
    else:
 
120
        token = create_token(16)
 
121
        request.session[token] = signed.dumps(orequest,
 
122
                                              settings.SECRET_KEY)
 
123
        response = HttpResponseRedirect('/%s/+decide' % token)
 
124
    referer = request.META.get('HTTP_REFERER')
 
125
    if referer:
 
126
        response.set_cookie('openid_referer', referer)
 
127
    return response
 
128
 
 
129
 
 
130
def _is_valid_openid_for_this_site(identity):
 
131
    try:
 
132
        urinorm(identity)
 
133
        idparts = urlparse.urlparse(identity)
 
134
        srvparts = urlparse.urlparse(settings.SSO_ROOT_URL)
 
135
        if identity == IDENTIFIER_SELECT:
 
136
            return True
 
137
        elif (idparts.port != srvparts.port or idparts.scheme !=
 
138
              srvparts.scheme):
 
139
            return False
 
140
        elif not (idparts.hostname == srvparts.hostname or \
 
141
                srvparts.hostname.endswith(".%s" % idparts.hostname)):
 
142
            return False
 
143
        accept_path_patterns = [
 
144
            '/',
 
145
            '^/\+id/[a-zA-Z0-9\-_\.]+$',
 
146
            '^/~[a-zA-Z0-9\-_\.]+$',
 
147
        ]
 
148
        for pattern in accept_path_patterns:
 
149
            if re.match(pattern, idparts.path) is not None:
 
150
                return True
 
151
        return False
 
152
    except:
 
153
        return False
 
154
 
 
155
 
 
156
def decide(request, token):
 
157
    try:
 
158
        raw_orequest = request.session.get(token, None)
 
159
        orequest = signed.loads(raw_orequest, settings.SECRET_KEY)
 
160
        rpconfig = utils.get_rpconfig(orequest.trust_root)
 
161
    except:
 
162
        return HttpResponse("Invalid OpenID transaction")
 
163
 
 
164
    if request.user.is_authenticated():
 
165
        if 'yes' in request.POST or (rpconfig is not None
 
166
            and rpconfig.auto_authorize):
 
167
            return _process_decide(request, orequest, True)
 
168
        else:
 
169
            sreg_request = SRegRequest.fromOpenIDRequest(orequest)
 
170
            sreg_data = _sreg_fields(request.user, sreg_request, rpconfig)
 
171
            sreg_data = _humanize_sreg_labels(sreg_data)
 
172
            context = RequestContext(request, {
 
173
                'account': request.user,
 
174
                'trust_root': orequest.trust_root,
 
175
                'rpconfig': rpconfig,
 
176
                'sreg_data': sreg_data,
 
177
                'token': token,
 
178
                'message': request.session.get('message', None),
 
179
                'message_style': 'informational',
 
180
                'sane_trust_root': _request_has_sane_trust_root(orequest)
 
181
            })
 
182
            try:
 
183
                del request.session['message']
 
184
            except KeyError:
 
185
                pass
 
186
            return render_to_response('decide.html', context)
 
187
    else:
 
188
        return ui.login(request, token, rpconfig=rpconfig)
 
189
 
 
190
 
 
191
def _request_has_sane_trust_root(openid_request):
 
192
    """Return True if the RP's trust root looks sane."""
 
193
    assert openid_request is not None, (
 
194
        'Could not find the OpenID request')
 
195
    trust_root = TrustRoot.parse(openid_request.trust_root)
 
196
    return trust_root.isSane()
 
197
 
 
198
 
 
199
def pre_authorize(request):
 
200
    logger.debug("HTTP_REFERER for this request: %s\n >>> \n" %
 
201
                 request.META.get('HTTP_REFERER', 'None'))
 
202
    form = PreAuthorizeForm(request.REQUEST)
 
203
    if form.is_valid():
 
204
        try:
 
205
            trust_root, callback, referer = \
 
206
                    _get_valid_pre_auth_data(request, form)
 
207
        except:
 
208
            # Unauthorized trust root or referrer.
 
209
            _clear_pre_auth_session_data(request)
 
210
            return HttpResponseBadRequest()
 
211
 
 
212
        if request.user.is_authenticated():
 
213
            logger.debug("Approved for %s, %s\n >>> \n" %
 
214
                         (referer, request.user))
 
215
            client_id = request.session.session_key
 
216
            expires = (datetime.utcnow()
 
217
                + timedelta(hours=getattr(
 
218
                    settings, 'PRE_AUTHORIZATION_VALIDITY', 2)))
 
219
            OpenIDAuthorization.objects.authorize(
 
220
                request.user, trust_root, expires, client_id)
 
221
            return HttpResponseRedirect(callback)
 
222
        else:
 
223
            request.session['pre_auth_referer'] = referer
 
224
            request.session['pre_auth_referer_for'] = trust_root
 
225
            next = "%s?%s" % (request.META.get('PATH_INFO'),
 
226
                              request.META.get('QUERY_STRING', ''))
 
227
            return HttpResponseRedirect('/+login?next=%s' %
 
228
                                        urllib.quote(next))
 
229
    else:
 
230
        _clear_pre_auth_session_data(request)
 
231
        return HttpResponseBadRequest()
 
232
 
 
233
 
 
234
def _get_valid_pre_auth_data(request, form):
 
235
    trust_root = form.cleaned_data['trust_root']
 
236
    callback = form.cleaned_data['callback']
 
237
    http_referer = _get_pre_auth_referer(request, trust_root)
 
238
 
 
239
    if http_referer is None:
 
240
        raise Exception("Pre-auth not approved")
 
241
 
 
242
    for line in getattr(settings, 'OPENID_PREAUTHORIZATION_ACL', []):
 
243
        referer, acl_trust_root = line
 
244
        if http_referer.startswith(referer) and trust_root == acl_trust_root:
 
245
            return (trust_root, callback, http_referer)
 
246
    raise Exception("Pre-auth not approved")
 
247
 
 
248
 
 
249
def _get_pre_auth_referer(request, trust_root):
 
250
    sess_referer = request.session.get('pre_auth_referer', None)
 
251
    sess_referer_for = request.session.get('pre_auth_referer_for', None)
 
252
    if sess_referer is not None and sess_referer_for is not None:
 
253
        _clear_pre_auth_session_data(request)
 
254
        if sess_referer_for == trust_root:
 
255
            logger.debug("getting referer %s from session" % sess_referer)
 
256
            logger.debug("http referer was %s" %
 
257
                         request.META.get('HTTP_REFERER', ''))
 
258
            return sess_referer
 
259
        else:
 
260
            return None
 
261
    else:
 
262
        return request.META.get('HTTP_REFERER', None)
 
263
 
 
264
 
 
265
def _clear_pre_auth_session_data(request):
 
266
    try:
 
267
        del request.session['pre_auth_referer']
 
268
        del request.session['pre_auth_referer_for']
 
269
    except:
 
270
        pass
 
271
 
 
272
 
 
273
def cancel(request, token):
 
274
    try:
 
275
        raw_orequest = request.session.get(token, None)
 
276
        orequest = signed.loads(raw_orequest, settings.SECRET_KEY)
 
277
    except:
 
278
        return HttpResponse("Invalid OpenID transaction")
 
279
    if request.user.is_authenticated():
 
280
        return _process_decide(request, orequest, False)
 
281
    else:
 
282
        oresponse = orequest.answer(False, settings.SSO_PROVIDER_URL)
 
283
        response = _django_response(request, oresponse)
 
284
        return response
 
285
 
 
286
 
 
287
def xrds(request):
 
288
    logger.debug("xrds()")
 
289
    context = {
 
290
        'endpoint_url': settings.SSO_PROVIDER_URL,
 
291
    }
 
292
    resp = render_to_response('openidapplication-xrds.xml', context)
 
293
    resp['Content-type'] = 'application/xrds+xml'
 
294
    return resp
 
295
 
 
296
 
 
297
@accept_xrds
 
298
def identity_page(request, identifier):
 
299
    account = get_object_or_404(Account, openid_identifier=identifier)
 
300
    if not account.is_active:
 
301
        raise Http404()
 
302
    context = {
 
303
        'provider_url': settings.SSO_PROVIDER_URL,
 
304
        'identity_url': account.openid_identity_url,
 
305
        'display_name': account.displayname,
 
306
    }
 
307
    resp = render_to_response('person.html', context)
 
308
    resp[YADIS_HEADER_NAME] = "%s/+xrds" % account.openid_identity_url
 
309
    return resp
 
310
 
 
311
 
 
312
def xrds_identity_page(request, identifier):
 
313
    account = get_object_or_404(Account, openid_identifier=identifier)
 
314
    if not account.is_active:
 
315
        raise Http404()
 
316
    context = {
 
317
        'provider_url': settings.SSO_PROVIDER_URL,
 
318
        'identity_url': account.openid_identity_url,
 
319
    }
 
320
    resp = render_to_response('person-xrds.xml', context)
 
321
    resp['Content-type'] = 'application/xrds+xml'
 
322
    return resp
 
323
 
 
324
 
 
325
def _openid_is_authorized(request, openid_request):
 
326
    logger.debug("openid_is_authorized(%s, %s)" %
 
327
                 (openid_request.identity, openid_request.trust_root))
 
328
 
 
329
    if (not request.user.is_authenticated() or
 
330
        not _is_identity_owner(request.user, openid_request)):
 
331
        logger.debug("openid_is_authorized() -> False (id_owner)")
 
332
        return False
 
333
 
 
334
    elif _should_reauthenticate(openid_request, request.user):
 
335
        logger.debug("openid_is_authorized() -> True (should_reauthenticate)")
 
336
        auth.logout(request)
 
337
        return False
 
338
 
 
339
    elif _is_auto_authorized_rp(utils.get_rpconfig(openid_request.trust_root)):
 
340
        logger.debug("openid_is_authorized() -> True (rpconfig)")
 
341
        return True
 
342
 
 
343
    else:
 
344
        ret = OpenIDAuthorization.objects.is_authorized(
 
345
            request.user, openid_request.trust_root,
 
346
            request.session.session_key)
 
347
        logger.debug("openid_is_authorized() -> %s" % ret)
 
348
        return ret
 
349
 
 
350
 
 
351
def _is_auto_authorized_rp(rp):
 
352
    return rp is not None and rp.auto_authorize
 
353
 
 
354
 
 
355
def _should_reauthenticate(openid_request, user):
 
356
    """Should the user re-enter their password?
 
357
 
 
358
    Return True if the user entered their password more than
 
359
    max_auth_age seconds ago. Return False otherwise.
 
360
 
 
361
    The max_auth_age parameter is defined in the OpenID Provider
 
362
    Authentication Policy Extension.
 
363
    http://openid.net/
 
364
        specs/openid-provider-authentication-policy-extension-1_0-07.html
 
365
 
 
366
    This parameter contains the maximum number of seconds before which
 
367
    an authenticated user must enter their password again. By default,
 
368
    there is no such maximum and if the user is logged in Launchpad, they
 
369
    can simply click-through to Sign In the relying party.
 
370
 
 
371
    But if the relaying party provides a value for that parameter, the
 
372
    user most have logged in not more than that number of seconds ago,
 
373
    Otherwise, they'll have to enter their password again.
 
374
    """
 
375
    pape_request = pape.Request.fromOpenIDRequest(openid_request)
 
376
 
 
377
    # If there is no parameter, the login is valid.
 
378
    if pape_request is None or pape_request.max_auth_age is None:
 
379
        logger.debug("No pape request")
 
380
        return False
 
381
 
 
382
    try:
 
383
        max_auth_age = int(pape_request.max_auth_age)
 
384
    except ValueError:
 
385
        logger.debug("pape:max_auth_age parameter should be an integer: %s" %
 
386
                     max_auth_age)
 
387
        raise HttpResponseBadRequest()
 
388
 
 
389
    cutoff = datetime.utcnow() - timedelta(seconds=max_auth_age)
 
390
    logger.debug("%s" % user.last_login)
 
391
    return user.last_login <= cutoff
 
392
 
 
393
 
 
394
def _django_response(request, oresponse, auth_success=False):
 
395
    """ Convert an OpenID response into a Django HttpResponse """
 
396
    webresponse = _get_openid_server().encodeResponse(oresponse)
 
397
    response = HttpResponse(webresponse.body, mimetype="text/plain")
 
398
    response.status_code = webresponse.code
 
399
    for key, value in webresponse.headers.items():
 
400
        response[key] = value
 
401
        logger.debug("response[%s] = %s" % (key, value))
 
402
    logger.debug("response_body = " + webresponse.body)
 
403
    if auth_success and isinstance(oresponse.request, CheckIDRequest):
 
404
        logger.debug("oresponse.fields = " + str(oresponse.fields))
 
405
        OpenIDRPSummary.objects.record(request.user,
 
406
                                       oresponse.request.trust_root)
 
407
    return response
 
408
 
 
409
 
 
410
def _sreg_fields(account, sreg_request, rpconfig):
 
411
    field_names = set(sreg_request.required + sreg_request.optional)
 
412
    if rpconfig is None:
 
413
        # The nickname field is permitted by default.
 
414
        field_names.intersection_update(['nickname'])
 
415
    elif rpconfig.allowed_sreg is not None:
 
416
        field_names.intersection_update(rpconfig.allowed_sreg.split(','))
 
417
    sreg_field_names = [name for name in SREG_DATA_FIELDS_ORDER
 
418
                        if name in field_names]
 
419
    # Collect registration values
 
420
    values = {}
 
421
    values['fullname'] = account.displayname
 
422
    if account.preferredemail is not None:
 
423
        values['email'] = account.preferredemail.email
 
424
    if account.person is not None:
 
425
        values['nickname'] = account.person.name
 
426
        if account.person.time_zone is not None:
 
427
            values['timezone'] = account.person.time_zone
 
428
    logger.debug("values (sreg_fields) = " + str(values))
 
429
    return [(field, values[field])
 
430
            for field in sreg_field_names if field in values]
 
431
 
 
432
 
 
433
def _humanize_sreg_labels(sreg_data):
 
434
    new_data = []
 
435
    for k, v in sreg_data:
 
436
        k = SREG_LABELS.get(k, k)
 
437
        new_data.append((k, v))
 
438
    return new_data
 
439
 
 
440
 
 
441
def _is_identity_owner(user, openid_request):
 
442
    assert user is not None, (
 
443
        "user should be logged in by now.")
 
444
    ret = (openid_request.idSelect() or
 
445
           openid_request.identity == user.openid_identity_url)
 
446
    logger.debug("_is_identity_owner() -> %s" % ret)
 
447
    return ret
 
448
 
 
449
 
 
450
def _add_sreg(account, openid_request, openid_response):
 
451
    # Add sreg result data
 
452
    sreg_request = SRegRequest.fromOpenIDRequest(openid_request)
 
453
    sreg_fields = _sreg_fields(account, sreg_request,
 
454
        utils.get_rpconfig(openid_request.trust_root))
 
455
    if sreg_fields:
 
456
        sreg_response = SRegResponse.extractResponse(
 
457
            sreg_request, dict(sreg_fields))
 
458
        openid_response.addExtension(sreg_response)
 
459
 
 
460
 
 
461
@login_required
 
462
def _process_decide(request, orequest, decision):
 
463
    oresponse = orequest.answer(decision,
 
464
        identity=request.user.openid_identity_url)
 
465
    if decision:
 
466
        # If they use PAPE, let them know of the last logged in time.
 
467
        pape_request = pape.Request.fromOpenIDRequest(orequest)
 
468
        if pape_request:
 
469
            last_login = request.user.last_login
 
470
            pape_response = pape.Response(
 
471
                auth_time=last_login.strftime('%Y-%m-%dT%H:%M:%SZ'))
 
472
            oresponse.addExtension(pape_response)
 
473
 
 
474
        OpenIDAuthorization.objects.authorize(
 
475
            request.user,
 
476
            orequest.trust_root,
 
477
            datetime.now(),
 
478
            request.session.session_key)
 
479
        _add_sreg(request.user, orequest, oresponse)
 
480
        _check_team_membership(request.user, orequest, oresponse)
 
481
    return _django_response(request, oresponse, decision)
 
482
 
 
483
 
 
484
def _get_openid_server():
 
485
    logger.debug("_get_server()")
 
486
    store = DjangoOpenIDStore()
 
487
    openid_server = Server(store, settings.SSO_PROVIDER_URL)
 
488
    return openid_server
 
489
 
 
490
 
 
491
def _check_team_membership(account, openid_request, openid_response):
 
492
    """Perform team membership checks.
 
493
 
 
494
    If any team membership checks have been requested as part of
 
495
    the OpenID request, annotate the response with the list of
 
496
    teams the user is actually a member of.
 
497
    """
 
498
    logger.debug("_check_team_membership(%s, %s, %s)" %
 
499
                 (account, openid_request, openid_response))
 
500
    assert account is not None, (
 
501
        'Must be logged in to calculate team membership')
 
502
    if account.person is None:
 
503
        logger.debug("_check_team_membership() -> no Person found")
 
504
        return
 
505
    args = openid_request.message.getArgs(LAUNCHPAD_TEAMS_NS)
 
506
    team_names = args.get('query_membership')
 
507
    if not team_names:
 
508
        logger.debug("_check_team_membership() -> no membership query")
 
509
        return
 
510
    team_names = team_names.split(',')
 
511
    logger.debug("_check_team_membership() teams = %s" + str(team_names))
 
512
    only_public_teams = (
 
513
        utils.get_rpconfig(openid_request.trust_root) is None or not
 
514
        utils.get_rpconfig(openid_request.trust_root).can_query_any_team)
 
515
    memberships = get_team_memberships(
 
516
        team_names, account, only_public_teams)
 
517
    openid_response.fields.namespaces.addAlias(LAUNCHPAD_TEAMS_NS, 'lp')
 
518
    openid_response.fields.setArg(
 
519
        LAUNCHPAD_TEAMS_NS, 'is_member', ','.join(memberships))
 
520
    logger.debug("_check_team_membership() memberships = " + str(memberships))
 
521
 
 
522
 
 
523
def get_team_memberships(team_names, account, only_public_teams):
 
524
    memberships = []
 
525
    for team_name in team_names:
 
526
        try:
 
527
            team = Person.objects.get(name=team_name)
 
528
        except Person.DoesNotExist:
 
529
            team = None
 
530
        if team is None or not team.is_team():
 
531
            continue
 
532
        # Control access to private teams
 
533
        if (team.visibility != PERSON_VISIBILITY_PUBLIC and only_public_teams):
 
534
            continue
 
535
        if account.person.in_team(team):
 
536
            memberships.append(team_name)
 
537
    return memberships
 
538
 
 
539
 
 
540
def untrusted(request, token):
 
541
    raw_orequest = request.session.get(token, None)
 
542
    orequest = signed.loads(raw_orequest, settings.SECRET_KEY)
 
543
    context = RequestContext(request, {
 
544
        'trust_root': orequest.trust_root,
 
545
    })
 
546
    return render_to_response('untrusted.html', context)