From c8204b7344e6c3fa5926a5b96010b21ef827efca Mon Sep 17 00:00:00 2001 From: Serghei Mihai Date: Wed, 4 Feb 2015 15:24:41 +0100 Subject: [PATCH] login function redirecting to idp and callback view added --- ckanext/ozwillo_pyoidc/conf.py | 54 +++++++ ckanext/ozwillo_pyoidc/oidc.py | 234 +++++++++++++++++++++++++++++++ ckanext/ozwillo_pyoidc/plugin.py | 59 +++++++- 3 files changed, 346 insertions(+), 1 deletion(-) create mode 100644 ckanext/ozwillo_pyoidc/conf.py create mode 100755 ckanext/ozwillo_pyoidc/oidc.py mode change 100644 => 100755 ckanext/ozwillo_pyoidc/plugin.py diff --git a/ckanext/ozwillo_pyoidc/conf.py b/ckanext/ozwillo_pyoidc/conf.py new file mode 100644 index 0000000..80a8a84 --- /dev/null +++ b/ckanext/ozwillo_pyoidc/conf.py @@ -0,0 +1,54 @@ +PORT = 8666 +#BASE = "https://lingon.ladok.umu.se:" + str(PORT) + "/" +BASE = "http://ckan.dev.entrouvert.org" + + +# If BASE is https these has to be specified +SERVER_CERT = "certs/server.crt" +SERVER_KEY = "certs/server.key" +CA_BUNDLE = None + +VERIFY_SSL = False + +# information used when registering the client, this may be the same for all OPs + +ME = { + "application_type": "web", + "application_name": "idpproxy", + "contacts": ["ops@example.com"], + "redirect_uris": ["%sauthz_cb" % BASE], + "post_logout_redirect_uris": ["%slogout" % BASE], + "response_types": ["code"] +} + +BEHAVIOUR = { + "response_type": "code", + "scope": ["openid", "profile", "email", "address", "phone"], +} + +ACR_VALUES = ["SAML"] + +# The keys in this dictionary are the OPs short userfriendly name +# not the issuer (iss) name. + +CLIENTS = { + # The ones that support webfinger, OP discovery and client registration + # This is the default, any client that is not listed here is expected to + # support dynamic discovery and registration. + # Supports OP information lookup but not client registration + "ozwillo": { + "srv_discovery_url": "https://accounts.ozwillo-preprod.eu/", + "client_registration": { + "client_id": "64a1002e-3149-4e1d-a374-6ff08b79dae6", + "client_secret": "RCjT6YTN7CY0l8UAbGUOtSOrAKZKW4XXzK1ZWi7u0nE", + "redirect_uris": ["https://ckan.dev.entrouvert.org/openid/callback"], + }, + "behaviour": { + "response_type": "code", + "scope": ["openid", "profile"] + }, + "allow": { + "issuer_mismatch": True + } + } +} diff --git a/ckanext/ozwillo_pyoidc/oidc.py b/ckanext/ozwillo_pyoidc/oidc.py new file mode 100755 index 0000000..412bdf0 --- /dev/null +++ b/ckanext/ozwillo_pyoidc/oidc.py @@ -0,0 +1,234 @@ +from oic.utils.http_util import Redirect +from oic.exception import MissingAttribute +from oic import oic +from oic.oauth2 import rndstr, ErrorResponse +from oic.oic import ProviderConfigurationResponse, AuthorizationResponse +from oic.oic import RegistrationResponse +from oic.oic import AuthorizationRequest +from oic.utils.authn.client import CLIENT_AUTHN_METHOD + +__author__ = 'roland' + +import logging + +logger = logging.getLogger(__name__) + + +class OIDCError(Exception): + pass + + +class Client(oic.Client): + def __init__(self, client_id=None, ca_certs=None, + client_prefs=None, client_authn_method=None, keyjar=None, + verify_ssl=True, behaviour=None): + oic.Client.__init__(self, client_id, ca_certs, client_prefs, + client_authn_method, keyjar, verify_ssl) + if behaviour: + self.behaviour = behaviour + + def create_authn_request(self, session, acr_value=None): + session["state"] = rndstr() + session["nonce"] = rndstr() + request_args = { + "response_type": self.behaviour["response_type"], + "scope": self.behaviour["scope"], + "state": session["state"], + # "nonce": session["nonce"], + "redirect_uri": self.registration_response["redirect_uris"][0] + } + + if acr_value is not None: + request_args["acr_values"] = acr_value + + cis = self.construct_AuthorizationRequest(request_args=request_args) + logger.debug("request: %s" % cis) + + url, body, ht_args, cis = self.uri_and_body(AuthorizationRequest, cis, + method="GET", + request_args=request_args) + + logger.debug("body: %s" % body) + logger.info("URL: %s" % url) + logger.debug("ht_args: %s" % ht_args) + + return str(url), ht_args + + def callback(self, response): + """ + This is the method that should be called when an AuthN response has been + received from the OP. + + :param response: The URL returned by the OP + :return: + """ + authresp = self.parse_response(AuthorizationResponse, response, + sformat="dict", keyjar=self.keyjar) + + if isinstance(authresp, ErrorResponse): + return OIDCError("Access denied") + + try: + self.id_token[authresp["state"]] = authresp["id_token"] + except KeyError: + pass + + if self.behaviour["response_type"] == "code": + # get the access token + try: + args = { + "grant_type": "authorization_code", + "code": authresp["code"], + "redirect_uri": self.registration_response[ + "redirect_uris"][0], + "client_id": self.client_id, + "client_secret": self.client_secret + } + + atresp = self.do_access_token_request( + scope="openid", state=authresp["state"], request_args=args, + authn_method=self.registration_response["token_endpoint_auth_method"]) + except Exception as err: + logger.error("%s" % err) + raise + + if isinstance(atresp, ErrorResponse): + raise OIDCError("Invalid response %s." % atresp["error"]) + + inforesp = self.do_user_info_request(state=authresp["state"]) + + if isinstance(inforesp, ErrorResponse): + raise OIDCError("Invalid response %s." % inforesp["error"]) + + userinfo = inforesp.to_dict() + + logger.debug("UserInfo: %s" % inforesp) + + return userinfo + + +class OIDCClients(object): + def __init__(self, config): + """ + + :param config: Imported configuration module + :return: + """ + self.client = {} + self.client_cls = Client + self.config = config + + for key, val in config.CLIENTS.items(): + if key == "": + continue + else: + self.client[key] = self.create_client(**val) + + def create_client(self, userid="", **kwargs): + """ + Do an instantiation of a client instance + + :param userid: An identifier of the user + :param: Keyword arguments + Keys are ["srv_discovery_url", "client_info", "client_registration", + "provider_info"] + :return: client instance + """ + + _key_set = set(kwargs.keys()) + args = {} + for param in ["verify_ssl"]: + try: + args[param] = kwargs[param] + except KeyError: + pass + else: + _key_set.discard(param) + + client = self.client_cls(client_authn_method=CLIENT_AUTHN_METHOD, + behaviour=kwargs["behaviour"], verify_ssl=self.config.VERIFY_SSL, **args) + + # The behaviour parameter is not significant for the election process + _key_set.discard("behaviour") + for param in ["allow"]: + try: + setattr(client, param, kwargs[param]) + except KeyError: + pass + else: + _key_set.discard(param) + + if _key_set == set(["client_info"]): # Everything dynamic + # There has to be a userid + if not userid: + raise MissingAttribute("Missing userid specification") + + # Find the service that provides information about the OP + issuer = client.wf.discovery_query(userid) + # Gather OP information + _ = client.provider_config(issuer) + # register the client + _ = client.register(client.provider_info["registration_endpoint"], + **kwargs["client_info"]) + elif _key_set == set(["client_info", "srv_discovery_url"]): + # Ship the webfinger part + # Gather OP information + _ = client.provider_config(kwargs["srv_discovery_url"]) + # register the client + _ = client.register(client.provider_info["registration_endpoint"], + **kwargs["client_info"]) + elif _key_set == set(["provider_info", "client_info"]): + client.handle_provider_config( + ProviderConfigurationResponse(**kwargs["provider_info"]), + kwargs["provider_info"]["issuer"]) + _ = client.register(client.provider_info["registration_endpoint"], + **kwargs["client_info"]) + elif _key_set == set(["provider_info", "client_registration"]): + client.handle_provider_config( + ProviderConfigurationResponse(**kwargs["provider_info"]), + kwargs["provider_info"]["issuer"]) + client.store_registration_info(RegistrationResponse( + **kwargs["client_registration"])) + elif _key_set == set(["srv_discovery_url", "client_registration"]): + _ = client.provider_config(kwargs["srv_discovery_url"]) + client.store_registration_info(RegistrationResponse( + **kwargs["client_registration"])) + else: + raise Exception("Configuration error ?") + + return client + + def dynamic_client(self, userid): + client = self.client_cls(client_authn_method=CLIENT_AUTHN_METHOD, + verify_ssl=self.config.VERIFY_SSL) + + issuer = client.wf.discovery_query(userid) + if issuer in self.client: + return self.client[issuer] + else: + # Gather OP information + _pcr = client.provider_config(issuer) + # register the client + _ = client.register(_pcr["registration_endpoint"], + **self.config.CLIENTS[""]["client_info"]) + try: + client.behaviour.update(**self.config.CLIENTS[""]["behaviour"]) + except KeyError: + pass + + self.client[issuer] = client + return client + + def __getitem__(self, item): + """ + Given a service or user identifier return a suitable client + :param item: + :return: + """ + try: + return self.client[item] + except KeyError: + return self.dynamic_client(item) + + def keys(self): + return self.client.keys() diff --git a/ckanext/ozwillo_pyoidc/plugin.py b/ckanext/ozwillo_pyoidc/plugin.py old mode 100644 new mode 100755 index 46b7ad4..2c0cf4d --- a/ckanext/ozwillo_pyoidc/plugin.py +++ b/ckanext/ozwillo_pyoidc/plugin.py @@ -1,13 +1,70 @@ +import logging + import ckan.plugins as plugins import ckan.plugins.toolkit as toolkit +from ckan.common import session +import ckan.lib.base as base +from pylons import config, request + +from oidc import OIDCClients + +import conf + +from oic.oic import Client, AuthorizationRequest +from oic.utils.authn.client import CLIENT_AUTHN_METHOD + +plugin_config_prefix = 'ckanext.ozwillo_pyoidc.' + +log = logging.getLogger(__name__) + +Client = OIDCClients(conf)['ozwillo'] + +def openid_callback(context, data): + print context + print data class OzwilloPyoidcPlugin(plugins.SingletonPlugin): plugins.implements(plugins.IConfigurer) + plugins.implements(plugins.IRoutes) + plugins.implements(plugins.IAuthenticator, inherit=True) - # IConfigurer + def __init__(self, name=None): + self.client = Client + + def before_map(self, map): + map.redirect('/organization/{id:.*}/sso', '/user/login') + map.connect('/openid/callback', + controller='ckanext.ozwillo_pyoidc.plugin:OpenidController', + action='openid_callback') + return map + + def after_map(self, map): + return map + + def identify(self): + # must set toolkit.c.user + pass + + def login(self): + url, ht_args = self.client.create_authn_request(session, conf.ACR_VALUES) + if ht_args: + toolkit.request.headers.update(ht_args) + toolkit.redirect_to(url) + + def logout(self): + # revoke all auth tokens + # redirect to logout in ozwillo + revoke_endpoint = 'https://portal.ozwillo-preprod.eu/a/revoke' + toolkit.redirect('/user/_logout') def update_config(self, config_): toolkit.add_template_directory(config_, 'templates') toolkit.add_public_directory(config_, 'public') toolkit.add_resource('fanstatic', 'ozwillo_pyoidc') + +class OpenidController(base.BaseController): + + def openid_callback(self): + userinfo = Client.callback(request.GET) + return "userinfo: %s" % userinfo