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