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