Add attribute mapping for user information
[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 dictionary
166             ad = {
167                 "Trust Root": request.trust_root,
168             }
169             userattrs = us.get_user_attrs()
170             for n, e in self.cfg.extensions.items():
171                 data = e.get_display_data(request, userattrs)
172                 self.debug('%s returned %s' % (n, repr(data)))
173                 for key, value in data.items():
174                     ad[self.cfg.mapping.display_name(key)] = value
175
176             context = {
177                 "title": 'Consent',
178                 "action": '%s/openid/Consent' % (self.basepath),
179                 "trustroot": request.trust_root,
180                 "username": user.name,
181                 "authz_details": ad,
182             }
183             context.update(dict((self.trans.get_POST_tuple(),)))
184             # pylint: disable=star-args
185             return self._template('openid/consent_form.html', **context)
186
187     def _response(self, request, session):
188         user = session.get_user()
189         identity_url = self.cfg.identity_url_template % {'username': user.name}
190         response = request.answer(
191             True,
192             identity=identity_url,
193             claimed_id=identity_url
194         )
195         userattrs = session.get_user_attrs()
196         for _, e in self.cfg.extensions.items():
197             resp = e.get_response(request, userattrs)
198             if resp is not None:
199                 response.addExtension(resp)
200         return response
201
202     def _respond(self, response):
203         try:
204             self.debug('Response: %s' % response)
205             webresponse = self.cfg.server.encodeResponse(response)
206             cherrypy.response.headers.update(webresponse.headers)
207             cherrypy.response.status = webresponse.code
208             return webresponse.body
209         except EncodingError, encoding_error:
210             self.debug('Unable to respond because: %s' % encoding_error)
211             cherrypy.response.headers = {
212                 'Content-Type': 'text/plain; charset=UTF-8'
213             }
214             cherrypy.response.status = 400
215             return encoding_error.response.encodeToKVForm()
216
217
218 class Continue(AuthenticateRequest):
219
220     def GET(self, *args, **kwargs):
221         transdata = self.trans.retrieve()
222         self.stage = transdata.get('openid_stage', None)
223         openid_request = transdata.get('openid_request', None)
224         if self.stage is None or openid_request is None:
225             raise AuthenticationError("unknown state", 400)
226
227         kwargs = json.loads(openid_request)
228         return self.auth(**kwargs)
229
230
231 class Consent(AuthenticateRequest):
232
233     def POST(self, *args, **kwargs):
234         transdata = self.trans.retrieve()
235         self.stage = transdata.get('openid_stage', None)
236         openid_request = transdata.get('openid_request', None)
237         if self.stage is None or openid_request is None:
238             raise AuthenticationError("unknown state", 400)
239
240         args = ({'form': kwargs},)
241         kwargs = json.loads(openid_request)
242         return self.auth(*args, **kwargs)
243
244
245 class OpenID(AuthenticateRequest):
246
247     def __init__(self, *args, **kwargs):
248         super(OpenID, self).__init__(*args, **kwargs)
249         self.XRDS = XRDSHandler(*args, **kwargs)
250         self.yadis = UserXRDSHandler(*args, **kwargs)
251         self.id = IDHandler(*args, **kwargs)
252         self.Continue = Continue(*args, **kwargs)
253         self.Consent = Consent(*args, **kwargs)
254         self.trans = None
255
256     def GET(self, *args, **kwargs):
257         return self.auth(**kwargs)
258
259     def POST(self, *args, **kwargs):
260         return self.auth(**kwargs)