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