diff --git a/flask_oidc/__init__.py b/flask_oidc/__init__.py index 6ea1520..da9b447 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 @@ -103,7 +105,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 +122,27 @@ 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) + # 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... + with app.app_context(): + self.init_provider(provider) + + def __exit__(self, exception_type, exception_value, traceback): + self.logout() + + self.client_secrets = None + + current_app.config['OIDC_VALID_ISSUERS'] = GOOGLE_ISSUERS + def init_app(self, app): """ Do setup that requires a Flask app. @@ -130,11 +150,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') @@ -142,9 +160,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) @@ -169,13 +185,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 +196,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') or GOOGLE_ISSUERS) + + 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: {}".format(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( + 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": self._oidc_callback, + "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( + 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': self._oidc_callback, + '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 +517,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 +682,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 +709,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']: