Show login target on the login screen
[cascardo/ipsilon.git] / ipsilon / providers / openid / auth.py
1 #!/usr/bin/python
2 #
3 # Copyright (C) 2014  Ipsilon project Contributors, for licensee see COPYING
4
5 from ipsilon.providers.common import ProviderPageBase
6 from ipsilon.providers.common import AuthenticationError, InvalidRequest
7 from ipsilon.providers.openid.meta import XRDSHandler, UserXRDSHandler
8 from ipsilon.providers.openid.meta import IDHandler
9 from ipsilon.util.trans import Transaction
10 from ipsilon.util.user import UserSession
11
12 from openid.server.server import ProtocolError, EncodingError
13
14 import cherrypy
15 import time
16 import json
17
18
19 class AuthenticateRequest(ProviderPageBase):
20
21     def __init__(self, *args, **kwargs):
22         super(AuthenticateRequest, self).__init__(*args, **kwargs)
23         self.stage = 'init'
24         self.trans = None
25
26     def _preop(self, *args, **kwargs):
27         try:
28             # generate a new id or get current one
29             self.trans = Transaction('openid', **kwargs)
30             if self.trans.cookie.value != self.trans.provider:
31                 self.debug('Invalid transaction, %s != %s' % (
32                            self.trans.cookie.value, self.trans.provider))
33         except Exception, e:  # pylint: disable=broad-except
34             self.debug('Transaction initialization failed: %s' % repr(e))
35             raise cherrypy.HTTPError(400, 'Invalid transaction id')
36
37     def pre_GET(self, *args, **kwargs):
38         self._preop(*args, **kwargs)
39
40     def pre_POST(self, *args, **kwargs):
41         self._preop(*args, **kwargs)
42
43     def _get_form(self, *args):
44         form = None
45         if args is not None:
46             first = args[0] if len(args) > 0 else None
47             second = first[0] if len(first) > 0 else None
48             if type(second) is dict:
49                 form = second.get('form', None)
50         return form
51
52     def auth(self, *args, **kwargs):
53         request = None
54         form = self._get_form(args)
55         try:
56             request = self._parse_request(**kwargs)
57             return self._openid_checks(request, form, **kwargs)
58         except InvalidRequest, e:
59             raise cherrypy.HTTPError(e.code, e.msg)
60         except AuthenticationError, e:
61             if request is None:
62                 raise cherrypy.HTTPError(e.code, e.msg)
63             return self._respond(request.answer(False))
64
65     def _parse_request(self, **kwargs):
66         request = None
67         try:
68             request = self.cfg.server.decodeRequest(kwargs)
69         except ProtocolError, openid_error:
70             self.debug('ProtocolError: %s' % openid_error)
71             raise InvalidRequest('Invalid OpenID request', 400)
72
73         if request is None:
74             self.debug('No request')
75             raise cherrypy.HTTPRedirect(self.basepath)
76
77         return request
78
79     def _openid_checks(self, request, form, **kwargs):
80         us = UserSession()
81         user = us.get_user()
82         immediate = False
83
84         self.debug('Mode: %s Stage: %s User: %s' % (
85             kwargs['openid.mode'], self.stage, user.name))
86         if kwargs.get('openid.mode', None) == 'checkid_setup':
87             if user.is_anonymous:
88                 if self.stage == 'init':
89                     returl = '%s/openid/Continue?%s' % (
90                         self.basepath, self.trans.get_GET_arg())
91                     data = {'openid_stage': 'auth',
92                             'openid_request': json.dumps(kwargs),
93                             'login_return': returl,
94                             'login_target': request.trust_root}
95                     self.trans.store(data)
96                     redirect = '%s/login?%s' % (self.basepath,
97                                                 self.trans.get_GET_arg())
98                     self.debug('Redirecting: %s' % redirect)
99                     raise cherrypy.HTTPRedirect(redirect)
100                 else:
101                     raise AuthenticationError("unknown user", 401)
102
103         elif kwargs.get('openid.mode', None) == 'checkid_immediate':
104             # This is immediate, so we need to assert or fail
105             if user.is_anonymous:
106                 return self._respond(request.answer(False))
107
108             immediate = True
109
110         else:
111             return self._respond(self.cfg.server.handleRequest(request))
112
113         # check if this is discovery or ned identity matching checks
114         if not request.idSelect():
115             idurl = self.cfg.identity_url_template % {'username': user.name}
116             if request.identity != idurl:
117                 raise AuthenticationError("User ID mismatch!", 401)
118
119         # check if the ralying party is trusted
120         if request.trust_root in self.cfg.untrusted_roots:
121             raise AuthenticationError("Untrusted Relying party", 401)
122
123         # if the party is explicitly whitelisted just respond
124         if request.trust_root in self.cfg.trusted_roots:
125             return self._respond(self._response(request, us))
126
127         allowroot = 'allow-%s' % request.trust_root
128
129         try:
130             userdata = user.load_plugin_data(self.cfg.name)
131             expiry = int(userdata[allowroot])
132         except Exception, e:  # pylint: disable=broad-except
133             self.debug(e)
134             expiry = 0
135         if expiry > int(time.time()):
136             self.debug("User has unexpired previous authorization")
137             return self._respond(self._response(request, us))
138
139         if immediate:
140             raise AuthenticationError("No consent for immediate", 401)
141
142         if self.stage == 'consent':
143             if form is None:
144                 raise AuthenticationError("Unintelligible consent", 401)
145             allow = form.get('decided_allow', False)
146             if not allow:
147                 raise AuthenticationError("User declined", 401)
148             try:
149                 days = int(form.get('remember_for_days', '0'))
150                 if days < 0 or days > 7:
151                     raise
152                 userdata = {allowroot: str(int(time.time()) + (days*86400))}
153                 user.save_plugin_data(self.cfg.name, userdata)
154             except Exception, e:  # pylint: disable=broad-except
155                 self.debug(e)
156                 days = 0
157
158             # all done we consent!
159             return self._respond(self._response(request, us))
160
161         else:
162             data = {'openid_stage': 'consent',
163                     'openid_request': json.dumps(kwargs)}
164             self.trans.store(data)
165
166             # Add extension data to this dictionary
167             ad = {
168                 "Trust Root": request.trust_root,
169             }
170             userattrs = us.get_user_attrs()
171             for n, e in self.cfg.extensions.items():
172                 data = e.get_display_data(request, userattrs)
173                 self.debug('%s returned %s' % (n, repr(data)))
174                 for key, value in data.items():
175                     ad[self.cfg.mapping.display_name(key)] = value
176
177             context = {
178                 "title": 'Consent',
179                 "action": '%s/openid/Consent' % (self.basepath),
180                 "trustroot": request.trust_root,
181                 "username": user.name,
182                 "authz_details": ad,
183             }
184             context.update(dict((self.trans.get_POST_tuple(),)))
185             # pylint: disable=star-args
186             return self._template('openid/consent_form.html', **context)
187
188     def _response(self, request, session):
189         user = session.get_user()
190         identity_url = self.cfg.identity_url_template % {'username': user.name}
191         response = request.answer(
192             True,
193             identity=identity_url,
194             claimed_id=identity_url
195         )
196         userattrs = session.get_user_attrs()
197         for _, e in self.cfg.extensions.items():
198             resp = e.get_response(request, userattrs)
199             if resp is not None:
200                 response.addExtension(resp)
201         return response
202
203     def _respond(self, response):
204         try:
205             self.debug('Response: %s' % response)
206             webresponse = self.cfg.server.encodeResponse(response)
207             cherrypy.response.headers.update(webresponse.headers)
208             cherrypy.response.status = webresponse.code
209             return webresponse.body
210         except EncodingError, encoding_error:
211             self.debug('Unable to respond because: %s' % encoding_error)
212             cherrypy.response.headers = {
213                 'Content-Type': 'text/plain; charset=UTF-8'
214             }
215             cherrypy.response.status = 400
216             return encoding_error.response.encodeToKVForm()
217
218
219 class Continue(AuthenticateRequest):
220
221     def GET(self, *args, **kwargs):
222         transdata = self.trans.retrieve()
223         self.stage = transdata.get('openid_stage', None)
224         openid_request = transdata.get('openid_request', None)
225         if self.stage is None or openid_request is None:
226             raise AuthenticationError("unknown state", 400)
227
228         kwargs = json.loads(openid_request)
229         return self.auth(**kwargs)
230
231
232 class Consent(AuthenticateRequest):
233
234     def POST(self, *args, **kwargs):
235         transdata = self.trans.retrieve()
236         self.stage = transdata.get('openid_stage', None)
237         openid_request = transdata.get('openid_request', None)
238         if self.stage is None or openid_request is None:
239             raise AuthenticationError("unknown state", 400)
240
241         args = ({'form': kwargs},)
242         kwargs = json.loads(openid_request)
243         return self.auth(*args, **kwargs)
244
245
246 class OpenID(AuthenticateRequest):
247
248     def __init__(self, *args, **kwargs):
249         super(OpenID, self).__init__(*args, **kwargs)
250         self.XRDS = XRDSHandler(*args, **kwargs)
251         self.yadis = UserXRDSHandler(*args, **kwargs)
252         self.id = IDHandler(*args, **kwargs)
253         self.Continue = Continue(*args, **kwargs)
254         self.Consent = Consent(*args, **kwargs)
255         self.trans = None
256
257     def GET(self, *args, **kwargs):
258         return self.auth(**kwargs)
259
260     def POST(self, *args, **kwargs):
261         return self.auth(**kwargs)