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.value != self.trans.provider:
30 self.debug('Invalid transaction, %s != %s' % (
31 self.trans.cookie.value, self.trans.provider))
32 except Exception, e: # pylint: disable=broad-except
33 self.debug('Transaction initialization failed: %s' % repr(e))
34 raise cherrypy.HTTPError(400, 'Invalid transaction id')
36 def pre_GET(self, *args, **kwargs):
37 self._preop(*args, **kwargs)
39 def pre_POST(self, *args, **kwargs):
40 self._preop(*args, **kwargs)
42 def _get_form(self, *args):
45 first = args[0] if len(args) > 0 else None
46 second = first[0] if len(first) > 0 else None
47 if isinstance(second, dict):
48 form = second.get('form', None)
51 def auth(self, *args, **kwargs):
53 form = self._get_form(args)
55 request = self._parse_request(**kwargs)
56 return self._openid_checks(request, form, **kwargs)
57 except InvalidRequest, e:
58 raise cherrypy.HTTPError(e.code, e.msg)
59 except AuthenticationError, e:
61 raise cherrypy.HTTPError(e.code, e.msg)
62 return self._respond(request.answer(False))
64 # get attributes, and apply policy mapping and filtering
65 def _source_attributes(self, session):
66 policy = Policy(self.cfg.default_attribute_mapping,
67 self.cfg.default_allowed_attributes)
68 userattrs = session.get_user_attrs()
69 mappedattrs, _ = policy.map_attributes(userattrs)
70 attributes = policy.filter_attributes(mappedattrs)
71 self.debug('Filterd attributes: %s' % repr(attributes))
74 def _parse_request(self, **kwargs):
77 request = self.cfg.server.decodeRequest(kwargs)
78 except ProtocolError, openid_error:
79 self.debug('ProtocolError: %s' % openid_error)
80 raise InvalidRequest('Invalid OpenID request', 400)
83 self.debug('No request')
84 raise cherrypy.HTTPRedirect(self.basepath)
88 def _openid_checks(self, request, form, **kwargs):
93 self.debug('Mode: %s Stage: %s User: %s' % (
94 kwargs['openid.mode'], self.stage, user.name))
95 if kwargs.get('openid.mode', None) == 'checkid_setup':
97 if self.stage == 'init':
98 returl = '%s/openid/Continue?%s' % (
99 self.basepath, self.trans.get_GET_arg())
100 data = {'openid_stage': 'auth',
101 'openid_request': json.dumps(kwargs),
102 'login_return': returl,
103 'login_target': request.trust_root}
104 self.trans.store(data)
105 redirect = '%s/login?%s' % (self.basepath,
106 self.trans.get_GET_arg())
107 self.debug('Redirecting: %s' % redirect)
108 raise cherrypy.HTTPRedirect(redirect)
110 raise AuthenticationError("unknown user", 401)
112 elif kwargs.get('openid.mode', None) == 'checkid_immediate':
113 # This is immediate, so we need to assert or fail
114 if user.is_anonymous:
115 return self._respond(request.answer(False))
120 return self._respond(self.cfg.server.handleRequest(request))
122 # check if this is discovery or needs identity matching checks
123 if not request.idSelect():
124 idurl = self.cfg.identity_url_template % {'username': user.name}
125 if request.identity != idurl:
126 raise AuthenticationError("User ID mismatch!", 401)
128 # check if the relying party is trusted
129 if request.trust_root in self.cfg.untrusted_roots:
130 raise AuthenticationError("Untrusted Relying party", 401)
132 # if the party is explicitly whitelisted just respond
133 if request.trust_root in self.cfg.trusted_roots:
134 return self._respond(self._response(request, us))
136 allowroot = 'allow-%s' % request.trust_root
139 userdata = user.load_plugin_data(self.cfg.name)
140 expiry = int(userdata[allowroot])
141 except Exception, e: # pylint: disable=broad-except
144 if expiry > int(time.time()):
145 self.debug("User has unexpired previous authorization")
146 return self._respond(self._response(request, us))
149 raise AuthenticationError("No consent for immediate", 401)
151 if self.stage == 'consent':
153 raise AuthenticationError("Unintelligible consent", 401)
154 allow = form.get('decided_allow', False)
156 raise AuthenticationError("User declined", 401)
158 days = int(form.get('remember_for_days', '0'))
159 if days < 0 or days > 7:
161 userdata = {allowroot: str(int(time.time()) + (days*86400))}
162 user.save_plugin_data(self.cfg.name, userdata)
163 except Exception, e: # pylint: disable=broad-except
167 # all done we consent!
168 return self._respond(self._response(request, us))
171 data = {'openid_stage': 'consent',
172 'openid_request': json.dumps(kwargs)}
173 self.trans.store(data)
175 # Add extension data to this dictionary
177 "Trust Root": request.trust_root,
179 userattrs = self._source_attributes(us)
180 for n, e in self.cfg.extensions.available().items():
181 data = e.get_display_data(request, userattrs)
182 self.debug('%s returned %s' % (n, repr(data)))
183 for key, value in data.items():
184 ad[self.cfg.mapping.display_name(key)] = value
188 "action": '%s/openid/Consent' % (self.basepath),
189 "trustroot": request.trust_root,
190 "username": user.name,
193 context.update(dict((self.trans.get_POST_tuple(),)))
194 return self._template('openid/consent_form.html', **context)
196 def _response(self, request, session):
197 user = session.get_user()
198 identity_url = self.cfg.identity_url_template % {'username': user.name}
199 response = request.answer(
201 identity=identity_url,
202 claimed_id=identity_url
204 userattrs = self._source_attributes(session)
205 for _, e in self.cfg.extensions.available().items():
206 resp = e.get_response(request, userattrs)
208 response.addExtension(resp)
211 def _respond(self, response):
213 self.debug('Response: %s' % response)
214 webresponse = self.cfg.server.encodeResponse(response)
215 cherrypy.response.headers.update(webresponse.headers)
216 cherrypy.response.status = webresponse.code
217 return webresponse.body
218 except EncodingError, encoding_error:
219 self.debug('Unable to respond because: %s' % encoding_error)
220 cherrypy.response.headers = {
221 'Content-Type': 'text/plain; charset=UTF-8'
223 cherrypy.response.status = 400
224 return encoding_error.response.encodeToKVForm()
227 class Continue(AuthenticateRequest):
229 def GET(self, *args, **kwargs):
230 transdata = self.trans.retrieve()
231 self.stage = transdata.get('openid_stage', None)
232 openid_request = transdata.get('openid_request', None)
233 if self.stage is None or openid_request is None:
234 raise AuthenticationError("unknown state", 400)
236 kwargs = json.loads(openid_request)
237 return self.auth(**kwargs)
240 class Consent(AuthenticateRequest):
242 def POST(self, *args, **kwargs):
243 transdata = self.trans.retrieve()
244 self.stage = transdata.get('openid_stage', None)
245 openid_request = transdata.get('openid_request', None)
246 if self.stage is None or openid_request is None:
247 raise AuthenticationError("unknown state", 400)
249 args = ({'form': kwargs},)
250 kwargs = json.loads(openid_request)
251 return self.auth(*args, **kwargs)
254 class OpenID(AuthenticateRequest):
256 def __init__(self, *args, **kwargs):
257 super(OpenID, self).__init__(*args, **kwargs)
258 self.XRDS = XRDSHandler(*args, **kwargs)
259 self.yadis = UserXRDSHandler(*args, **kwargs)
260 self.id = IDHandler(*args, **kwargs)
261 self.Continue = Continue(*args, **kwargs)
262 self.Consent = Consent(*args, **kwargs)
265 def GET(self, *args, **kwargs):
266 return self.auth(**kwargs)
268 def POST(self, *args, **kwargs):
269 return self.auth(**kwargs)