Remember last login method
[astakos] / snf-astakos-app / astakos / im / target / shibboleth.py
1 # Copyright 2011-2012 GRNET S.A. All rights reserved.
2 #
3 # Redistribution and use in source and binary forms, with or
4 # without modification, are permitted provided that the following
5 # conditions are met:
6 #
7 #   1. Redistributions of source code must retain the above
8 #      copyright notice, this list of conditions and the following
9 #      disclaimer.
10 #
11 #   2. Redistributions in binary form must reproduce the above
12 #      copyright notice, this list of conditions and the following
13 #      disclaimer in the documentation and/or other materials
14 #      provided with the distribution.
15 #
16 # THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
17 # OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
18 # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
19 # PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
20 # CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21 # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22 # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
23 # USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
24 # AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
25 # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
26 # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
27 # POSSIBILITY OF SUCH DAMAGE.
28 #
29 # The views and conclusions contained in the software and
30 # documentation are those of the authors and should not be
31 # interpreted as representing official policies, either expressed
32 # or implied, of GRNET S.A.
33
34 import json
35
36 from django.http import HttpResponseBadRequest
37 from django.utils.translation import ugettext as _
38 from django.contrib import messages
39 from django.template import RequestContext
40 from django.views.decorators.http import require_http_methods
41 from django.http import HttpResponseRedirect
42 from django.core.urlresolvers import reverse
43 from django.core.exceptions import ImproperlyConfigured
44 from django.shortcuts import get_object_or_404
45
46 from urlparse import urlunsplit, urlsplit
47
48 from astakos.im.util import prepare_response, get_context
49 from astakos.im.views import requires_anonymous, render_response, \
50         requires_auth_provider
51 from astakos.im.settings import ENABLE_LOCAL_ACCOUNT_MIGRATION, BASEURL
52 from astakos.im.models import AstakosUser, PendingThirdPartyUser
53 from astakos.im.forms import LoginForm
54 from astakos.im.activation_backends import get_backend, SimpleBackend
55 from astakos.im import auth_providers
56 from astakos.im import settings
57
58 import astakos.im.messages as astakos_messages
59
60 import logging
61
62 logger = logging.getLogger(__name__)
63
64 class Tokens:
65     # these are mapped by the Shibboleth SP software
66     SHIB_EPPN = "HTTP_EPPN"  # eduPersonPrincipalName
67     SHIB_NAME = "HTTP_SHIB_INETORGPERSON_GIVENNAME"
68     SHIB_SURNAME = "HTTP_SHIB_PERSON_SURNAME"
69     SHIB_CN = "HTTP_SHIB_PERSON_COMMONNAME"
70     SHIB_DISPLAYNAME = "HTTP_SHIB_INETORGPERSON_DISPLAYNAME"
71     SHIB_EP_AFFILIATION = "HTTP_SHIB_EP_AFFILIATION"
72     SHIB_SESSION_ID = "HTTP_SHIB_SESSION_ID"
73     SHIB_MAIL = "HTTP_SHIB_MAIL"
74
75 @requires_auth_provider('shibboleth', login=True)
76 @require_http_methods(["GET", "POST"])
77 def login(
78     request,
79     template='im/third_party_check_local.html',
80     extra_context=None
81 ):
82     extra_context = extra_context or {}
83
84     tokens = request.META
85
86     try:
87         eppn = tokens.get(Tokens.SHIB_EPPN)
88
89         if not eppn:
90             raise KeyError(_(astakos_messages.SHIBBOLETH_MISSING_EPPN))
91         if Tokens.SHIB_DISPLAYNAME in tokens:
92             realname = tokens[Tokens.SHIB_DISPLAYNAME]
93         elif Tokens.SHIB_CN in tokens:
94             realname = tokens[Tokens.SHIB_CN]
95         elif Tokens.SHIB_NAME in tokens and Tokens.SHIB_SURNAME in tokens:
96             realname = tokens[Tokens.SHIB_NAME] + ' ' + tokens[Tokens.SHIB_SURNAME]
97         else:
98             print settings.SHIBBOLETH_REQUIRE_NAME_INFO, "LALALALAL"
99             if settings.SHIBBOLETH_REQUIRE_NAME_INFO:
100                 raise KeyError(_(astakos_messages.SHIBBOLETH_MISSING_NAME))
101             else:
102                 realname = ''
103     except KeyError, e:
104         # invalid shibboleth headers, redirect to login, display message
105         messages.error(request, e.message)
106         return HttpResponseRedirect(reverse('login'))
107
108     affiliation = tokens.get(Tokens.SHIB_EP_AFFILIATION, '')
109     email = tokens.get(Tokens.SHIB_MAIL, '')
110     provider_info = {'eppn': eppn, 'email': email}
111
112     # an existing user accessed the view
113     if request.user.is_authenticated():
114         if request.user.has_auth_provider('shibboleth', identifier=eppn):
115             return HttpResponseRedirect(reverse('edit_profile'))
116
117         # automatically add eppn provider to user
118         user = request.user
119         if not request.user.can_add_auth_provider('shibboleth',
120                                                   identifier=eppn):
121             messages.error(request, 'Account already exists.')
122             return HttpResponseRedirect(reverse('edit_profile'))
123
124         user.add_auth_provider('shibboleth', identifier=eppn,
125                                affiliation=affiliation)
126         messages.success(request, 'Account assigned.')
127         return HttpResponseRedirect(reverse('edit_profile'))
128
129     try:
130         # astakos user exists ?
131         user = AstakosUser.objects.get_auth_provider_user(
132             'shibboleth',
133             identifier=eppn
134         )
135         if user.is_active:
136             # authenticate user
137             response = prepare_response(request,
138                                     user,
139                                     request.GET.get('next'),
140                                     'renew' in request.GET)
141             response.set_cookie('astakos_last_login_method', 'local')
142             return response
143         else:
144             message = user.get_inactive_message()
145             messages.error(request, message)
146             return HttpResponseRedirect(reverse('login'))
147
148     except AstakosUser.DoesNotExist, e:
149         provider = auth_providers.get_provider('shibboleth')
150         if not provider.is_available_for_create():
151             messages.error(request,
152                            _(astakos_messages.AUTH_PROVIDER_NOT_ACTIVE) % provider.get_title_display)
153             return HttpResponseRedirect(reverse('login'))
154
155         # eppn not stored in astakos models, create pending profile
156         user, created = PendingThirdPartyUser.objects.get_or_create(
157             third_party_identifier=eppn,
158             provider='shibboleth'
159         )
160         # update pending user
161         user.realname = realname
162         user.affiliation = affiliation
163         user.email = email
164         user.info = json.dumps(provider_info)
165         user.generate_token()
166         user.save()
167
168         extra_context['provider'] = 'shibboleth'
169         extra_context['provider_title'] = 'Academic credentials'
170         extra_context['token'] = user.token
171         extra_context['signup_url'] = reverse('signup') + \
172                                         "?third_party_token=%s" % user.token
173
174         return render_response(
175             template,
176             context_instance=get_context(request, extra_context)
177         )
178