diff options
author | Patrick Uiterwijk <puiterwijk@redhat.com> | 2014-10-06 19:12:13 +0200 |
---|---|---|
committer | Patrick Uiterwijk <puiterwijk@redhat.com> | 2014-10-24 18:02:21 +0200 |
commit | f461a713ce28e434a34dca4e4d1abbfe255ef1ff (patch) | |
tree | 20e3167cfe3b28e8c7163e661fc757ffd8df228b /ipsilon/providers/openid | |
parent | c6fab2542f52f6cca71c207c1925785971e51295 (diff) | |
download | ipsilon-f461a713ce28e434a34dca4e4d1abbfe255ef1ff.tar.gz ipsilon-f461a713ce28e434a34dca4e4d1abbfe255ef1ff.tar.xz ipsilon-f461a713ce28e434a34dca4e4d1abbfe255ef1ff.zip |
Add OpenIDP Provider
This commit implements all the core functionality needed to expose
an OpenID Identity Provider including a framework to dynamycally add
extensions.
Signed-off-by: Patrick Uiterwijk <puiterwijk@redhat.com>
Signed-off-by: Simo Sorce <simo@redhat.com>
Reviewed-by: Patrick Uiterwijk <puiterwijk@redhat.com>
Diffstat (limited to 'ipsilon/providers/openid')
-rw-r--r-- | ipsilon/providers/openid/__init__.py | 0 | ||||
-rwxr-xr-x | ipsilon/providers/openid/auth.py | 261 | ||||
-rw-r--r-- | ipsilon/providers/openid/extensions/__init__.py | 0 | ||||
-rwxr-xr-x | ipsilon/providers/openid/extensions/common.py | 67 | ||||
-rwxr-xr-x | ipsilon/providers/openid/meta.py | 102 |
5 files changed, 430 insertions, 0 deletions
diff --git a/ipsilon/providers/openid/__init__.py b/ipsilon/providers/openid/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/ipsilon/providers/openid/__init__.py diff --git a/ipsilon/providers/openid/auth.py b/ipsilon/providers/openid/auth.py new file mode 100755 index 0000000..abf19ae --- /dev/null +++ b/ipsilon/providers/openid/auth.py @@ -0,0 +1,261 @@ +#!/usr/bin/python +# +# Copyright (C) 2014 Ipsilon project Contributors, for licensee see COPYING + +from ipsilon.providers.common import ProviderPageBase +from ipsilon.providers.common import AuthenticationError, InvalidRequest +from ipsilon.providers.openid.meta import XRDSHandler, UserXRDSHandler +from ipsilon.providers.openid.meta import IDHandler +from ipsilon.util.trans import Transaction +from ipsilon.util.user import UserSession + +from openid.server.server import ProtocolError, EncodingError + +import cherrypy +import time +import json + + +class AuthenticateRequest(ProviderPageBase): + + def __init__(self, *args, **kwargs): + super(AuthenticateRequest, self).__init__(*args, **kwargs) + self.stage = 'init' + self.trans = None + + def _preop(self, *args, **kwargs): + try: + # generate a new id or get current one + self.trans = Transaction('openid', **kwargs) + if self.trans.cookie.value != self.trans.provider: + self.debug('Invalid transaction, %s != %s' % ( + self.trans.cookie.value, self.trans.provider)) + except Exception, e: # pylint: disable=broad-except + self.debug('Transaction initialization failed: %s' % repr(e)) + raise cherrypy.HTTPError(400, 'Invalid transaction id') + + def pre_GET(self, *args, **kwargs): + self._preop(*args, **kwargs) + + def pre_POST(self, *args, **kwargs): + self._preop(*args, **kwargs) + + def _get_form(self, *args): + form = None + if args is not None: + first = args[0] if len(args) > 0 else None + second = first[0] if len(first) > 0 else None + if type(second) is dict: + form = second.get('form', None) + return form + + def auth(self, *args, **kwargs): + request = None + form = self._get_form(args) + try: + request = self._parse_request(**kwargs) + return self._openid_checks(request, form, **kwargs) + except InvalidRequest, e: + raise cherrypy.HTTPError(e.code, e.msg) + except AuthenticationError, e: + if request is None: + raise cherrypy.HTTPError(e.code, e.msg) + return self._respond(request.answer(False)) + + def _parse_request(self, **kwargs): + request = None + try: + request = self.cfg.server.decodeRequest(kwargs) + except ProtocolError, openid_error: + self.debug('ProtocolError: %s' % openid_error) + raise InvalidRequest('Invalid OpenID request', 400) + + if request is None: + self.debug('No request') + raise cherrypy.HTTPRedirect(self.basepath) + + return request + + def _openid_checks(self, request, form, **kwargs): + us = UserSession() + user = us.get_user() + immediate = False + + self.debug('Mode: %s Stage: %s User: %s' % ( + kwargs['openid.mode'], self.stage, user.name)) + if kwargs.get('openid.mode', None) == 'checkid_setup': + if user.is_anonymous: + if self.stage == 'init': + returl = '%s/openid/Continue?%s' % ( + self.basepath, self.trans.get_GET_arg()) + data = {'openid_stage': 'auth', + 'openid_request': json.dumps(kwargs), + 'login_return': returl} + self.trans.store(data) + redirect = '%s/login?%s' % (self.basepath, + self.trans.get_GET_arg()) + self.debug('Redirecting: %s' % redirect) + raise cherrypy.HTTPRedirect(redirect) + else: + raise AuthenticationError("unknown user", 401) + + elif kwargs.get('openid.mode', None) == 'checkid_immediate': + # This is immediate, so we need to assert or fail + if user.is_anonymous: + return self._respond(request.answer(False)) + + immediate = True + + else: + return self._respond(self.cfg.server.handleRequest(request)) + + # check if this is discovery or ned identity matching checks + if not request.idSelect(): + idurl = self.cfg.identity_url_template % {'username': user.name} + if request.identity != idurl: + raise AuthenticationError("User ID mismatch!", 401) + + # check if the ralying party is trusted + if request.trust_root in self.cfg.untrusted_roots: + raise AuthenticationError("Untrusted Relying party", 401) + + # if the party is explicitly whitelisted just respond + if request.trust_root in self.cfg.trusted_roots: + return self._respond(self._response(request, us)) + + allowroot = 'allow-%s' % request.trust_root + + try: + userdata = user.load_plugin_data(self.cfg.name) + expiry = int(userdata[allowroot]) + except Exception, e: # pylint: disable=broad-except + self.debug(e) + expiry = 0 + if expiry > int(time.time()): + self.debug("User has unexpired previous authorization") + return self._respond(self._response(request, us)) + + if immediate: + raise AuthenticationError("No consent for immediate", 401) + + if self.stage == 'consent': + if form is None: + raise AuthenticationError("Unintelligible consent", 401) + allow = form.get('decided_allow', False) + if not allow: + raise AuthenticationError("User declined", 401) + try: + days = int(form.get('remember_for_days', '0')) + if days < 0 or days > 7: + raise + userdata = {allowroot: str(int(time.time()) + (days*86400))} + user.save_plugin_data(self.cfg.name, userdata) + except Exception, e: # pylint: disable=broad-except + self.debug(e) + days = 0 + + # all done we consent! + return self._respond(self._response(request, us)) + + else: + data = {'openid_stage': 'consent', + 'openid_request': json.dumps(kwargs)} + self.trans.store(data) + + # Add extension data to this list of dictionaries + ad = [ + { + "Trust Root": request.trust_root, + }, + ] + userattrs = us.get_user_attrs() + for n, e in self.cfg.extensions.items(): + data = e.get_display_data(request, userattrs) + self.debug('%s returned %s' % (n, repr(data))) + ad.append(data) + + context = { + "title": 'Consent', + "action": '%s/openid/Consent' % (self.basepath), + "trustroot": request.trust_root, + "username": user.name, + "authz_details": ad, + } + context.update(dict((self.trans.get_POST_tuple(),))) + # pylint: disable=star-args + return self._template('openid/consent_form.html', **context) + + def _response(self, request, session): + user = session.get_user() + identity_url = self.cfg.identity_url_template % {'username': user.name} + response = request.answer( + True, + identity=identity_url, + claimed_id=identity_url + ) + userattrs = session.get_user_attrs() + for _, e in self.cfg.extensions.items(): + resp = e.get_response(request, userattrs) + if resp is not None: + response.addExtension(resp) + return response + + def _respond(self, response): + try: + self.debug('Response: %s' % response) + webresponse = self.cfg.server.encodeResponse(response) + cherrypy.response.headers.update(webresponse.headers) + cherrypy.response.status = webresponse.code + return webresponse.body + except EncodingError, encoding_error: + self.debug('Unable to respond because: %s' % encoding_error) + cherrypy.response.headers = { + 'Content-Type': 'text/plain; charset=UTF-8' + } + cherrypy.response.status = 400 + return encoding_error.response.encodeToKVForm() + + +class Continue(AuthenticateRequest): + + def GET(self, *args, **kwargs): + transdata = self.trans.retrieve() + self.stage = transdata.get('openid_stage', None) + openid_request = transdata.get('openid_request', None) + if self.stage is None or openid_request is None: + raise AuthenticationError("unknown state", 400) + + kwargs = json.loads(openid_request) + return self.auth(**kwargs) + + +class Consent(AuthenticateRequest): + + def POST(self, *args, **kwargs): + transdata = self.trans.retrieve() + self.stage = transdata.get('openid_stage', None) + openid_request = transdata.get('openid_request', None) + if self.stage is None or openid_request is None: + raise AuthenticationError("unknown state", 400) + + args = ({'form': kwargs},) + kwargs = json.loads(openid_request) + return self.auth(*args, **kwargs) + + +class OpenID(AuthenticateRequest): + + def __init__(self, *args, **kwargs): + super(OpenID, self).__init__(*args, **kwargs) + self.XRDS = XRDSHandler(*args, **kwargs) + self.yadis = UserXRDSHandler(*args, **kwargs) + self.id = IDHandler(*args, **kwargs) + self.Continue = Continue(*args, **kwargs) + self.Consent = Consent(*args, **kwargs) + self.trans = None + + def GET(self, *args, **kwargs): + return self.auth(**kwargs) + + def POST(self, *args, **kwargs): + return self.auth(**kwargs) diff --git a/ipsilon/providers/openid/extensions/__init__.py b/ipsilon/providers/openid/extensions/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/ipsilon/providers/openid/extensions/__init__.py diff --git a/ipsilon/providers/openid/extensions/common.py b/ipsilon/providers/openid/extensions/common.py new file mode 100755 index 0000000..b75d394 --- /dev/null +++ b/ipsilon/providers/openid/extensions/common.py @@ -0,0 +1,67 @@ +#!/usr/bin/python +# +# Copyright (C) 2014 Ipsilon project Contributors, for licensee see COPYING + +from __future__ import absolute_import + +from ipsilon.providers.common import FACILITY +from ipsilon.util.plugin import PluginLoader +from ipsilon.util.log import Log + + +class OpenidExtensionBase(Log): + + def __init__(self, name=None): + self.name = name + self.enabled = False + self.type_uris = [] + + def _display(self, request, userdata): + raise NotImplementedError + + def _response(self, request, userdata): + raise NotImplementedError + + def get_type_uris(self): + if self.enabled: + return self.type_uris + return [] + + def get_display_data(self, request, userdata): + if self.enabled: + return self._display(request, userdata) + return {} + + def get_response(self, request, userdata): + if self.enabled: + return self._response(request, userdata) + return None + + def enable(self): + self.enabled = True + + def disable(self): + self.enabled = False + + +FACILITY = 'openid_extensions' + + +class LoadExtensions(Log): + + def __init__(self, enabled): + loader = PluginLoader(LoadExtensions, FACILITY, 'OpenidExtension') + self.plugins = loader.get_plugin_data() + + available = self.plugins['available'].keys() + self._debug('Available Extensions: %s' % str(available)) + + for item in enabled: + if item not in self.plugins['available']: + self.debug('<%s> not available' % item) + continue + self.debug('Enable OpenId extension: %s' % item) + self.plugins['available'][item].enable() + + def get_extensions(self): + return self.plugins['available'] diff --git a/ipsilon/providers/openid/meta.py b/ipsilon/providers/openid/meta.py new file mode 100755 index 0000000..a04a78c --- /dev/null +++ b/ipsilon/providers/openid/meta.py @@ -0,0 +1,102 @@ +#!/usr/bin/python +# +# Copyright (C) 2014 Ipsilon project Contributors, for licensee see COPYING + +from ipsilon.providers.common import ProviderPageBase + +import cherrypy + + +class MetaHandler(ProviderPageBase): + + def __init__(self, *args, **kwargs): + super(MetaHandler, self).__init__(*args, **kwargs) + self.default_headers.update({ + 'Cache-Control': 'no-cache, must-revalidate', + 'Pragma': 'no-cache', + 'Expires': 'Thu, 01 Dec 1994 16:00:00 GMT', + }) + self._template_name = None + self._take_args = False + + def reply(self, **kwargs): + if self._template_name is None: + raise ValueError('Template not set') + return self._template(self._template_name, **kwargs) + + def default(self, *args, **kwargs): + if self._take_args: + return self.root(*args, **kwargs) + raise cherrypy.NotFound() + + +class XRDSHandler(MetaHandler): + + def __init__(self, *args, **kwargs): + super(XRDSHandler, self).__init__(*args, **kwargs) + self.default_headers['Content-Type'] = 'application/xrds+xml' + self._template_name = 'openid/xrds.xml' + + def GET(self, *args, **kwargs): + types = [ + 'http://specs.openid.net/auth/2.0/server', + 'http://openid.net/server/1.0', + ] + for _, e in self.cfg.extensions.items(): + types.extend(e.get_type_uris()) + + return self.reply(types=types, + uri=self.cfg.endpoint_url) + + +class UserXRDSHandler(XRDSHandler): + + def __init__(self, *args, **kwargs): + super(UserXRDSHandler, self).__init__(*args, **kwargs) + self._take_args = True + + def GET(self, *args, **kwargs): + if len(args) != 1: + raise cherrypy.NotFound() + if args[0].endswith('.xrds'): + name = args[0][:-5] + identity_url = self.cfg.identity_url_template % {'username': name} + types = [ + 'http://specs.openid.net/auth/2.0/signon', + 'http://openid.net/signon/1.0', + ] + for _, e in self.cfg.extensions.items(): + types.extend(e.get_type_uris()) + + return self.reply(types=types, + uri=self.cfg.endpoint_url, + localid=identity_url) + + raise cherrypy.NotFound() + + +class IDHandler(MetaHandler): + + def __init__(self, *args, **kwargs): + super(IDHandler, self).__init__(*args, **kwargs) + self._template_name = 'openid/userpage.html' + self._take_args = True + + def GET(self, *args, **kwargs): + if len(args) != 1: + raise cherrypy.NotFound() + name = args[0] + yadis = '%syadis/%s.xrds' % (self.cfg.endpoint_url, name) + cherrypy.response.headers['X-XRDS-Location'] = yadis + + endpoint_url = self.cfg.endpoint_url + identity_url = self.cfg.identity_url_template % {'username': name} + + HEAD_LINK = '<link rel="%s" href="%s">' + provider_heads = [HEAD_LINK % ('openid2.provider', endpoint_url), + HEAD_LINK % ('openid.server', endpoint_url)] + user_heads = [HEAD_LINK % ('openid2.delegate', identity_url), + HEAD_LINK % ('openid.local_id', identity_url)] + heads = {'provider': provider_heads, 'user': user_heads} + + return self.reply(title='Userpage', username=name, heads=heads) |