1 # -*- coding: utf-8 -*-
2 # Copyright (c) 2007, 2008, BenoƮt Chesneau
3 # Copyright (c) 2007 Simon Willison, original work on django-openid
7 # Redistribution and use in source and binary forms, with or without
8 # modification, are permitted provided that the following conditions are
11 # * Redistributions of source code must retain the above copyright
12 # * notice, this list of conditions and the following disclaimer.
13 # * Redistributions in binary form must reproduce the above copyright
14 # * notice, this list of conditions and the following disclaimer in the
15 # * documentation and/or other materials provided with the
16 # * distribution. Neither the name of the <ORGANIZATION> nor the names
17 # * of its contributors may be used to endorse or promote products
18 # * derived from this software without specific prior written
21 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
22 # IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
23 # THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
24 # PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
25 # CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
26 # EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
27 # PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA,
28 # OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
29 # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
30 # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
31 # THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
33 from django.http import HttpResponseRedirect, get_host
34 from django.shortcuts import render_to_response as render
35 from django.template import RequestContext, loader, Context
36 from django.conf import settings
37 from django.contrib.auth.models import User
38 from django.contrib.auth import login, logout
39 from django.contrib.auth.decorators import login_required
40 from django.core.urlresolvers import reverse
41 from django.utils.encoding import smart_unicode
42 from django.utils.html import escape
43 from django.utils.translation import ugettext as _
44 from django.contrib.sites.models import Site
45 from django.utils.http import urlquote_plus
46 from django.core.mail import send_mail
48 from openid.consumer.consumer import Consumer, \
49 SUCCESS, CANCEL, FAILURE, SETUP_NEEDED
50 from openid.consumer.discover import DiscoveryFailure
51 from openid.extensions import sreg
52 # needed for some linux distributions like debian
54 from openid.yadis import xri
62 from django_authopenid.util import OpenID, DjangoOpenIDStore, from_openid_response, clean_next
63 from django_authopenid.models import UserAssociation, UserPasswordQueue
64 from django_authopenid.forms import OpenidSigninForm, OpenidAuthForm, OpenidRegisterForm, \
65 OpenidVerifyForm, RegistrationForm, ChangepwForm, ChangeemailForm, \
66 ChangeopenidForm, DeleteForm, EmailPasswordForm
68 def get_url_host(request):
69 if request.is_secure():
73 host = escape(get_host(request))
74 return '%s://%s' % (protocol, host)
76 def get_full_url(request):
77 return get_url_host(request) + request.get_full_path()
81 def ask_openid(request, openid_url, redirect_to, on_failure=None,
83 """ basic function to ask openid and return response """
84 on_failure = on_failure or signin_failure
87 settings, 'OPENID_TRUST_ROOT', get_url_host(request) + '/'
89 if xri.identifierScheme(openid_url) == 'XRI' and getattr(
90 settings, 'OPENID_DISALLOW_INAMES', False
92 msg = _("i-names are not supported")
93 return on_failure(request, msg)
94 consumer = Consumer(request.session, DjangoOpenIDStore())
96 auth_request = consumer.begin(openid_url)
97 except DiscoveryFailure:
98 msg = _("The OpenID %s was invalid" % openid_url)
99 return on_failure(request, msg)
102 auth_request.addExtension(sreg_request)
103 redirect_url = auth_request.redirectURL(trust_root, redirect_to)
104 return HttpResponseRedirect(redirect_url)
106 def complete(request, on_success=None, on_failure=None, return_to=None):
107 """ complete openid signin """
108 on_success = on_success or default_on_success
109 on_failure = on_failure or default_on_failure
111 consumer = Consumer(request.session, DjangoOpenIDStore())
112 # make sure params are encoded in utf8
113 params = dict((k,smart_unicode(v)) for k, v in request.GET.items())
114 openid_response = consumer.complete(params, return_to)
117 if openid_response.status == SUCCESS:
118 return on_success(request, openid_response.identity_url,
120 elif openid_response.status == CANCEL:
121 return on_failure(request, 'The request was canceled')
122 elif openid_response.status == FAILURE:
123 return on_failure(request, openid_response.message)
124 elif openid_response.status == SETUP_NEEDED:
125 return on_failure(request, 'Setup needed')
127 assert False, "Bad openid status: %s" % openid_response.status
129 def default_on_success(request, identity_url, openid_response):
130 """ default action on openid signin success """
131 request.session['openid'] = from_openid_response(openid_response)
132 return HttpResponseRedirect(clean_next(request.GET.get('next')))
134 def default_on_failure(request, message):
135 """ default failure action on signin """
136 return render('openid_failure.html', {
141 def not_authenticated(func):
142 """ decorator that redirect user to next page if
143 he is already logged."""
144 def decorated(request, *args, **kwargs):
145 if request.user.is_authenticated():
146 next = request.GET.get("next", "/")
147 return HttpResponseRedirect(next)
148 return func(request, *args, **kwargs)
154 signin page. It manage the legacy authentification (user/password)
155 and authentification with openid.
159 template : authopenid/signin.htm
162 on_failure = signin_failure
163 next = clean_next(request.GET.get('next'))
165 form_signin = OpenidSigninForm(initial={'next':next})
166 form_auth = OpenidAuthForm(initial={'next':next})
169 if 'bsignin' in request.POST.keys():
170 form_signin = OpenidSigninForm(request.POST)
171 if form_signin.is_valid():
172 next = clean_next(form_signin.cleaned_data.get('next'))
173 sreg_req = sreg.SRegRequest(optional=['nickname', 'email'])
174 redirect_to = "%s%s?%s" % (
175 get_url_host(request),
176 reverse('user_complete_signin'),
177 urllib.urlencode({'next':next})
180 return ask_openid(request,
181 form_signin.cleaned_data['openid_url'],
183 on_failure=signin_failure,
184 sreg_request=sreg_req)
186 elif 'blogin' in request.POST.keys():
187 # perform normal django authentification
188 form_auth = OpenidAuthForm(request.POST)
189 if form_auth.is_valid():
190 user_ = form_auth.get_user()
191 login(request, user_)
192 next = clean_next(form_auth.cleaned_data.get('next'))
193 return HttpResponseRedirect(next)
196 return render('authopenid/signin.html', {
198 'form2': form_signin,
199 'msg': request.GET.get('msg',''),
200 'sendpw_url': reverse('user_sendpw'),
201 }, context_instance=RequestContext(request))
203 def complete_signin(request):
204 """ in case of complete signin with openid """
205 return complete(request, signin_success, signin_failure,
206 get_url_host(request) + reverse('user_complete_signin'))
209 def signin_success(request, identity_url, openid_response):
211 openid signin success.
213 If the openid is already registered, the user is redirected to
214 url set par next or in settings with OPENID_REDIRECT_NEXT variable.
215 If none of these urls are set user is redirectd to /.
217 if openid isn't registered user is redirected to register page.
220 openid_ = from_openid_response(openid_response)
221 request.session['openid'] = openid_
223 rel = UserAssociation.objects.get(openid_url__exact = str(openid_))
225 # try to register this new user
226 return register(request)
229 user_.backend = "django.contrib.auth.backends.ModelBackend"
230 login(request, user_)
232 next = clean_next(request.GET.get('next'))
233 return HttpResponseRedirect(next)
235 def is_association_exist(openid_url):
236 """ test if an openid is already in database """
239 uassoc = UserAssociation.objects.get(openid_url__exact = openid_url)
245 def register(request):
249 If user is already a member he can associate its openid with
252 A new account could also be created and automaticaly associated
257 template : authopenid/complete.html
261 next = clean_next(request.GET.get('next'))
262 openid_ = request.session.get('openid', None)
264 return HttpResponseRedirect(reverse('user_signin') + next)
266 nickname = openid_.sreg.get('nickname', '')
267 email = openid_.sreg.get('email', '')
269 form1 = OpenidRegisterForm(initial={
271 'username': nickname,
274 form2 = OpenidVerifyForm(initial={
276 'username': nickname,
280 just_completed = False
281 if 'bnewaccount' in request.POST.keys():
282 form1 = OpenidRegisterForm(request.POST)
284 next = clean_next(form1.cleaned_data.get('next'))
286 tmp_pwd = User.objects.make_random_password()
287 user_ = User.objects.create_user(form1.cleaned_data['username'],
288 form1.cleaned_data['email'], tmp_pwd)
290 # make association with openid
291 uassoc = UserAssociation(openid_url=str(openid_),
296 user_.backend = "django.contrib.auth.backends.ModelBackend"
297 login(request, user_)
298 elif 'bverify' in request.POST.keys():
299 form2 = OpenidVerifyForm(request.POST)
302 next = clean_next(form2.cleaned_data.get('next'))
303 user_ = form2.get_user()
305 uassoc = UserAssociation(openid_url=str(openid_),
308 login(request, user_)
310 # redirect, can redirect only if forms are valid.
312 return HttpResponseRedirect(next)
314 return render('authopenid/complete.html', {
317 'nickname': nickname,
319 }, context_instance=RequestContext(request))
321 def signin_failure(request, message):
323 falure with openid signin. Go back to signin page.
325 template : "authopenid/signin.html"
327 next = clean_next(request.GET.get('next'))
328 form_signin = OpenidSigninForm(initial={'next': next})
329 form_auth = OpenidAuthForm(initial={'next': next})
331 return render('authopenid/signin.html', {
334 'form2': form_signin,
335 }, context_instance=RequestContext(request))
340 signup page. Create a legacy account
344 templates: authopenid/signup.html, authopenid/confirm_email.txt
346 action_signin = reverse('user_signin')
347 next = clean_next(request.GET.get('next'))
348 form = RegistrationForm(initial={'next':next})
349 form_signin = OpenidSigninForm(initial={'next':next})
352 form = RegistrationForm(request.POST)
354 next = clean_next(form.cleaned_data.get('next'))
355 user_ = User.objects.create_user( form.cleaned_data['username'],
356 form.cleaned_data['email'], form.cleaned_data['password1'])
358 user_.backend = "django.contrib.auth.backends.ModelBackend"
359 login(request, user_)
362 current_domain = Site.objects.get_current().domain
363 subject = _("Welcome")
364 message_template = loader.get_template(
365 'authopenid/confirm_email.txt'
367 message_context = Context({
368 'site_url': 'http://%s/' % current_domain,
369 'username': form.cleaned_data['username'],
370 'password': form.cleaned_data['password1']
372 message = message_template.render(message_context)
373 send_mail(subject, message, settings.DEFAULT_FROM_EMAIL,
376 return HttpResponseRedirect(next)
378 return render('authopenid/signup.html', {
380 'form2': form_signin,
381 }, context_instance=RequestContext(request))
384 def signout(request):
386 signout from the website. Remove openid from session and kill it.
391 del request.session['openid']
394 next = clean_next(request.GET.get('next'))
397 return HttpResponseRedirect(next)
400 url_host = get_url_host(request)
402 "%s%s" % (url_host, reverse('user_complete_signin'))
404 return render('authopenid/yadis.xrdf', {
405 'return_to': return_to
406 }, context_instance=RequestContext(request))
409 def account_settings(request):
411 index pages to changes some basic account settings :
414 - associate a new openid
419 template : authopenid/settings.html
421 msg = request.GET.get('msg', '')
425 uassoc = UserAssociation.objects.get(
426 user__username__exact=request.user.username
432 return render('authopenid/settings.html', {
434 'is_openid': is_openid
435 }, context_instance=RequestContext(request))
438 def changepw(request):
440 change password view.
443 template: authopenid/changepw.html
449 form = ChangepwForm(request.POST, user=user_)
451 user_.set_password(form.cleaned_data['password1'])
453 msg = _("Password changed.")
454 redirect = "%s?msg=%s" % (
455 reverse('user_account_settings'),
457 return HttpResponseRedirect(redirect)
459 form = ChangepwForm(user=user_)
461 return render('authopenid/changepw.html', {'form': form },
462 context_instance=RequestContext(request))
465 def changeemail(request):
467 changeemail view. It require password or openid to allow change.
471 template : authopenid/changeemail.html
473 msg = request.GET.get('msg', '')
477 redirect_to = get_url_host(request) + reverse('user_changeemail')
480 form = ChangeemailForm(request.POST, user=user_)
482 if not form.test_openid:
483 user_.email = form.cleaned_data['email']
485 msg = _("Email changed.")
486 redirect = "%s?msg=%s" % (reverse('user_account_settings'),
488 return HttpResponseRedirect(redirect)
490 request.session['new_email'] = form.cleaned_data['email']
491 return ask_openid(request, form.cleaned_data['password'],
492 redirect_to, on_failure=emailopenid_failure)
493 elif not request.POST and 'openid.mode' in request.GET:
494 return complete(request, emailopenid_success,
495 emailopenid_failure, redirect_to)
497 form = ChangeemailForm(initial={'email': user_.email},
500 return render('authopenid/changeemail.html', {
503 }, context_instance=RequestContext(request))
506 def emailopenid_success(request, identity_url, openid_response):
507 openid_ = from_openid_response(openid_response)
511 uassoc = UserAssociation.objects.get(
512 openid_url__exact=identity_url
515 return emailopenid_failure(request,
516 _("No OpenID %s found associated in our database" % identity_url))
518 if uassoc.user.username != request.user.username:
519 return emailopenid_failure(request,
520 _("The OpenID %s isn't associated to current user logged in" %
523 new_email = request.session.get('new_email', '')
525 user_.email = new_email
527 del request.session['new_email']
528 msg = _("Email Changed.")
530 redirect = "%s?msg=%s" % (reverse('user_account_settings'),
532 return HttpResponseRedirect(redirect)
535 def emailopenid_failure(request, message):
536 redirect_to = "%s?msg=%s" % (
537 reverse('user_changeemail'), urlquote_plus(message))
538 return HttpResponseRedirect(redirect_to)
541 def changeopenid(request):
543 change openid view. Allow user to change openid
544 associated to its username.
548 template: authopenid/changeopenid.html
554 msg = request.GET.get('msg', '')
559 uopenid = UserAssociation.objects.get(user=user_)
560 openid_url = uopenid.openid_url
564 redirect_to = get_url_host(request) + reverse('user_changeopenid')
565 if request.POST and has_openid:
566 form = ChangeopenidForm(request.POST, user=user_)
568 return ask_openid(request, form.cleaned_data['openid_url'],
569 redirect_to, on_failure=changeopenid_failure)
570 elif not request.POST and has_openid:
571 if 'openid.mode' in request.GET:
572 return complete(request, changeopenid_success,
573 changeopenid_failure, redirect_to)
575 form = ChangeopenidForm(initial={'openid_url': openid_url }, user=user_)
576 return render('authopenid/changeopenid.html', {
578 'has_openid': has_openid,
580 }, context_instance=RequestContext(request))
582 def changeopenid_success(request, identity_url, openid_response):
583 openid_ = from_openid_response(openid_response)
586 uassoc = UserAssociation.objects.get(openid_url__exact=identity_url)
592 uassoc = UserAssociation.objects.get(
593 user__username__exact=request.user.username
595 uassoc.openid_url = identity_url
598 uassoc = UserAssociation(user=request.user,
599 openid_url=identity_url)
601 elif uassoc.user.username != request.user.username:
602 return changeopenid_failure(request,
603 _('This OpenID is already associated with another account.'))
605 request.session['openids'] = []
606 request.session['openids'].append(openid_)
608 msg = _("OpenID %s is now associated with your account." % identity_url)
609 redirect = "%s?msg=%s" % (
610 reverse('user_account_settings'),
612 return HttpResponseRedirect(redirect)
615 def changeopenid_failure(request, message):
616 redirect_to = "%s?msg=%s" % (
617 reverse('user_changeopenid'),
618 urlquote_plus(message))
619 return HttpResponseRedirect(redirect_to)
624 delete view. Allow user to delete its account. Password/openid are required to
625 confirm it. He should also check the confirm checkbox.
629 template : authopenid/delete.html
636 redirect_to = get_url_host(request) + reverse('user_delete')
638 form = DeleteForm(request.POST, user=user_)
640 if not form.test_openid:
642 return signout(request)
644 return ask_openid(request, form.cleaned_data['password'],
645 redirect_to, on_failure=deleteopenid_failure)
646 elif not request.POST and 'openid.mode' in request.GET:
647 return complete(request, deleteopenid_success, deleteopenid_failure,
650 form = DeleteForm(user=user_)
652 msg = request.GET.get('msg','')
653 return render('authopenid/delete.html', {
656 }, context_instance=RequestContext(request))
658 def deleteopenid_success(request, identity_url, openid_response):
659 openid_ = from_openid_response(openid_response)
663 uassoc = UserAssociation.objects.get(
664 openid_url__exact=identity_url
667 return deleteopenid_failure(request,
668 _("No OpenID %s found associated in our database" % identity_url))
670 if uassoc.user.username == user_.username:
672 return signout(request)
674 return deleteopenid_failure(request,
675 _("The OpenID %s isn't associated to current user logged in" %
678 msg = _("Account deleted.")
679 redirect = "/?msg=%s" % (urlquote_plus(msg))
680 return HttpResponseRedirect(redirect)
683 def deleteopenid_failure(request, message):
684 redirect_to = "%s?msg=%s" % (reverse('user_delete'), urlquote_plus(message))
685 return HttpResponseRedirect(redirect_to)
690 send a new password to the user. It return a mail with
691 a new pasword and a confirm link in. To activate the
692 new password, the user should click on confirm link.
696 templates : authopenid/sendpw_email.txt, authopenid/sendpw.html
699 msg = request.GET.get('msg','')
701 form = EmailPasswordForm(request.POST)
703 new_pw = User.objects.make_random_password()
704 confirm_key = UserPasswordQueue.objects.get_new_confirm_key()
706 uqueue = UserPasswordQueue.objects.get(
710 uqueue = UserPasswordQueue(
713 uqueue.new_password = new_pw
714 uqueue.confirm_key = confirm_key
717 current_domain = Site.objects.get_current().domain
718 subject = _("Request for new password")
719 message_template = loader.get_template(
720 'authopenid/sendpw_email.txt')
721 message_context = Context({
722 'site_url': 'http://%s' % current_domain,
723 'confirm_key': confirm_key,
724 'username': form.user_cache.username,
726 'url_confirm': reverse('user_confirmchangepw'),
728 message = message_template.render(message_context)
729 send_mail(subject, message, settings.DEFAULT_FROM_EMAIL,
730 [form.user_cache.email])
731 msg = _("A new password has been sent to your email address.")
733 form = EmailPasswordForm()
735 return render('authopenid/sendpw.html', {
738 }, context_instance=RequestContext(request))
741 def confirmchangepw(request):
743 view to set new password when the user click on confirm link
744 in its mail. Basically it check if the confirm key exist, then
745 replace old password with new password and remove confirm
746 ley from the queue. Then it redirect the user to signin
749 url : /sendpw/confirm/?key
752 confirm_key = request.GET.get('key', '')
754 return HttpResponseRedirect('/')
757 uqueue = UserPasswordQueue.objects.get(
758 confirm_key__exact=confirm_key
761 msg = _("Could not change password. Confirmation key '%s'\
762 is not registered." % confirm_key)
763 redirect = "%s?msg=%s" % (
764 reverse('user_sendpw'), urlquote_plus(msg))
765 return HttpResponseRedirect(redirect)
768 user_ = User.objects.get(id=uqueue.user.id)
770 msg = _("Can not change password. User don't exist anymore \
772 redirect = "%s?msg=%s" % (reverse('user_sendpw'),
774 return HttpResponseRedirect(redirect)
776 user_.set_password(uqueue.new_password)
779 msg = _("Password changed for %s. You may now sign in." %
781 redirect = "%s?msg=%s" % (reverse('user_signin'),
784 return HttpResponseRedirect(redirect)