def init_app(self, provider_metadata_extras=None, client_metadata_extras=None, **kwargs): required_provider_metadata = { 'issuer': self.PROVIDER_BASEURL, 'authorization_endpoint': self.PROVIDER_BASEURL + '/auth', 'jwks_uri': self.PROVIDER_BASEURL + '/jwks' } if provider_metadata_extras: required_provider_metadata.update(provider_metadata_extras) provider_metadata = ProviderMetadata(**required_provider_metadata) required_client_metadata = { 'client_id': self.CLIENT_ID, 'client_secret': 'secret1' } if client_metadata_extras: required_client_metadata.update(client_metadata_extras) client_metadata = ClientMetadata(**required_client_metadata) provider_configurations = { self.PROVIDER_NAME: ProviderConfiguration(provider_metadata=provider_metadata, client_metadata=client_metadata, **kwargs) } authn = OIDCAuthentication(provider_configurations) authn.init_app(self.app) return authn
def provider_metadata(self): return ProviderMetadata( issuer="https://{DOMAIN}/".format(DOMAIN=self.oidc_config.OIDC_DOMAIN,), authorization_endpoint=self.oidc_config.auth_endpoint(), token_endpoint=self.oidc_config.token_endpoint(), userinfo_endpoint=self.oidc_config.userinfo_endpoint(), )
def init_app(self, provider_metadata_extras=None, client_metadata_extras=None, **kwargs): required_provider_metadata = { "issuer": self.PROVIDER_BASEURL, "authorization_endpoint": self.PROVIDER_BASEURL + "/auth", "jwks_uri": self.PROVIDER_BASEURL + "/jwks", } if provider_metadata_extras: required_provider_metadata.update(provider_metadata_extras) provider_metadata = ProviderMetadata(**required_provider_metadata) required_client_metadata = { "client_id": self.CLIENT_ID, "client_secret": "secret1" } if client_metadata_extras: required_client_metadata.update(client_metadata_extras) client_metadata = ClientMetadata(**required_client_metadata) provider_configurations = { self.PROVIDER_NAME: ProviderConfiguration(provider_metadata=provider_metadata, client_metadata=client_metadata, **kwargs) } authn = OIDCAuthentication(provider_configurations) authn.init_app(self.app) return authn
def test_should_register_client_if_not_registered_before( self, post_logout_redirect_uris): registration_endpoint = self.PROVIDER_BASEURL + '/register' provider_metadata = ProviderMetadata( self.PROVIDER_BASEURL, self.PROVIDER_BASEURL + '/auth', self.PROVIDER_BASEURL + '/jwks', registration_endpoint=registration_endpoint) client_metadata = {} if post_logout_redirect_uris: client_metadata[ 'post_logout_redirect_uris'] = post_logout_redirect_uris provider_configurations = { self.PROVIDER_NAME: ProviderConfiguration( provider_metadata=provider_metadata, client_registration_info=ClientRegistrationInfo( **client_metadata)) } authn = OIDCAuthentication(provider_configurations) authn.init_app(self.app) # register logout view to force 'post_logout_redirect_uris' to be included in registration request logout_view_mock = self.get_view_mock() self.app.add_url_rule('/logout', view_func=logout_view_mock) authn.oidc_logout(logout_view_mock) responses.add(responses.POST, registration_endpoint, json={ 'client_id': 'client1', 'client_secret': 'secret1' }) view_mock = self.get_view_mock() with self.app.test_request_context('/'): auth_redirect = authn.oidc_auth(self.PROVIDER_NAME)(view_mock)() self.assert_auth_redirect(auth_redirect) registration_request = json.loads( responses.calls[0].request.body.decode('utf-8')) with self.app.app_context(): full_redirect_uri = flask.url_for( registration_request['redirect_uris'][0]) registration_request['redirect_uris'] = full_redirect_uri expected_post_logout_redirect_uris = post_logout_redirect_uris if post_logout_redirect_uris else [ 'http://{}/logout'.format(self.CLIENT_DOMAIN) ] expected_registration_request = { 'redirect_uris': 'http://{}/redirect_uri'.format(self.CLIENT_DOMAIN), 'post_logout_redirect_uris': expected_post_logout_redirect_uris } assert registration_request == expected_registration_request
def test_should_detect_nonce_mismatch(self, client_mock): client = PyoidcFacade( ProviderConfiguration( provider_metadata=ProviderMetadata(issuer=self.ISSUER), client_metadata=ClientMetadata(client_id=self.CLIENT_ID)), redirect_uri='https://client.example.com/redirect') client.exchange_authorization_code = MagicMock( return_value=self.TOKEN_RESPONSE) auth_request = { 'state': self.AUTH_RESPONSE['state'], 'nonce': 'other_nonce' } with pytest.raises(InvalidIdTokenError): AuthResponseHandler(client).process_auth_response( self.AUTH_RESPONSE, auth_request)
def get_provider_metadata(login_hint_token, mccmnc): client_id = os.getenv('CLIENT_ID') d_url = os.getenv('DISCOVERY_URL') url = '%s?config=true&client_id=%s&login_hint_token=%s&mccmnc=%s' % ( d_url, client_id, login_hint_token, mccmnc) response = requests.get(url) response_json = response.json() if (response_json == {} or response_json['issuer'] == None): return None metadata = ProviderMetadata( issuer=response_json['issuer'], authorization_endpoint=response_json['authorization_endpoint'], jwks_uri=response_json['jwks_uri'], token_endpoint=response_json['token_endpoint'], userinfo_endpoint=response_json['userinfo_endpoint']) return metadata
def create_app(config=None): app = Flask(__name__, instance_relative_config=True) # Load application config from various sources # ------------------------------------------------------------------------------ # 1. Defaults from this package app.config.from_object(DefaultConfig) # 2. From a config.py file in the application directory app.config.from_pyfile(filename=os.path.join(os.getcwd(), "config.py"), silent=True) # 3. From a dynamically configurable file location if os.environ.get('CONFIG_LOCATION'): app.config.from_pyfile(filename=os.environ.get('CONFIG_LOCATION'), silent=False) # 4. Testing config if config: app.config.from_mapping(config) # 5. Load some final computed config # NOTE: This is placed here as it relies on other config values that # may be configured after the user provided config for example. oidc_logout_redirect_uri = os.environ.get( 'OIDC_LOGOUT_REDIRECT_URI', 'https://' + app.config['SERVER_NAME'] + '/logout') oidc_auth_request_params = json.loads( os.environ.get('OIDC_AUTH_REQUEST_PARAMS', '{}')) if oidc_auth_request_params: if app.config['OIDC_EXTRA_AUTH_REQUEST_PARAMS']: app.logger.warning( 'OIDC_EXTRA_AUTH_REQUEST_PARAMS is being overridden by OIDC_AUTH_REQUEST_PARAMS being explicitly set.' ) else: oidc_auth_request_params['scope'] = " ".join( re.split(",| ", app.config['OIDC_SCOPE'])) if app.config['OIDC_EXTRA_AUTH_REQUEST_PARAMS']: oidc_auth_request_params.update( app.config['OIDC_EXTRA_AUTH_REQUEST_PARAMS']) app.config.from_mapping({ 'OIDC_LOGOUT_REDIRECT_URI': oidc_logout_redirect_uri, 'OIDC_CLIENT_METADATA': { 'client_id': app.config['OIDC_CLIENT_ID'], 'client_secret': app.config['OIDC_CLIENT_SECRET'], 'post_logout_redirect_uris': str.split(oidc_logout_redirect_uri, ",") }, 'OIDC_AUTH_REQUEST_PARAMS': oidc_auth_request_params, }) # Initialize OpenID Connect extension # ------------------------------------------------------------------------------ # The client metadata will be consumed no matter what... # https://github.com/zamzterz/Flask-pyoidc#dynamic-provider-configuration client_metadata = ClientMetadata(**app.config['OIDC_CLIENT_METADATA']) # ... but if explicit OIDC provider information is provided, we use that # instead of the information dynamically provided by the # .well-known/openid-configuration endpoint. if app.config['OIDC_PROVIDER_METADATA']: provider_metadata = ProviderMetadata( **app.config['OIDC_PROVIDER_METADATA']) provider = ProviderConfiguration( provider_metadata=provider_metadata, client_metadata=client_metadata, auth_request_params=app.config['OIDC_AUTH_REQUEST_PARAMS'], ) else: provider = ProviderConfiguration( issuer=app.config['OIDC_ISSUER'], client_metadata=client_metadata, auth_request_params=app.config['OIDC_AUTH_REQUEST_PARAMS'], ) auth = OIDCAuthentication( provider_configurations={ 'default': provider, }, app=app, ) # The /health endpoint returns a JSON string like... # {"hostname": "a3731af16461", "status": "success", "timestamp": 1551186453.8854501, "results": []} health = HealthCheck(app, "/health") # If an OAuth error response is received, either in the authentication or # token response, it will be passed to the "error view". @auth.error_view def error(error=None, error_description=None): return jsonify({'error': error, 'message': error_description}) @app.route('/') def index(): """ If a user tries to access this application directly, just redirect them to Discourse. :return: Redirect to the configurated DISCOURSE_URL """ return redirect(app.config.get('DISCOURSE_URL'), 302) @app.route('/sso/login') def payload_check(): """ Verify the payload and signature coming from a Discourse server and if correct redirect to the authentication page after saving the nonce in the session as discourse_nonce. :return: The redirection page to the authentication page """ # Get payload and signature from Discourse request payload = request.args.get('sso', '') signature = request.args.get('sig', '') if not payload or not signature: app.logger.info( '/sso/login -> 400: missing payload="%s" or signature="%s"', payload, signature) abort(400) app.logger.debug('Request to login with payload="%s" signature="%s"', payload, signature) app.logger.debug('Session Secret Key: %s', app.secret_key) app.logger.debug('SSO Secret Key: %s', app.config.get('DISCOURSE_SECRET_KEY')) # Calculate and compare request signature dig = hmac.new( app.config.get('DISCOURSE_SECRET_KEY', '').encode('utf-8'), payload.encode('utf-8'), hashlib.sha256).hexdigest() app.logger.debug('Calculated hash: %s', dig) if dig != signature: app.logger.info( '/sso/login -> 400: dig / signature mismatch. dig="%s" and signature="%s"', dig, signature) abort(400) # Decode the payload and store in session decoded_msg = base64.b64decode(payload).decode('utf-8') session[ 'discourse_nonce'] = decoded_msg # This can't just be 'nonce' as Flask-pyoidc will steamroll it # Redirect to authorization endpoint return redirect(url_for('sso_auth')) @app.route('/sso/auth') @auth.oidc_auth('default') def sso_auth(): """ Read the user attributes provided by Flask-pyoidc and create the payload to send to Discourse. :return: The redirection page to Discourse """ # Check to make sure we have a valid session if 'discourse_nonce' not in session: app.logger.info( '/sso/auth -> 403: discourse_nonce not found in session, arriving here without coming from /sso/login?' ) abort(403) attribute_map = app.config.get('USERINFO_SSO_MAP') sso_attributes = {} userinfo = session['userinfo'] # Check if the provided userinfo should be used to set information to be # passed to discourse. Do it by checking if the userinfo field is... # 1. explicitly mapped using the provided map # 2. if it can match one of the known attributes with discourse_ prefixed # 3. if it can match one of the known attributes directly for userinfo_key, userinfo_value in userinfo.items(): attribute_key = attribute_map.get(userinfo_key) if attribute_key: pass elif userinfo_key in [ "discourse_" + attr for attr in ALL_ATTRIBUTES ]: attribute_key = userinfo_key[len("discourse_"):] elif userinfo_key in ALL_ATTRIBUTES: attribute_key = userinfo_key if attribute_key: if attribute_key in BOOL_ATTRIBUTES: userinfo_value = "false" if str.lower( str(userinfo_value)) in ['false', 'f', '0'] else "true" sso_attributes[attribute_key] = userinfo_value # Check if we have a default value that should be used default_sso_attributes = app.config.get('DEFAULT_SSO_ATTRIBUTES') for default_attribute_key, default_attribute_value in default_sso_attributes.items( ): if default_attribute_key not in sso_attributes: sso_attributes[default_attribute_key] = default_attribute_value # Check if we got the required attributes for required_attribute in REQUIRED_ATTRIBUTES: if not sso_attributes.get(required_attribute): app.logger.info( f'/sso/auth -> 403: {required_attribute} not found in userinfo: ' + json.dumps(session['userinfo'])) abort(403) # All systems are go! app.logger.debug( f'Authenticating "{sso_attributes.get("external_id")}", named "{sso_attributes.get("name")}" with email: "{sso_attributes.get("email")}"' ) # Construct the response inner query parameters query = session['discourse_nonce'] for sso_attribute_key, sso_attribute_value in sso_attributes.items(): query += f'&{sso_attribute_key}={quote(str(sso_attribute_value))}' app.logger.debug('Query string to return: %s', query) # Encode response query_b64 = base64.b64encode(query.encode('utf-8')) app.logger.debug('Base64 query string to return: %s', query_b64) # Build URL-safe response query_urlenc = quote(query_b64) app.logger.debug('URLEnc query string to return: %s', query_urlenc) # Generate signature for response sig = hmac.new( app.config.get('DISCOURSE_SECRET_KEY').encode('utf-8'), query_b64, hashlib.sha256, ).hexdigest() app.logger.debug('Signature: %s', sig) # Build redirect URL redirect_url = (app.config.get('DISCOURSE_URL') + '/session/sso_login?' 'sso=' + query_urlenc + '&sig=' + sig) # Redirect back to Discourse return redirect(redirect_url) @app.route('/logout') @auth.oidc_logout def logout(): """ Handle logging a user out. Flask-pyoidc does the heavy lifting here. :return: Redirect to the application index """ return redirect(url_for('index'), 302) @app.errorhandler(403) def attribute_not_provided(error): """ Render a custom error page in case the IdP authenticate the user but does not provide the requested attributes :type error: object """ app.logger.info(f'403: error: "{error}"') return render_template('403.html'), 403 return app
class TestPyoidcFacade(object): PROVIDER_BASEURL = "http://rp.example.com" PROVIDER_METADATA = ProviderMetadata(PROVIDER_BASEURL, PROVIDER_BASEURL + "/auth", PROVIDER_BASEURL + "/jwks") CLIENT_METADATA = ClientMetadata("client1", "secret1") CLIENT_DOMAIN = "client.example.com" REDIRECT_URI = "redirect_uri" FULL_REDIRECT_URI = "http://client.example.com/redirect_uri" @pytest.fixture(autouse=True) def create_flask_app(self): self.app = Flask(__name__) self.app.add_url_rule("/redirect_uri", "redirect_uri") self.app.config.update({ "SERVER_NAME": self.CLIENT_DOMAIN, "SECRET_KEY": "test_key" }) def test_registered_client_metadata_is_forwarded_to_pyoidc(self): config = ProviderConfiguration( provider_metadata=self.PROVIDER_METADATA, client_metadata=self.CLIENT_METADATA, ) facade = PyoidcFacade(config, self.REDIRECT_URI) assert facade._client.registration_response def test_no_registered_client_metadata_is_handled(self): config = ProviderConfiguration( provider_metadata=self.PROVIDER_METADATA, client_registration_info=ClientRegistrationInfo(), ) facade = PyoidcFacade(config, self.REDIRECT_URI) assert not facade._client.registration_response def test_is_registered(self): unregistered = ProviderConfiguration( provider_metadata=self.PROVIDER_METADATA, client_registration_info=ClientRegistrationInfo(), ) registered = ProviderConfiguration( provider_metadata=self.PROVIDER_METADATA, client_metadata=self.CLIENT_METADATA, ) assert PyoidcFacade(unregistered, self.REDIRECT_URI).is_registered() is False assert PyoidcFacade(registered, self.REDIRECT_URI).is_registered() is True @responses.activate def test_register(self): registration_endpoint = self.PROVIDER_BASEURL + "/register" responses.add(responses.POST, registration_endpoint, json=self.CLIENT_METADATA.to_dict()) provider_metadata = self.PROVIDER_METADATA.copy( registration_endpoint=registration_endpoint) unregistered = ProviderConfiguration( provider_metadata=provider_metadata, client_registration_info=ClientRegistrationInfo(), ) facade = PyoidcFacade(unregistered, self.REDIRECT_URI) facade.register() assert facade.is_registered() is True def test_authentication_request(self): extra_user_auth_params = {"foo": "bar", "abc": "xyz"} config = ProviderConfiguration( provider_metadata=self.PROVIDER_METADATA, client_metadata=self.CLIENT_METADATA, auth_request_params=extra_user_auth_params, ) state = "test_state" nonce = "test_nonce" facade = PyoidcFacade(config, self.REDIRECT_URI) extra_lib_auth_params = {"foo": "baz", "qwe": "rty"} auth_request = facade.authentication_request(state, nonce, self.FULL_REDIRECT_URI, extra_lib_auth_params) assert auth_request.startswith( self.PROVIDER_METADATA["authorization_endpoint"]) auth_request_params = dict(parse_qsl(urlparse(auth_request).query)) expected_auth_params = { "scope": "openid", "response_type": "code", "client_id": self.CLIENT_METADATA["client_id"], "redirect_uri": self.FULL_REDIRECT_URI, "state": state, "nonce": nonce, } expected_auth_params.update(extra_user_auth_params) expected_auth_params.update(extra_lib_auth_params) assert auth_request_params == expected_auth_params def test_parse_authentication_response(self): facade = PyoidcFacade( ProviderConfiguration( provider_metadata=self.PROVIDER_METADATA, client_metadata=self.CLIENT_METADATA, ), self.REDIRECT_URI, ) auth_code = "auth_code-1234" state = "state-1234" auth_response = AuthorizationResponse(**{ "state": state, "code": auth_code }) parsed_auth_response = facade.parse_authentication_response( auth_response.to_dict()) assert isinstance(parsed_auth_response, AuthorizationResponse) assert parsed_auth_response.to_dict() == auth_response.to_dict() def test_parse_authentication_response_handles_error_response(self): facade = PyoidcFacade( ProviderConfiguration( provider_metadata=self.PROVIDER_METADATA, client_metadata=self.CLIENT_METADATA, ), self.REDIRECT_URI, ) error_response = AuthorizationErrorResponse(**{ "error": "invalid_request", "state": "state-1234" }) parsed_auth_response = facade.parse_authentication_response( error_response) assert isinstance(parsed_auth_response, AuthorizationErrorResponse) assert parsed_auth_response.to_dict() == error_response.to_dict() @responses.activate def test_parse_authentication_response_preserves_id_token_jwt(self): facade = PyoidcFacade( ProviderConfiguration( provider_metadata=self.PROVIDER_METADATA, client_metadata=self.CLIENT_METADATA, ), self.REDIRECT_URI, ) state = "state-1234" now = int(time.time()) id_token, id_token_signing_key = signed_id_token({ "iss": self.PROVIDER_METADATA["issuer"], "sub": "test_sub", "aud": "client1", "exp": now + 1, "iat": now, }) responses.add( responses.GET, self.PROVIDER_METADATA["jwks_uri"], json={"keys": [id_token_signing_key.serialize()]}, ) auth_response = AuthorizationResponse(**{ "state": state, "id_token": id_token }) parsed_auth_response = facade.parse_authentication_response( auth_response) assert isinstance(parsed_auth_response, AuthorizationResponse) assert parsed_auth_response["state"] == state assert parsed_auth_response["id_token_jwt"] == id_token @responses.activate def test_token_request(self): token_endpoint = self.PROVIDER_BASEURL + "/token" now = int(time.time()) id_token_claims = { "iss": self.PROVIDER_METADATA["issuer"], "sub": "test_user", "aud": [self.CLIENT_METADATA["client_id"]], "exp": now + 1, "iat": now, "nonce": "test_nonce", } id_token_jwt, id_token_signing_key = signed_id_token(id_token_claims) token_response = AccessTokenResponse(access_token="test_access_token", token_type="Bearer", id_token=id_token_jwt) responses.add(responses.POST, token_endpoint, json=token_response.to_dict()) provider_metadata = self.PROVIDER_METADATA.copy( token_endpoint=token_endpoint) facade = PyoidcFacade( ProviderConfiguration( provider_metadata=provider_metadata, client_metadata=self.CLIENT_METADATA, ), self.REDIRECT_URI, ) auth_code = "auth_code-1234" responses.add( responses.GET, self.PROVIDER_METADATA["jwks_uri"], json={"keys": [id_token_signing_key.serialize()]}, ) with self.app.app_context(): token_response = facade.token_request(auth_code) assert isinstance(token_response, AccessTokenResponse) expected_token_response = token_response.to_dict() expected_token_response["id_token"] = id_token_claims expected_token_response["id_token_jwt"] = id_token_jwt assert token_response.to_dict() == expected_token_response token_request = dict(parse_qsl(responses.calls[0].request.body)) expected_token_request = { "grant_type": "authorization_code", "code": auth_code, "redirect_uri": self.FULL_REDIRECT_URI } assert token_request == expected_token_request @responses.activate def test_token_request_handles_error_response(self): token_endpoint = self.PROVIDER_BASEURL + "/token" token_response = TokenErrorResponse( error="invalid_request", error_description="test error description") responses.add(responses.POST, token_endpoint, json=token_response.to_dict(), status=400) provider_metadata = self.PROVIDER_METADATA.copy( token_endpoint=token_endpoint) facade = PyoidcFacade( ProviderConfiguration( provider_metadata=provider_metadata, client_metadata=self.CLIENT_METADATA, ), self.REDIRECT_URI, ) with self.app.app_context(): assert facade.token_request("1234") == token_response def test_token_request_handles_missing_provider_token_endpoint(self): facade = PyoidcFacade( ProviderConfiguration( provider_metadata=self.PROVIDER_METADATA, client_metadata=self.CLIENT_METADATA, ), self.REDIRECT_URI, ) assert facade.token_request("1234") is None @pytest.mark.parametrize("userinfo_http_method", ["GET", "POST"]) @responses.activate def test_configurable_userinfo_endpoint_method_is_used( self, userinfo_http_method): userinfo_endpoint = self.PROVIDER_BASEURL + "/userinfo" userinfo_response = OpenIDSchema(sub="user1") responses.add(userinfo_http_method, userinfo_endpoint, json=userinfo_response.to_dict()) provider_metadata = self.PROVIDER_METADATA.copy( userinfo_endpoint=userinfo_endpoint) facade = PyoidcFacade( ProviderConfiguration( provider_metadata=provider_metadata, client_metadata=self.CLIENT_METADATA, userinfo_http_method=userinfo_http_method, ), self.REDIRECT_URI, ) assert facade.userinfo_request("test_token") == userinfo_response def test_no_userinfo_request_is_made_if_no_userinfo_http_method_is_configured( self): facade = PyoidcFacade( ProviderConfiguration( provider_metadata=self.PROVIDER_METADATA, client_metadata=self.CLIENT_METADATA, userinfo_http_method=None, ), self.REDIRECT_URI, ) assert facade.userinfo_request("test_token") is None def test_no_userinfo_request_is_made_if_no_userinfo_endpoint_is_configured( self): facade = PyoidcFacade( ProviderConfiguration( provider_metadata=self.PROVIDER_METADATA, client_metadata=self.CLIENT_METADATA, ), self.REDIRECT_URI, ) assert facade.userinfo_request("test_token") is None def test_no_userinfo_request_is_made_if_no_access_token(self): provider_metadata = self.PROVIDER_METADATA.copy( userinfo_endpoint=self.PROVIDER_BASEURL + "/userinfo") facade = PyoidcFacade( ProviderConfiguration( provider_metadata=provider_metadata, client_metadata=self.CLIENT_METADATA, ), self.REDIRECT_URI, ) assert facade.userinfo_request(None) is None
class TestPyoidcFacade(object): PROVIDER_BASEURL = 'https://op.example.com' PROVIDER_METADATA = ProviderMetadata(PROVIDER_BASEURL, PROVIDER_BASEURL + '/auth', PROVIDER_BASEURL + '/jwks') CLIENT_METADATA = ClientMetadata('client1', 'secret1') def test_registered_client_metadata_is_forwarded_to_pyoidc(self): config = ProviderConfiguration( provider_metadata=self.PROVIDER_METADATA, client_metadata=self.CLIENT_METADATA) facade = PyoidcFacade(config, REDIRECT_URI) assert facade._client.registration_response def test_no_registered_client_metadata_is_handled(self): config = ProviderConfiguration( provider_metadata=self.PROVIDER_METADATA, client_registration_info=ClientRegistrationInfo()) facade = PyoidcFacade(config, REDIRECT_URI) assert not facade._client.registration_response def test_is_registered(self): unregistered = ProviderConfiguration( provider_metadata=self.PROVIDER_METADATA, client_registration_info=ClientRegistrationInfo()) registered = ProviderConfiguration( provider_metadata=self.PROVIDER_METADATA, client_metadata=self.CLIENT_METADATA) assert PyoidcFacade(unregistered, REDIRECT_URI).is_registered() is False assert PyoidcFacade(registered, REDIRECT_URI).is_registered() is True @responses.activate def test_register(self): registration_endpoint = self.PROVIDER_BASEURL + '/register' responses.add(responses.POST, registration_endpoint, json=self.CLIENT_METADATA.to_dict()) provider_metadata = self.PROVIDER_METADATA.copy( registration_endpoint=registration_endpoint) unregistered = ProviderConfiguration( provider_metadata=provider_metadata, client_registration_info=ClientRegistrationInfo()) facade = PyoidcFacade(unregistered, REDIRECT_URI) facade.register() assert facade.is_registered() is True def test_authentication_request(self): extra_user_auth_params = {'foo': 'bar', 'abc': 'xyz'} config = ProviderConfiguration( provider_metadata=self.PROVIDER_METADATA, client_metadata=self.CLIENT_METADATA, auth_request_params=extra_user_auth_params) state = 'test_state' nonce = 'test_nonce' facade = PyoidcFacade(config, REDIRECT_URI) extra_lib_auth_params = {'foo': 'baz', 'qwe': 'rty'} auth_request = facade.authentication_request(state, nonce, extra_lib_auth_params) assert auth_request.startswith( self.PROVIDER_METADATA['authorization_endpoint']) auth_request_params = dict(parse_qsl(urlparse(auth_request).query)) expected_auth_params = { 'scope': 'openid', 'response_type': 'code', 'client_id': self.CLIENT_METADATA['client_id'], 'redirect_uri': REDIRECT_URI, 'state': state, 'nonce': nonce } expected_auth_params.update(extra_user_auth_params) expected_auth_params.update(extra_lib_auth_params) assert auth_request_params == expected_auth_params def test_parse_authentication_response(self): facade = PyoidcFacade( ProviderConfiguration(provider_metadata=self.PROVIDER_METADATA, client_metadata=self.CLIENT_METADATA), REDIRECT_URI) auth_code = 'auth_code-1234' state = 'state-1234' auth_response = AuthorizationResponse(**{ 'state': state, 'code': auth_code }) parsed_auth_response = facade.parse_authentication_response( auth_response.to_dict()) assert isinstance(parsed_auth_response, AuthorizationResponse) assert parsed_auth_response.to_dict() == auth_response.to_dict() def test_parse_authentication_response_handles_error_response(self): facade = PyoidcFacade( ProviderConfiguration(provider_metadata=self.PROVIDER_METADATA, client_metadata=self.CLIENT_METADATA), REDIRECT_URI) error_response = AuthorizationErrorResponse(**{ 'error': 'invalid_request', 'state': 'state-1234' }) parsed_auth_response = facade.parse_authentication_response( error_response) assert isinstance(parsed_auth_response, AuthorizationErrorResponse) assert parsed_auth_response.to_dict() == error_response.to_dict() @responses.activate def test_parse_authentication_response_preserves_id_token_jwt(self): facade = PyoidcFacade( ProviderConfiguration(provider_metadata=self.PROVIDER_METADATA, client_metadata=self.CLIENT_METADATA), REDIRECT_URI) state = 'state-1234' now = int(time.time()) id_token, id_token_signing_key = signed_id_token({ 'iss': self.PROVIDER_METADATA['issuer'], 'sub': 'test_sub', 'aud': 'client1', 'exp': now + 1, 'iat': now }) responses.add(responses.GET, self.PROVIDER_METADATA['jwks_uri'], json={'keys': [id_token_signing_key.serialize()]}) auth_response = AuthorizationResponse(**{ 'state': state, 'id_token': id_token }) parsed_auth_response = facade.parse_authentication_response( auth_response) assert isinstance(parsed_auth_response, AuthorizationResponse) assert parsed_auth_response['state'] == state assert parsed_auth_response['id_token_jwt'] == id_token @pytest.mark.parametrize( 'request_func,expected_token_request', [(lambda facade: facade.exchange_authorization_code('auth-code'), { 'grant_type': 'authorization_code', 'code': 'auth-code', 'redirect_uri': REDIRECT_URI }), (lambda facade: facade.refresh_token('refresh-token'), { 'grant_type': 'refresh_token', 'refresh_token': 'refresh-token', 'redirect_uri': REDIRECT_URI })]) @responses.activate def test_token_request(self, request_func, expected_token_request): token_endpoint = self.PROVIDER_BASEURL + '/token' now = int(time.time()) id_token_claims = { 'iss': self.PROVIDER_METADATA['issuer'], 'sub': 'test_user', 'aud': [self.CLIENT_METADATA['client_id']], 'exp': now + 1, 'iat': now, 'nonce': 'test_nonce' } id_token_jwt, id_token_signing_key = signed_id_token(id_token_claims) token_response = AccessTokenResponse(access_token='test_access_token', token_type='Bearer', id_token=id_token_jwt) responses.add(responses.POST, token_endpoint, json=token_response.to_dict()) provider_metadata = self.PROVIDER_METADATA.copy( token_endpoint=token_endpoint) facade = PyoidcFacade( ProviderConfiguration(provider_metadata=provider_metadata, client_metadata=self.CLIENT_METADATA), REDIRECT_URI) responses.add(responses.GET, self.PROVIDER_METADATA['jwks_uri'], json={'keys': [id_token_signing_key.serialize()]}) token_response = request_func(facade) assert isinstance(token_response, AccessTokenResponse) expected_token_response = token_response.to_dict() expected_token_response['id_token'] = id_token_claims expected_token_response['id_token_jwt'] = id_token_jwt assert token_response.to_dict() == expected_token_response token_request = dict(parse_qsl(responses.calls[0].request.body)) assert token_request == expected_token_request @responses.activate def test_token_request_handles_error_response(self): token_endpoint = self.PROVIDER_BASEURL + '/token' token_response = TokenErrorResponse( error='invalid_request', error_description='test error description') responses.add(responses.POST, token_endpoint, json=token_response.to_dict(), status=400) provider_metadata = self.PROVIDER_METADATA.copy( token_endpoint=token_endpoint) facade = PyoidcFacade( ProviderConfiguration(provider_metadata=provider_metadata, client_metadata=self.CLIENT_METADATA), REDIRECT_URI) assert facade.exchange_authorization_code('1234') == token_response def test_token_request_handles_missing_provider_token_endpoint(self): facade = PyoidcFacade( ProviderConfiguration(provider_metadata=self.PROVIDER_METADATA, client_metadata=self.CLIENT_METADATA), REDIRECT_URI) assert facade.exchange_authorization_code('1234') is None @pytest.mark.parametrize('userinfo_http_method', ['GET', 'POST']) @responses.activate def test_configurable_userinfo_endpoint_method_is_used( self, userinfo_http_method): userinfo_endpoint = self.PROVIDER_BASEURL + '/userinfo' userinfo_response = OpenIDSchema(sub='user1') responses.add(userinfo_http_method, userinfo_endpoint, json=userinfo_response.to_dict()) provider_metadata = self.PROVIDER_METADATA.copy( userinfo_endpoint=userinfo_endpoint) facade = PyoidcFacade( ProviderConfiguration(provider_metadata=provider_metadata, client_metadata=self.CLIENT_METADATA, userinfo_http_method=userinfo_http_method), REDIRECT_URI) assert facade.userinfo_request('test_token') == userinfo_response def test_no_userinfo_request_is_made_if_no_userinfo_http_method_is_configured( self): facade = PyoidcFacade( ProviderConfiguration(provider_metadata=self.PROVIDER_METADATA, client_metadata=self.CLIENT_METADATA, userinfo_http_method=None), REDIRECT_URI) assert facade.userinfo_request('test_token') is None def test_no_userinfo_request_is_made_if_no_userinfo_endpoint_is_configured( self): facade = PyoidcFacade( ProviderConfiguration(provider_metadata=self.PROVIDER_METADATA, client_metadata=self.CLIENT_METADATA), REDIRECT_URI) assert facade.userinfo_request('test_token') is None def test_no_userinfo_request_is_made_if_no_access_token(self): provider_metadata = self.PROVIDER_METADATA.copy( userinfo_endpoint=self.PROVIDER_BASEURL + '/userinfo') facade = PyoidcFacade( ProviderConfiguration(provider_metadata=provider_metadata, client_metadata=self.CLIENT_METADATA), REDIRECT_URI) assert facade.userinfo_request(None) is None
if app.config.get("AUTH_ENABLED") == "OIDC": app.logger.info("AUTHENTICATION ENABLED WITH %s" % app.config.get("AUTH_ENABLED")) if not os.environ.get("GPM_OIDC_REDIRECT_DOMAIN"): app.logger.error( "Authentication is enabled with OIDC but GPM_OIDC_REDIRECT_DOMAIN environment variable has not been set." ) provider_metadata = ProviderMetadata( issuer=os.environ.get("GPM_OIDC_ISSUER"), authorization_endpoint=os.environ.get( "GPM_OIDC_AUTHORIZATION_ENDPOINT"), jwks_uri=os.environ.get("GPM_OIDC_JWKS_URI"), token_endpoint=os.environ.get("GPM_OIDC_TOKEN_ENDPOINT"), token_introspection_endpoint=os.environ.get( "GPM_OIDC_INTROSPECTION_ENDPOINT"), userinfo_endpoint=os.environ.get("GPM_OIDC_USERINFO_ENDPOINT"), end_session_endpoint=os.environ.get("GPM_OIDC_END_SESSION_ENDPOINT"), ) provider_config = ProviderConfiguration( issuer=os.environ.get("GPM_OIDC_ISSUER"), provider_metadata=provider_metadata, session_refresh_interval_seconds=10, client_metadata=ClientMetadata( client_id=os.environ.get("GPM_OIDC_CLIENT_ID"), client_secret=os.environ.get("GPM_OIDC_CLIENT_SECRET"), ), )
from cognitodemo.access import AwsAccesser from cognitodemo.mfa import get_mfa_challenge, verify_mfa_challenge, user_has_software_token_mfa app = Flask(__name__) app.config.from_envvar('COGNITO_DEMO_SETTINGS') app.config.update({ 'OIDC_REDIRECT_URI': 'http://localhost:5000/redirect_uri', 'DEBUG': True }) issuer = f'https://{app.config["PROVIDER_NAME"]}' cognito_config = ProviderConfiguration( provider_metadata=ProviderMetadata( issuer=issuer, authorization_endpoint=f'{app.config["COGNITO_URL"]}/oauth2/authorize', jwks_uri=f'{issuer}/.well-known/jwks.json', token_endpoint=f'{app.config["COGNITO_URL"]}/oauth2/token', ), client_metadata=ClientMetadata(app.config["CLIENT_ID"], app.config["CLIENT_SECRET"]), auth_request_params={ 'scope': ['openid', 'aws.cognito.signin.user.admin' ] # scope required to update MFA for logged-in user }) auth = OIDCAuthentication({'cognito': cognito_config}) aws_accesser = AwsAccesser(app.config['AWS_ACCOUNT_ID'], app.config['IDENTITY_POOL_ID'], app.config['PROVIDER_NAME'])
def provider_metadata(self, **kwargs): return ProviderMetadata(issuer='', authorization_endpoint='', jwks_uri='', **kwargs)