1
# Copyright 2010 Canonical Ltd. This software is licensed under the
2
# GNU Affero General Public License version 3 (see the file LICENSE).
9
from datetime import datetime, timedelta
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,
16
from openid.server.trustroot import TrustRoot
17
from openid.urinorm import urinorm
18
from openid.yadis.constants import YADIS_HEADER_NAME
20
import django.contrib.auth as auth
22
from django.conf import settings
23
from django.contrib.auth.decorators import login_required
24
from django.http import (Http404, HttpResponse, HttpResponseBadRequest,
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
30
import identityprovider.signed as signed
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,
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
45
accept_xrds = decorator_from_middleware(XRDSMiddleware)
46
registerNamespaceAlias(LAUNCHPAD_TEAMS_NS, 'lp')
47
logger = logging.getLogger('sso')
52
def openid_provider(request):
53
openid_server = _get_openid_server()
54
querydict = dict(request.REQUEST.items())
55
logger.debug("querydict = " + str(querydict))
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)
63
def _process_openid_request(request, orequest, openid_server):
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,
75
return HttpResponseRedirect('/%s/+untrusted' % token)
76
return _handle_user_response(request, orequest)
78
oresponse = openid_server.handleRequest(orequest)
79
return _django_response(request, oresponse)
82
def _handle_openid_error(error):
83
if error.whichEncoding() == ENCODE_URL:
84
url = error.encodeToURL()
85
return HttpResponseRedirect(url)
87
response = HttpResponse(error.encodeToKVForm())
88
response['Content-Type'] = 'text/plain;charset=utf-8'
92
def _handle_user_response(request, orequest):
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(),
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)
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)
120
token = create_token(16)
121
request.session[token] = signed.dumps(orequest,
123
response = HttpResponseRedirect('/%s/+decide' % token)
124
referer = request.META.get('HTTP_REFERER')
126
response.set_cookie('openid_referer', referer)
130
def _is_valid_openid_for_this_site(identity):
133
idparts = urlparse.urlparse(identity)
134
srvparts = urlparse.urlparse(settings.SSO_ROOT_URL)
135
if identity == IDENTIFIER_SELECT:
137
elif (idparts.port != srvparts.port or idparts.scheme !=
140
elif not (idparts.hostname == srvparts.hostname or \
141
srvparts.hostname.endswith(".%s" % idparts.hostname)):
143
accept_path_patterns = [
145
'^/\+id/[a-zA-Z0-9\-_\.]+$',
146
'^/~[a-zA-Z0-9\-_\.]+$',
148
for pattern in accept_path_patterns:
149
if re.match(pattern, idparts.path) is not None:
156
def decide(request, token):
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)
162
return HttpResponse("Invalid OpenID transaction")
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)
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,
178
'message': request.session.get('message', None),
179
'message_style': 'informational',
180
'sane_trust_root': _request_has_sane_trust_root(orequest)
183
del request.session['message']
186
return render_to_response('decide.html', context)
188
return ui.login(request, token, rpconfig=rpconfig)
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()
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)
205
trust_root, callback, referer = \
206
_get_valid_pre_auth_data(request, form)
208
# Unauthorized trust root or referrer.
209
_clear_pre_auth_session_data(request)
210
return HttpResponseBadRequest()
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)
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' %
230
_clear_pre_auth_session_data(request)
231
return HttpResponseBadRequest()
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)
239
if http_referer is None:
240
raise Exception("Pre-auth not approved")
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")
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', ''))
262
return request.META.get('HTTP_REFERER', None)
265
def _clear_pre_auth_session_data(request):
267
del request.session['pre_auth_referer']
268
del request.session['pre_auth_referer_for']
273
def cancel(request, token):
275
raw_orequest = request.session.get(token, None)
276
orequest = signed.loads(raw_orequest, settings.SECRET_KEY)
278
return HttpResponse("Invalid OpenID transaction")
279
if request.user.is_authenticated():
280
return _process_decide(request, orequest, False)
282
oresponse = orequest.answer(False, settings.SSO_PROVIDER_URL)
283
response = _django_response(request, oresponse)
288
logger.debug("xrds()")
290
'endpoint_url': settings.SSO_PROVIDER_URL,
292
resp = render_to_response('openidapplication-xrds.xml', context)
293
resp['Content-type'] = 'application/xrds+xml'
298
def identity_page(request, identifier):
299
account = get_object_or_404(Account, openid_identifier=identifier)
300
if not account.is_active:
303
'provider_url': settings.SSO_PROVIDER_URL,
304
'identity_url': account.openid_identity_url,
305
'display_name': account.displayname,
307
resp = render_to_response('person.html', context)
308
resp[YADIS_HEADER_NAME] = "%s/+xrds" % account.openid_identity_url
312
def xrds_identity_page(request, identifier):
313
account = get_object_or_404(Account, openid_identifier=identifier)
314
if not account.is_active:
317
'provider_url': settings.SSO_PROVIDER_URL,
318
'identity_url': account.openid_identity_url,
320
resp = render_to_response('person-xrds.xml', context)
321
resp['Content-type'] = 'application/xrds+xml'
325
def _openid_is_authorized(request, openid_request):
326
logger.debug("openid_is_authorized(%s, %s)" %
327
(openid_request.identity, openid_request.trust_root))
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)")
334
elif _should_reauthenticate(openid_request, request.user):
335
logger.debug("openid_is_authorized() -> True (should_reauthenticate)")
339
elif _is_auto_authorized_rp(utils.get_rpconfig(openid_request.trust_root)):
340
logger.debug("openid_is_authorized() -> True (rpconfig)")
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)
351
def _is_auto_authorized_rp(rp):
352
return rp is not None and rp.auto_authorize
355
def _should_reauthenticate(openid_request, user):
356
"""Should the user re-enter their password?
358
Return True if the user entered their password more than
359
max_auth_age seconds ago. Return False otherwise.
361
The max_auth_age parameter is defined in the OpenID Provider
362
Authentication Policy Extension.
364
specs/openid-provider-authentication-policy-extension-1_0-07.html
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.
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.
375
pape_request = pape.Request.fromOpenIDRequest(openid_request)
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")
383
max_auth_age = int(pape_request.max_auth_age)
385
logger.debug("pape:max_auth_age parameter should be an integer: %s" %
387
raise HttpResponseBadRequest()
389
cutoff = datetime.utcnow() - timedelta(seconds=max_auth_age)
390
logger.debug("%s" % user.last_login)
391
return user.last_login <= cutoff
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)
410
def _sreg_fields(account, sreg_request, rpconfig):
411
field_names = set(sreg_request.required + sreg_request.optional)
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
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]
433
def _humanize_sreg_labels(sreg_data):
435
for k, v in sreg_data:
436
k = SREG_LABELS.get(k, k)
437
new_data.append((k, v))
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)
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))
456
sreg_response = SRegResponse.extractResponse(
457
sreg_request, dict(sreg_fields))
458
openid_response.addExtension(sreg_response)
462
def _process_decide(request, orequest, decision):
463
oresponse = orequest.answer(decision,
464
identity=request.user.openid_identity_url)
466
# If they use PAPE, let them know of the last logged in time.
467
pape_request = pape.Request.fromOpenIDRequest(orequest)
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)
474
OpenIDAuthorization.objects.authorize(
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)
484
def _get_openid_server():
485
logger.debug("_get_server()")
486
store = DjangoOpenIDStore()
487
openid_server = Server(store, settings.SSO_PROVIDER_URL)
491
def _check_team_membership(account, openid_request, openid_response):
492
"""Perform team membership checks.
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.
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")
505
args = openid_request.message.getArgs(LAUNCHPAD_TEAMS_NS)
506
team_names = args.get('query_membership')
508
logger.debug("_check_team_membership() -> no membership query")
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))
523
def get_team_memberships(team_names, account, only_public_teams):
525
for team_name in team_names:
527
team = Person.objects.get(name=team_name)
528
except Person.DoesNotExist:
530
if team is None or not team.is_team():
532
# Control access to private teams
533
if (team.visibility != PERSON_VISIBILITY_PUBLIC and only_public_teams):
535
if account.person.in_team(team):
536
memberships.append(team_name)
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,
546
return render_to_response('untrusted.html', context)