1 # Copyright (C) 2014 Ipsilon project Contributors, for licensee see COPYING
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
10 from openid.server.server import ProtocolError, EncodingError
17 class AuthenticateRequest(ProviderPageBase):
19 def __init__(self, *args, **kwargs):
20 super(AuthenticateRequest, self).__init__(*args, **kwargs)
24 def _preop(self, *args, **kwargs):
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')
35 def pre_GET(self, *args, **kwargs):
36 self._preop(*args, **kwargs)
38 def pre_POST(self, *args, **kwargs):
39 self._preop(*args, **kwargs)
41 def _get_form(self, *args):
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)
50 def auth(self, *args, **kwargs):
52 form = self._get_form(args)
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:
60 raise cherrypy.HTTPError(e.code, e.msg)
61 return self._respond(request.answer(False))
63 def _parse_request(self, **kwargs):
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)
72 self.debug('No request')
73 raise cherrypy.HTTPRedirect(self.basepath)
77 def _openid_checks(self, request, form, **kwargs):
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':
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)
99 raise AuthenticationError("unknown user", 401)
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))
109 return self._respond(self.cfg.server.handleRequest(request))
111 # check if this is discovery or ned 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)
117 # check if the ralying party is trusted
118 if request.trust_root in self.cfg.untrusted_roots:
119 raise AuthenticationError("Untrusted Relying party", 401)
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))
125 allowroot = 'allow-%s' % request.trust_root
128 userdata = user.load_plugin_data(self.cfg.name)
129 expiry = int(userdata[allowroot])
130 except Exception, e: # pylint: disable=broad-except
133 if expiry > int(time.time()):
134 self.debug("User has unexpired previous authorization")
135 return self._respond(self._response(request, us))
138 raise AuthenticationError("No consent for immediate", 401)
140 if self.stage == 'consent':
142 raise AuthenticationError("Unintelligible consent", 401)
143 allow = form.get('decided_allow', False)
145 raise AuthenticationError("User declined", 401)
147 days = int(form.get('remember_for_days', '0'))
148 if days < 0 or days > 7:
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
156 # all done we consent!
157 return self._respond(self._response(request, us))
160 data = {'openid_stage': 'consent',
161 'openid_request': json.dumps(kwargs)}
162 self.trans.store(data)
164 # Add extension data to this dictionary
166 "Trust Root": request.trust_root,
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
177 "action": '%s/openid/Consent' % (self.basepath),
178 "trustroot": request.trust_root,
179 "username": user.name,
182 context.update(dict((self.trans.get_POST_tuple(),)))
183 # pylint: disable=star-args
184 return self._template('openid/consent_form.html', **context)
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(
191 identity=identity_url,
192 claimed_id=identity_url
194 userattrs = session.get_user_attrs()
195 for _, e in self.cfg.extensions.available().items():
196 resp = e.get_response(request, userattrs)
198 response.addExtension(resp)
201 def _respond(self, response):
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'
213 cherrypy.response.status = 400
214 return encoding_error.response.encodeToKVForm()
217 class Continue(AuthenticateRequest):
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)
226 kwargs = json.loads(openid_request)
227 return self.auth(**kwargs)
230 class Consent(AuthenticateRequest):
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)
239 args = ({'form': kwargs},)
240 kwargs = json.loads(openid_request)
241 return self.auth(*args, **kwargs)
244 class OpenID(AuthenticateRequest):
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)
255 def GET(self, *args, **kwargs):
256 return self.auth(**kwargs)
258 def POST(self, *args, **kwargs):
259 return self.auth(**kwargs)