From 0a68d9954821c092a6aeaf7bcdd87d8864a46e13 Mon Sep 17 00:00:00 2001 From: Harry Kodden Date: Wed, 7 Nov 2018 10:11:10 +0100 Subject: [PATCH 1/7] Dynamic Provider --- flask_oidc/__init__.py | 162 ++++++++++++++++++++++++++++++++++++----- 1 file changed, 144 insertions(+), 18 deletions(-) diff --git a/flask_oidc/__init__.py b/flask_oidc/__init__.py index 6ea1520..52a1a0a 100644 --- a/flask_oidc/__init__.py +++ b/flask_oidc/__init__.py @@ -103,7 +103,8 @@ class OpenIDConnect(object): The core OpenID Connect client object. """ def __init__(self, app=None, credentials_store=None, http=None, time=None, - urandom=None): + urandom=None, provider=None): + self.credentials_store = credentials_store\ if credentials_store is not None\ else MemoryCredentials() @@ -119,10 +120,24 @@ def __init__(self, app=None, credentials_store=None, http=None, time=None, # By default, we do not have a custom callback self._custom_callback = None + # In the beginning we know nothing ... + self.client_secrets = None + # get stuff from the app's config, which may override stuff set above - if app is not None: + + if app: self.init_app(app) + if provider: + self.init_provider(app, provider) + + def __exit__(self, exception_type, exception_value, traceback): + self.logout() + + self.client_secrets = None + + current_app.config['OIDC_VALID_ISSUERS'] = None + def init_app(self, app): """ Do setup that requires a Flask app. @@ -130,11 +145,9 @@ def init_app(self, app): :param app: The application to initialize. :type app: Flask """ - secrets = self.load_secrets(app) - self.client_secrets = list(secrets.values())[0] - secrets_cache = DummySecretsCache(secrets) # Set some default configuration options + app.config.setdefault('OIDC_CLIENT_SECRETS', None) app.config.setdefault('OIDC_SCOPES', ['openid', 'email']) app.config.setdefault('OIDC_GOOGLE_APPS_DOMAIN', None) app.config.setdefault('OIDC_ID_TOKEN_COOKIE_NAME', 'oidc_id_token') @@ -169,13 +182,6 @@ def init_app(self, app): app.before_request(self._before_request) app.after_request(self._after_request) - # Initialize oauth2client - self.flow = flow_from_clientsecrets( - app.config['OIDC_CLIENT_SECRETS'], - scope=app.config['OIDC_SCOPES'], - cache=secrets_cache) - assert isinstance(self.flow, OAuth2WebServerFlow) - # create signers using the Flask secret key self.extra_data_serializer = JSONWebSignatureSerializer( app.config['SECRET_KEY']) @@ -187,11 +193,115 @@ def init_app(self, app): except KeyError: pass - def load_secrets(self, app): - # Load client_secrets.json to pre-initialize some configuration - return _json_loads(open(app.config['OIDC_CLIENT_SECRETS'], - 'r').read()) - + def init_provider(self, provider): + """ + Do setup for a specific provider + + :param provider: The provider to initialize. + :type provider: Dictionary with at lease 'base_url' item + """ + + secrets = self.load_secrets(provider) + assert secrets != None, "Problem with loading secrets" + + self.client_secrets = list(secrets.values())[0] + secrets_cache = DummySecretsCache(secrets) + + # Initialize oauth2client + self.flow = flow_from_clientsecrets( + current_app.config['OIDC_CLIENT_SECRETS'], + scope=current_app.config['OIDC_SCOPES'], + cache=secrets_cache) + + assert isinstance(self.flow, OAuth2WebServerFlow) + + current_app.config['OIDC_VALID_ISSUERS'] = self.client_secrets.get('issuer') + + def load_secrets(self, provider): + + try: + static_secrets = current_app.config.get('OIDC_CLIENT_SECRETS', None) + if static_secrets: + return _json_loads(open(static_secrets,'r').read()) + except Exception as e: + raise Exception("Error reading secrets: {}, error: {}".format(static_secrets, str(e))) + + if not provider: + raise Exception("No Provider specified") + + try: + url = provider.get('base_url') + + if not url.endswith('/'): + url += '/' + + url += ".well-known/openid-configuration" + + logger.debug("Loading: {}".format(url)) + + provider_info = json.load( + urllib.request.urlopen(url) + ) + + except Exception as e: + raise Exception("Can not obtain well known information: {}".format(str(e))) + + for path in ['issuer', 'registration_endpoint', 'authorization_endpoint', 'token_endpoint', 'userinfo_endpoint', 'jwks_uri']: + if path in provider_info and provider_info[path].startswith('/'): + provider_info[path] = "{}{}".format(provider.get('base_url'), provider_info[path]) + + registration = provider.get('registration', None) + + if not registration: + try: + logger.debug("Dynamic Registration...") + + registration = requests.post( + provider_info['registration_endpoint'], + data = json.dumps({ + "redirect_uris": REDIRECT_URL, + "grant_types": "authorization_code", + "client_name": provider.get('client_name', "Dynamic Registration"), + "response_types": "code", + "token_endpoint_auth_method": "client_secret_post", + "application_type": "native" + }), + headers = { + 'Content-Type': "application/json", + 'Cache-Control': "no-cache" + } + ).json() + + logger.debug("Registration: {}".format(registration)) + + except Exception as e: + raise Exception("Can not make client registration: {}".format(str(e))) + + try: + try: + jwks_keys = json.load( + urllib.request.urlopen(provider_info['jwks_uri']) + ) + except: + jwks_keys = None + + return { + 'web' : { + 'client_id': registration.get('client_id'), + 'client_secret': registration.get('*client_secret*', registration.get('client_secret', None)), + 'auth_uri': provider_info['authorization_endpoint'], + 'token_uri': provider_info['token_endpoint'], + 'userinfo_uri': provider_info['userinfo_endpoint'], + 'jwks_keys': jwks_keys, + 'redirect_uris': REDIRECT_URL, + 'issuer': provider_info['issuer'], + } + } + except Exception as e: + raise Exception("Error in preparing result: {}".format(str(e))) + + raise Exception("No secrets loaded !") + @property def user_loggedin(self): """ @@ -404,7 +514,9 @@ def _after_request(self, response): def _before_request(self): g.oidc_id_token = None - self.authenticate_or_redirect() + + if self.client_secrets: + self.authenticate_or_redirect() def authenticate_or_redirect(self): """ @@ -567,6 +679,16 @@ def redirect_to_auth_server(self, destination=None, customstate=None): self._set_cookie_id_token(None) return redirect(auth_url) + def token(self): + try: + return self.credentials_store[g.oidc_id_token['sub']] + except KeyError: + logger.debug("No Token !", exc_info=True) + return None + + def details(self): + return self._retrieve_userinfo() + def _is_id_token_valid(self, id_token): """ Check if `id_token` is a current ID token for this application, @@ -584,6 +706,10 @@ def _is_id_token_valid(self, id_token): % id_token['iss']) return False + # Make sure that we only have a list of audiende when there are more than 1...chec + if 'aud' in id_token and isinstance(id_token['aud'], list) and len(id_token['aud']) == 1: + id_token['aud'] = id_token['aud'][0] + if isinstance(id_token['aud'], list): # step 3 for audience list if self.flow.client_id not in id_token['aud']: From 19e76fdea52dff8f6a22aad295395dfa42f89776 Mon Sep 17 00:00:00 2001 From: Harry Kodden Date: Wed, 7 Nov 2018 16:16:23 +0100 Subject: [PATCH 2/7] update --- flask_oidc/__init__.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/flask_oidc/__init__.py b/flask_oidc/__init__.py index 52a1a0a..541b7b7 100644 --- a/flask_oidc/__init__.py +++ b/flask_oidc/__init__.py @@ -34,6 +34,8 @@ import calendar from six.moves.urllib.parse import urlencode +from six.moves.urllib.request import urlopen + from flask import request, session, redirect, url_for, g, current_app from oauth2client.client import flow_from_clientsecrets, OAuth2WebServerFlow,\ AccessTokenRefreshError, OAuth2Credentials @@ -155,9 +157,7 @@ def init_app(self, app): app.config.setdefault('OIDC_ID_TOKEN_COOKIE_TTL', 7 * 86400) # 7 days # should ONLY be turned off for local debugging app.config.setdefault('OIDC_COOKIE_SECURE', True) - app.config.setdefault('OIDC_VALID_ISSUERS', - (self.client_secrets.get('issuer') or - GOOGLE_ISSUERS)) + app.config.setdefault('OIDC_VALID_ISSUERS', GOOGLE_ISSUERS) app.config.setdefault('OIDC_CLOCK_SKEW', 60) # 1 minute app.config.setdefault('OIDC_REQUIRE_VERIFIED_EMAIL', False) app.config.setdefault('OIDC_OPENID_REALM', None) @@ -240,7 +240,7 @@ def load_secrets(self, provider): logger.debug("Loading: {}".format(url)) provider_info = json.load( - urllib.request.urlopen(url) + urlopen(url) ) except Exception as e: @@ -259,7 +259,7 @@ def load_secrets(self, provider): registration = requests.post( provider_info['registration_endpoint'], data = json.dumps({ - "redirect_uris": REDIRECT_URL, + "redirect_uris": self._oidc_callback, "grant_types": "authorization_code", "client_name": provider.get('client_name', "Dynamic Registration"), "response_types": "code", @@ -280,7 +280,7 @@ def load_secrets(self, provider): try: try: jwks_keys = json.load( - urllib.request.urlopen(provider_info['jwks_uri']) + urlopen(provider_info['jwks_uri']) ) except: jwks_keys = None @@ -293,7 +293,7 @@ def load_secrets(self, provider): 'token_uri': provider_info['token_endpoint'], 'userinfo_uri': provider_info['userinfo_endpoint'], 'jwks_keys': jwks_keys, - 'redirect_uris': REDIRECT_URL, + 'redirect_uris': self._oidc_callback, 'issuer': provider_info['issuer'], } } From 006bde1003ab3b3b8c0d5aea3c3a71ba98c46025 Mon Sep 17 00:00:00 2001 From: Harry Kodden Date: Thu, 8 Nov 2018 10:50:19 +0100 Subject: [PATCH 3/7] Update, for backwards compatibility, init_app will do an initial init_provider as well --- flask_oidc/__init__.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/flask_oidc/__init__.py b/flask_oidc/__init__.py index 541b7b7..2128f5e 100644 --- a/flask_oidc/__init__.py +++ b/flask_oidc/__init__.py @@ -130,8 +130,10 @@ def __init__(self, app=None, credentials_store=None, http=None, time=None, if app: self.init_app(app) - if provider: - self.init_provider(app, provider) + # Backwards compatible: When provider argument is ommitted, + # the value would be None, then in method load_secrets, the + # client_secrets will be tried to load... + self.init_provider(app, provider) def __exit__(self, exception_type, exception_value, traceback): self.logout() From c607d20f384a2a7dc9adccd26bfd34da8387da78 Mon Sep 17 00:00:00 2001 From: Harry Kodden Date: Thu, 8 Nov 2018 10:57:27 +0100 Subject: [PATCH 4/7] fix parameter error --- flask_oidc/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flask_oidc/__init__.py b/flask_oidc/__init__.py index 2128f5e..a7e90f3 100644 --- a/flask_oidc/__init__.py +++ b/flask_oidc/__init__.py @@ -133,7 +133,7 @@ def __init__(self, app=None, credentials_store=None, http=None, time=None, # Backwards compatible: When provider argument is ommitted, # the value would be None, then in method load_secrets, the # client_secrets will be tried to load... - self.init_provider(app, provider) + self.init_provider(provider) def __exit__(self, exception_type, exception_value, traceback): self.logout() From 2863bd8e9d4c69380036eeaa8c29c04538b301d9 Mon Sep 17 00:00:00 2001 From: Harry Kodden Date: Thu, 8 Nov 2018 11:18:12 +0100 Subject: [PATCH 5/7] was missing app_context... --- flask_oidc/__init__.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/flask_oidc/__init__.py b/flask_oidc/__init__.py index a7e90f3..0d93063 100644 --- a/flask_oidc/__init__.py +++ b/flask_oidc/__init__.py @@ -133,7 +133,8 @@ def __init__(self, app=None, credentials_store=None, http=None, time=None, # Backwards compatible: When provider argument is ommitted, # the value would be None, then in method load_secrets, the # client_secrets will be tried to load... - self.init_provider(provider) + with app.app_context(): + self.init_provider(provider) def __exit__(self, exception_type, exception_value, traceback): self.logout() @@ -226,7 +227,7 @@ def load_secrets(self, provider): if static_secrets: return _json_loads(open(static_secrets,'r').read()) except Exception as e: - raise Exception("Error reading secrets: {}, error: {}".format(static_secrets, str(e))) + raise Exception("Error reading secrets: {}".format(str(e))) if not provider: raise Exception("No Provider specified") From 32f9644b787d46aff29e672ed4966fe5fbf1f553 Mon Sep 17 00:00:00 2001 From: Harry Kodden Date: Thu, 8 Nov 2018 11:34:52 +0100 Subject: [PATCH 6/7] FIx on valid issuers --- flask_oidc/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/flask_oidc/__init__.py b/flask_oidc/__init__.py index 0d93063..51f7ada 100644 --- a/flask_oidc/__init__.py +++ b/flask_oidc/__init__.py @@ -141,7 +141,7 @@ def __exit__(self, exception_type, exception_value, traceback): self.client_secrets = None - current_app.config['OIDC_VALID_ISSUERS'] = None + current_app.config['OIDC_VALID_ISSUERS'] = [] def init_app(self, app): """ @@ -218,7 +218,7 @@ def init_provider(self, provider): assert isinstance(self.flow, OAuth2WebServerFlow) - current_app.config['OIDC_VALID_ISSUERS'] = self.client_secrets.get('issuer') + current_app.config['OIDC_VALID_ISSUERS'] = (self.client_secrets.get('issuer') or []) def load_secrets(self, provider): From 99e5554d09992dc98ea7a6dad0878df8388aaaca Mon Sep 17 00:00:00 2001 From: Harry Kodden Date: Thu, 8 Nov 2018 11:39:04 +0100 Subject: [PATCH 7/7] Test scripts require google as valid Issuer... --- flask_oidc/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/flask_oidc/__init__.py b/flask_oidc/__init__.py index 51f7ada..da9b447 100644 --- a/flask_oidc/__init__.py +++ b/flask_oidc/__init__.py @@ -141,7 +141,7 @@ def __exit__(self, exception_type, exception_value, traceback): self.client_secrets = None - current_app.config['OIDC_VALID_ISSUERS'] = [] + current_app.config['OIDC_VALID_ISSUERS'] = GOOGLE_ISSUERS def init_app(self, app): """ @@ -218,7 +218,7 @@ def init_provider(self, provider): assert isinstance(self.flow, OAuth2WebServerFlow) - current_app.config['OIDC_VALID_ISSUERS'] = (self.client_secrets.get('issuer') or []) + current_app.config['OIDC_VALID_ISSUERS'] = (self.client_secrets.get('issuer') or GOOGLE_ISSUERS) def load_secrets(self, provider):