3 # Copyright (C) 2014 Ipsilon project Contributors, for licensee see COPYING
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
12 from openid.server.server import ProtocolError, EncodingError
19 class AuthenticateRequest(ProviderPageBase):
21 def __init__(self, *args, **kwargs):
22 super(AuthenticateRequest, self).__init__(*args, **kwargs)
26 def _preop(self, *args, **kwargs):
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')
37 def pre_GET(self, *args, **kwargs):
38 self._preop(*args, **kwargs)
40 def pre_POST(self, *args, **kwargs):
41 self._preop(*args, **kwargs)
43 def _get_form(self, *args):
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)
52 def auth(self, *args, **kwargs):
54 form = self._get_form(args)
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:
62 raise cherrypy.HTTPError(e.code, e.msg)
63 return self._respond(request.answer(False))
65 def _parse_request(self, **kwargs):
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)
74 self.debug('No request')
75 raise cherrypy.HTTPRedirect(self.basepath)
79 def _openid_checks(self, request, form, **kwargs):
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':
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)
100 raise AuthenticationError("unknown user", 401)
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))
110 return self._respond(self.cfg.server.handleRequest(request))
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)
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)
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))
126 allowroot = 'allow-%s' % request.trust_root
129 userdata = user.load_plugin_data(self.cfg.name)
130 expiry = int(userdata[allowroot])
131 except Exception, e: # pylint: disable=broad-except
134 if expiry > int(time.time()):
135 self.debug("User has unexpired previous authorization")
136 return self._respond(self._response(request, us))
139 raise AuthenticationError("No consent for immediate", 401)
141 if self.stage == 'consent':
143 raise AuthenticationError("Unintelligible consent", 401)
144 allow = form.get('decided_allow', False)
146 raise AuthenticationError("User declined", 401)
148 days = int(form.get('remember_for_days', '0'))
149 if days < 0 or days > 7:
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
157 # all done we consent!
158 return self._respond(self._response(request, us))
161 data = {'openid_stage': 'consent',
162 'openid_request': json.dumps(kwargs)}
163 self.trans.store(data)
165 # Add extension data to this list of dictionaries
168 "Trust Root": request.trust_root,
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)))
179 "action": '%s/openid/Consent' % (self.basepath),
180 "trustroot": request.trust_root,
181 "username": user.name,
184 context.update(dict((self.trans.get_POST_tuple(),)))
185 # pylint: disable=star-args
186 return self._template('openid/consent_form.html', **context)
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(
193 identity=identity_url,
194 claimed_id=identity_url
196 userattrs = session.get_user_attrs()
197 for _, e in self.cfg.extensions.items():
198 resp = e.get_response(request, userattrs)
200 response.addExtension(resp)
203 def _respond(self, response):
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'
215 cherrypy.response.status = 400
216 return encoding_error.response.encodeToKVForm()
219 class Continue(AuthenticateRequest):
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)
228 kwargs = json.loads(openid_request)
229 return self.auth(**kwargs)
232 class Consent(AuthenticateRequest):
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)
241 args = ({'form': kwargs},)
242 kwargs = json.loads(openid_request)
243 return self.auth(*args, **kwargs)
246 class OpenID(AuthenticateRequest):
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)
257 def GET(self, *args, **kwargs):
258 return self.auth(**kwargs)
260 def POST(self, *args, **kwargs):
261 return self.auth(**kwargs)