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