1 # Copyright (C) 2014 Ipsilon project Contributors, for license 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.policy import Policy
8 from ipsilon.util.trans import Transaction
9 from ipsilon.util.user import UserSession
11 from openid.server.server import ProtocolError, EncodingError
18 class AuthenticateRequest(ProviderPageBase):
20 def __init__(self, *args, **kwargs):
21 super(AuthenticateRequest, self).__init__(*args, **kwargs)
25 def _preop(self, *args, **kwargs):
27 # generate a new id or get current one
28 self.trans = Transaction('openid', **kwargs)
29 if (self.trans.cookie and
30 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 isinstance(second, 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 # get attributes, and apply policy mapping and filtering
66 def _source_attributes(self, session):
67 policy = Policy(self.cfg.default_attribute_mapping,
68 self.cfg.default_allowed_attributes)
69 userattrs = session.get_user_attrs()
70 mappedattrs, _ = policy.map_attributes(userattrs)
71 attributes = policy.filter_attributes(mappedattrs)
72 self.debug('Filterd attributes: %s' % repr(attributes))
75 def _parse_request(self, **kwargs):
78 request = self.cfg.server.decodeRequest(kwargs)
79 except ProtocolError, openid_error:
80 self.debug('ProtocolError: %s' % openid_error)
81 raise InvalidRequest('Invalid OpenID request', 400)
84 self.debug('No request')
85 raise cherrypy.HTTPRedirect(self.basepath)
89 def _openid_checks(self, request, form, **kwargs):
94 self.debug('Mode: %s Stage: %s User: %s' % (
95 kwargs['openid.mode'], self.stage, user.name))
96 if kwargs.get('openid.mode', None) == 'checkid_setup':
98 if self.stage == 'init':
99 returl = '%s/openid/Continue?%s' % (
100 self.basepath, self.trans.get_GET_arg())
101 data = {'openid_stage': 'auth',
102 'openid_request': json.dumps(kwargs),
103 'login_return': returl,
104 'login_target': request.trust_root}
105 self.trans.store(data)
106 redirect = '%s/login?%s' % (self.basepath,
107 self.trans.get_GET_arg())
108 self.debug('Redirecting: %s' % redirect)
109 raise cherrypy.HTTPRedirect(redirect)
111 raise AuthenticationError("unknown user", 401)
113 elif kwargs.get('openid.mode', None) == 'checkid_immediate':
114 # This is immediate, so we need to assert or fail
115 if user.is_anonymous:
116 return self._respond(request.answer(False))
121 return self._respond(self.cfg.server.handleRequest(request))
123 # check if this is discovery or needs identity matching checks
124 if not request.idSelect():
125 idurl = self.cfg.identity_url_template % {'username': user.name}
126 if request.identity != idurl:
127 raise AuthenticationError("User ID mismatch!", 401)
129 # check if the relying party is trusted
130 if request.trust_root in self.cfg.untrusted_roots:
131 raise AuthenticationError("Untrusted Relying party", 401)
133 # if the party is explicitly whitelisted just respond
134 if request.trust_root in self.cfg.trusted_roots:
135 return self._respond(self._response(request, us))
137 allowroot = 'allow-%s' % request.trust_root
140 userdata = user.load_plugin_data(self.cfg.name)
141 expiry = int(userdata[allowroot])
142 except Exception, e: # pylint: disable=broad-except
145 if expiry > int(time.time()):
146 self.debug("User has unexpired previous authorization")
147 return self._respond(self._response(request, us))
150 raise AuthenticationError("No consent for immediate", 401)
152 if self.stage == 'consent':
154 raise AuthenticationError("Unintelligible consent", 401)
155 allow = form.get('decided_allow', False)
157 raise AuthenticationError("User declined", 401)
159 days = int(form.get('remember_for_days', '0'))
160 if days < 0 or days > 7:
162 userdata = {allowroot: str(int(time.time()) + (days*86400))}
163 user.save_plugin_data(self.cfg.name, userdata)
164 except Exception, e: # pylint: disable=broad-except
168 # all done we consent!
169 return self._respond(self._response(request, us))
172 data = {'openid_stage': 'consent',
173 'openid_request': json.dumps(kwargs)}
174 self.trans.store(data)
176 # Add extension data to this dictionary
178 "Trust Root": request.trust_root,
180 userattrs = self._source_attributes(us)
181 for n, e in self.cfg.extensions.available().items():
182 data = e.get_display_data(request, userattrs)
183 self.debug('%s returned %s' % (n, repr(data)))
184 for key, value in data.items():
185 ad[self.cfg.mapping.display_name(key)] = value
189 "action": '%s/openid/Consent' % (self.basepath),
190 "trustroot": request.trust_root,
191 "username": user.name,
194 context.update(dict((self.trans.get_POST_tuple(),)))
195 return self._template('openid/consent_form.html', **context)
197 def _response(self, request, session):
198 user = session.get_user()
199 identity_url = self.cfg.identity_url_template % {'username': user.name}
200 response = request.answer(
202 identity=identity_url,
203 claimed_id=identity_url
205 userattrs = self._source_attributes(session)
206 for _, e in self.cfg.extensions.available().items():
207 resp = e.get_response(request, userattrs)
209 response.addExtension(resp)
212 def _respond(self, response):
214 self.debug('Response: %s' % response)
215 webresponse = self.cfg.server.encodeResponse(response)
216 cherrypy.response.headers.update(webresponse.headers)
217 cherrypy.response.status = webresponse.code
218 return webresponse.body
219 except EncodingError, encoding_error:
220 self.debug('Unable to respond because: %s' % encoding_error)
221 cherrypy.response.headers = {
222 'Content-Type': 'text/plain; charset=UTF-8'
224 cherrypy.response.status = 400
225 return encoding_error.response.encodeToKVForm()
228 class Continue(AuthenticateRequest):
230 def GET(self, *args, **kwargs):
231 transdata = self.trans.retrieve()
232 self.stage = transdata.get('openid_stage', None)
233 openid_request = transdata.get('openid_request', None)
234 if self.stage is None or openid_request is None:
235 raise AuthenticationError("unknown state", 400)
237 kwargs = json.loads(openid_request)
238 return self.auth(**kwargs)
241 class Consent(AuthenticateRequest):
243 def POST(self, *args, **kwargs):
244 transdata = self.trans.retrieve()
245 self.stage = transdata.get('openid_stage', None)
246 openid_request = transdata.get('openid_request', None)
247 if self.stage is None or openid_request is None:
248 raise AuthenticationError("unknown state", 400)
250 args = ({'form': kwargs},)
251 kwargs = json.loads(openid_request)
252 return self.auth(*args, **kwargs)
255 class OpenID(AuthenticateRequest):
257 def __init__(self, *args, **kwargs):
258 super(OpenID, self).__init__(*args, **kwargs)
259 self.XRDS = XRDSHandler(*args, **kwargs)
260 self.yadis = UserXRDSHandler(*args, **kwargs)
261 self.id = IDHandler(*args, **kwargs)
262 self.Continue = Continue(*args, **kwargs)
263 self.Consent = Consent(*args, **kwargs)
266 def GET(self, *args, **kwargs):
267 return self.auth(**kwargs)
269 def POST(self, *args, **kwargs):
270 return self.auth(**kwargs)