login function redirecting to idp and callback view added
This commit is contained in:
parent
b169c79741
commit
c8204b7344
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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()
|
|
@ -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
|
||||
|
|
Reference in New Issue