Add OpenIDP Provider
[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                     self.trans.store(data)
95                     redirect = '%s/login?%s' % (self.basepath,
96                                                 self.trans.get_GET_arg())
97                     self.debug('Redirecting: %s' % redirect)
98                     raise cherrypy.HTTPRedirect(redirect)
99                 else:
100                     raise AuthenticationError("unknown user", 401)
101
102         elif kwargs.get('openid.mode', None) == 'checkid_immediate':
103             # This is immediate, so we need to assert or fail
104             if user.is_anonymous:
105                 return self._respond(request.answer(False))
106
107             immediate = True
108
109         else:
110             return self._respond(self.cfg.server.handleRequest(request))
111
112         # check if this is discovery or ned identity matching checks
113         if not request.idSelect():
114             idurl = self.cfg.identity_url_template % {'username': user.name}
115             if request.identity != idurl:
116                 raise AuthenticationError("User ID mismatch!", 401)
117
118         # check if the ralying party is trusted
119         if request.trust_root in self.cfg.untrusted_roots:
120             raise AuthenticationError("Untrusted Relying party", 401)
121
122         # if the party is explicitly whitelisted just respond
123         if request.trust_root in self.cfg.trusted_roots:
124             return self._respond(self._response(request, us))
125
126         allowroot = 'allow-%s' % request.trust_root
127
128         try:
129             userdata = user.load_plugin_data(self.cfg.name)
130             expiry = int(userdata[allowroot])
131         except Exception, e:  # pylint: disable=broad-except
132             self.debug(e)
133             expiry = 0
134         if expiry > int(time.time()):
135             self.debug("User has unexpired previous authorization")
136             return self._respond(self._response(request, us))
137
138         if immediate:
139             raise AuthenticationError("No consent for immediate", 401)
140
141         if self.stage == 'consent':
142             if form is None:
143                 raise AuthenticationError("Unintelligible consent", 401)
144             allow = form.get('decided_allow', False)
145             if not allow:
146                 raise AuthenticationError("User declined", 401)
147             try:
148                 days = int(form.get('remember_for_days', '0'))
149                 if days < 0 or days > 7:
150                     raise
151                 userdata = {allowroot: str(int(time.time()) + (days*86400))}
152                 user.save_plugin_data(self.cfg.name, userdata)
153             except Exception, e:  # pylint: disable=broad-except
154                 self.debug(e)
155                 days = 0
156
157             # all done we consent!
158             return self._respond(self._response(request, us))
159
160         else:
161             data = {'openid_stage': 'consent',
162                     'openid_request': json.dumps(kwargs)}
163             self.trans.store(data)
164
165             # Add extension data to this list of dictionaries
166             ad = [
167                 {
168                     "Trust Root": request.trust_root,
169                 },
170             ]
171             userattrs = us.get_user_attrs()
172             for n, e in self.cfg.extensions.items():
173                 data = e.get_display_data(request, userattrs)
174                 self.debug('%s returned %s' % (n, repr(data)))
175                 ad.append(data)
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)