def test_token_error_reponse_returns_default_error_if_no_error_view_set( self): token_endpoint = ISSUER + '/token' error_response = { 'error': 'invalid_request', 'error_description': 'test error' } responses.add(responses.POST, token_endpoint, body=json.dumps(error_response), content_type='application/json') authn = OIDCAuthentication(self.app, provider_configuration_info={ 'issuer': ISSUER, 'token_endpoint': token_endpoint }, client_registration_info=dict( client_id='abc', client_secret='foo')) state = 'test_tate' with self.app.test_request_context('/redirect_uri?code=foo&state=' + state): flask.session['state'] = state response = authn._handle_authentication_response() assert response == "Something went wrong with the authentication, please try to login again."
def test_token_error_reponse_calls_to_error_view_if_set(self): token_endpoint = ISSUER + '/token' error_response = { 'error': 'invalid_request', 'error_description': 'test error' } responses.add(responses.POST, token_endpoint, body=json.dumps(error_response), content_type='application/json') authn = OIDCAuthentication(self.app, provider_configuration_info={ 'issuer': ISSUER, 'token_endpoint': token_endpoint }, client_registration_info=dict( client_id='abc', client_secret='foo')) error_view_mock = MagicMock() authn._error_view = error_view_mock state = 'test_tate' with self.app.test_request_context('/redirect_uri?code=foo&state=' + state): flask.session['state'] = state authn._handle_authentication_response() error_view_mock.assert_called_with(**error_response)
def test_no_userinfo_request_is_done_if_no_userinfo_endpoint_method_is_specified(self): state = 'state' authn = OIDCAuthentication(self.app, provider_configuration_info={'issuer': ISSUER}, client_registration_info={'client_id': 'foo'}, userinfo_endpoint_method=None) userinfo_request_mock = MagicMock() authn.client.do_user_info_request = userinfo_request_mock authn._do_userinfo_request(state, None) assert not userinfo_request_mock.called
def test_authenticatate_with_extra_request_parameters(self): extra_params = {"foo": "bar", "abc": "xyz"} authn = OIDCAuthentication(self.app, provider_configuration_info={'issuer': ISSUER}, client_registration_info={'client_id': 'foo'}, extra_request_args=extra_params) with self.app.test_request_context('/'): a = authn._authenticate() request_params = dict(parse_qsl(urlparse(a.location).query)) assert set(extra_params.items()).issubset(set(request_params.items()))
def test_authenticatate_with_extra_request_parameters(self): extra_params = {"foo": "bar", "abc": "xyz"} authn = OIDCAuthentication( self.app, provider_configuration_info={'issuer': ISSUER}, client_registration_info={'client_id': 'foo'}, extra_request_args=extra_params) with self.app.test_request_context('/'): a = authn._authenticate() request_params = dict(parse_qsl(urlparse(a.location).query)) assert set(extra_params.items()).issubset(set(request_params.items()))
def test_no_userinfo_request_is_done_if_no_userinfo_endpoint_method_is_specified( self): state = 'state' authn = OIDCAuthentication( self.app, provider_configuration_info={'issuer': ISSUER}, client_registration_info={'client_id': 'foo'}, userinfo_endpoint_method=None) userinfo_request_mock = MagicMock() authn.client.do_user_info_request = userinfo_request_mock authn._do_userinfo_request(state, None) assert not userinfo_request_mock.called
def test_session_expiration_set_to_id_token_exp(self): token_endpoint = ISSUER + '/token' userinfo_endpoint = ISSUER + '/userinfo' exp_time = 10 epoch_int = int(time.mktime(datetime(2017, 1, 1).timetuple())) id_token = IdToken( **{ 'sub': 'sub1', 'iat': epoch_int, 'iss': ISSUER, 'aud': 'foo', 'nonce': 'test', 'exp': epoch_int + exp_time }) token_response = { 'access_token': 'test', 'token_type': 'Bearer', 'id_token': id_token.to_jwt() } userinfo_response = {'sub': 'sub1'} responses.add(responses.POST, token_endpoint, body=json.dumps(token_response), content_type='application/json') responses.add(responses.POST, userinfo_endpoint, body=json.dumps(userinfo_response), content_type='application/json') authn = OIDCAuthentication( self.app, provider_configuration_info={ 'issuer': ISSUER, 'token_endpoint': token_endpoint, 'userinfo_endpoint': userinfo_endpoint }, client_registration_info={ 'client_id': 'foo', 'client_secret': 'foo' }, ) self.app.config.update({'SESSION_PERMANENT': True}) with self.app.test_request_context( '/redirect_uri?state=test&code=test'): flask.session['destination'] = '/' flask.session['state'] = 'test' flask.session['nonce'] = 'test' flask.session['id_token'] = id_token.to_dict() flask.session['id_token_jwt'] = id_token.to_jwt() authn._handle_authentication_response() assert flask.session.permanent is True assert int(flask.session.permanent_session_lifetime) == exp_time
def test_dont_reauthenticate_with_valid_id_token(self): authn = OIDCAuthentication(self.app, provider_configuration_info={'issuer': ISSUER}, client_registration_info={'client_id': 'foo'}) client_mock = MagicMock() callback_mock = MagicMock() callback_mock.__name__ = 'test_callback' # required for Python 2 authn.client = client_mock with self.app.test_request_context('/'): flask.session['destination'] = '/' flask.session['id_token'] = {'exp': time.time() + 25} authn.oidc_auth(callback_mock)() assert not client_mock.construct_AuthorizationRequest.called assert callback_mock.called is True
def test_reauthenticate_if_no_session(self): authn = OIDCAuthentication( self.app, provider_configuration_info={'issuer': ISSUER}, client_registration_info={'client_id': 'foo'}) client_mock = MagicMock() callback_mock = MagicMock() callback_mock.__name__ = 'test_callback' # required for Python 2 authn.client = client_mock with self.app.test_request_context('/'): authn.oidc_auth(callback_mock)() assert client_mock.construct_AuthorizationRequest.called assert not callback_mock.called
def test_dont_reauthenticate_with_valid_id_token(self): authn = OIDCAuthentication( self.app, provider_configuration_info={'issuer': ISSUER}, client_registration_info={'client_id': 'foo'}) client_mock = MagicMock() callback_mock = MagicMock() callback_mock.__name__ = 'test_callback' # required for Python 2 authn.client = client_mock with self.app.test_request_context('/'): flask.session['destination'] = '/' flask.session['access_token'] = 'test token' authn.oidc_auth(callback_mock)() assert not client_mock.construct_AuthorizationRequest.called assert callback_mock.called is True
def auth(self, app): """ Creates the OIDCAuthentication object to be used to protect routes. Authentication is requested with the following audiences: - lando-api: The LANDO_API_OIDC_IDENTIFIER environment variable will be included as an audience. This allows lando-api to verify that tokens created by lando-ui were intended to be used by the api. Authentication is requested with the following scopes: - openid - Permission to get a unique identifier for the user. This also permits querying Auth0 at https://OIDC_DOMAIN/userinfo for additional user information. - email - Permission to get the user's email address. - profile - Permission to get additional information about the user such as their real name, picture url, and LDAP information. """ oidc = OIDCAuthentication( app, provider_configuration_info=self.provider_info(), client_registration_info=self.client_info(), extra_request_args={ "audience": [self.oidc_config.lando_api_oidc_id()], 'scope': ['openid', 'profile', 'email'] }) return oidc
def get_auth(app): auth = OIDCAuthentication( app, issuer=app.config['OIDC_ISSUER'], client_registration_info=app.config['OIDC_CLIENT_CONFIG'], ) return auth
def auth(self, app): o = OIDCAuthentication( app, provider_configuration_info=self.provider_info(), client_registration_info=self.client_info()) return o
def test_logout_handles_provider_without_end_session_endpoint(self): post_logout_uri = 'https://client.example.com/post_logout' authn = OIDCAuthentication(self.app, provider_configuration_info={'issuer': ISSUER}, client_registration_info={'client_id': 'foo', 'post_logout_redirect_uris': [post_logout_uri]}) id_token = IdToken(**{'sub': 'sub1', 'nonce': 'nonce'}) with self.app.test_request_context('/logout'): flask.session['access_token'] = 'abcde' flask.session['userinfo'] = {'foo': 'bar', 'abc': 'xyz'} flask.session['id_token'] = id_token.to_dict() flask.session['id_token_jwt'] = id_token.to_jwt() end_session_redirect = authn._logout() assert all(k not in flask.session for k in ['access_token', 'userinfo', 'id_token', 'id_token_jwt']) assert end_session_redirect is None
def test_oidc_logout_redirects_to_provider(self): end_session_endpoint = 'https://provider.example.com/end_session' post_logout_uri = 'https://client.example.com/post_logout' authn = OIDCAuthentication(self.app, provider_configuration_info={'issuer': ISSUER, 'end_session_endpoint': end_session_endpoint}, client_registration_info={'client_id': 'foo', 'post_logout_redirect_uris': [post_logout_uri]}) callback_mock = MagicMock() callback_mock.__name__ = 'test_callback' # required for Python 2 id_token = IdToken(**{'sub': 'sub1', 'nonce': 'nonce'}) with self.app.test_request_context('/logout'): flask.session['id_token_jwt'] = id_token.to_jwt() resp = authn.oidc_logout(callback_mock)() assert resp.status_code == 303 assert not callback_mock.called
def test_oidc_logout_handles_redirects_from_provider(self): end_session_endpoint = 'https://provider.example.com/end_session' post_logout_uri = 'https://client.example.com/post_logout' authn = OIDCAuthentication(self.app, provider_configuration_info={'issuer': ISSUER, 'end_session_endpoint': end_session_endpoint}, client_registration_info={'client_id': 'foo', 'post_logout_redirect_uris': [post_logout_uri]}) callback_mock = MagicMock() callback_mock.__name__ = 'test_callback' # required for Python 2 state = 'end_session_123' with self.app.test_request_context('/logout?state=' + state): flask.session['end_session_state'] = state authn.oidc_logout(callback_mock)() assert 'end_session_state' not in flask.session assert callback_mock.called
def test_reauthenticate_if_no_session(self): authn = OIDCAuthentication( self.app, provider_configuration_info={'issuer': ISSUER}, client_registration_info={'client_id': 'foo'}, ) client_mock = MagicMock() callback_mock = MagicMock() callback_mock.__name__ = 'test_callback' # required for Python 2 authn.client = client_mock id_token = IdToken(**{'sub': 'sub1', 'nonce': 'nonce'}) with self.app.test_request_context('/'): flask.session['destination'] = '/' flask.session['access_token'] = None flask.session['id_token_jwt'] = None authn.oidc_auth(callback_mock)() assert client_mock.construct_AuthorizationRequest.called is True assert callback_mock.called is False
def test_dont_reauthenticate_silent_if_authentication_not_expired(self): authn = OIDCAuthentication( self.app, provider_configuration_info={'issuer': ISSUER}, client_registration_info={ 'client_id': 'foo', 'session_refresh_interval_seconds': 999 }) client_mock = MagicMock() callback_mock = MagicMock() callback_mock.__name__ = 'test_callback' # required for Python 2 authn.client = client_mock with self.app.test_request_context('/'): flask.session['last_authenticated'] = time.time( ) # freshly authenticated authn.oidc_auth(callback_mock)() assert not client_mock.construct_AuthorizationRequest.called assert callback_mock.called
def test_authentication_error_reponse_returns_default_error_if_no_error_view_set( self): state = 'test_tate' error_response = { 'error': 'invalid_request', 'error_description': 'test error' } authn = OIDCAuthentication( self.app, provider_configuration_info={'issuer': ISSUER}, client_registration_info=dict(client_id='abc', client_secret='foo')) with self.app.test_request_context( '/redirect_uri?{error}&state={state}'.format( error=urlencode(error_response), state=state)): flask.session['state'] = state response = authn._handle_authentication_response() assert response == 'Something went wrong with the authentication, please try to login again.'
def get_oidc(self, app): extra_request_args = {"scope": ["openid", "profile"]} o = OIDCAuthentication( app, issuer="https://{DOMAIN}".format( DOMAIN=self.oidc_config.OIDC_DOMAIN), client_registration_info=self.client_info(), extra_request_args=extra_request_args, ) return o
def test_authentication_error_reponse_calls_to_error_view_if_set(self): state = 'test_tate' error_response = { 'error': 'invalid_request', 'error_description': 'test error' } authn = OIDCAuthentication( self.app, provider_configuration_info={'issuer': ISSUER}, client_registration_info=dict(client_id='abc', client_secret='foo')) error_view_mock = MagicMock() authn._error_view = error_view_mock with self.app.test_request_context( '/redirect_uri?{error}&state={state}'.format( error=urlencode(error_response), state=state)): flask.session['state'] = state authn._handle_authentication_response() error_view_mock.assert_called_with(**error_response)
def test_configurable_userinfo_endpoint_method_is_used(self, method): state = 'state' nonce = 'nonce' sub = 'foobar' authn = OIDCAuthentication(self.app, provider_configuration_info={'issuer': ISSUER, 'token_endpoint': '/token'}, client_registration_info={'client_id': 'foo'}, userinfo_endpoint_method=method) authn.client.do_access_token_request = MagicMock( return_value={'id_token': IdToken(**{'sub': sub, 'nonce': nonce}), 'access_token': 'access_token'}) userinfo_request_mock = MagicMock(return_value=OpenIDSchema(**{'sub': sub})) authn.client.do_user_info_request = userinfo_request_mock with self.app.test_request_context('/redirect_uri?code=foo&state=' + state): flask.session['state'] = state flask.session['nonce'] = nonce flask.session['destination'] = '/' authn._handle_authentication_response() userinfo_request_mock.assert_called_with(method=method, state=state)
def auth(self, app): o = OIDCAuthentication( app, provider_configuration_info=self.provider_info(), client_registration_info=self.client_info()) """ Patch rewrites redirect_uri to only SSL if running in production or stage. """ if os.environ['ENVIRONMENT'] == 'Production': redirect_uri = o.client.registration_response['redirect_uris'][0] o.client.registration_response['redirect_uris'][0] = \ redirect_uri.replace('http', 'https') return o
def auth(self, app): o = OIDCAuthentication(app, issuer='https://' + self.provider_info()['issuer'], client_registration_info=self.client_info()) """ Patch rewrites redirect_uri to only SSL if running in production or stage. """ if os.getenv('environment', 'production') is not 'development': redirect_uri = o.client.registration_response['redirect_uris'][0] o.client.registration_response['redirect_uris'][0] = \ redirect_uri.replace('http', 'http') return o
def test_logout_handles_provider_without_end_session_endpoint(self): post_logout_uri = 'https://client.example.com/post_logout' authn = OIDCAuthentication( self.app, provider_configuration_info={'issuer': ISSUER}, client_registration_info={ 'client_id': 'foo', 'post_logout_redirect_uris': [post_logout_uri] }) id_token = IdToken(**{'sub': 'sub1', 'nonce': 'nonce'}) with self.app.test_request_context('/logout'): flask.session['access_token'] = 'abcde' flask.session['userinfo'] = {'foo': 'bar', 'abc': 'xyz'} flask.session['id_token'] = id_token.to_dict() flask.session['id_token_jwt'] = id_token.to_jwt() end_session_redirect = authn._logout() assert all( k not in flask.session for k in ['access_token', 'userinfo', 'id_token', 'id_token_jwt']) assert end_session_redirect is None
def auth(self, app): if config.fake_account: return FakeOIDCAuthentication() oidc = OIDCAuthentication( app, issuer='https://{DOMAIN}/'.format(DOMAIN=config.oidc_domain), client_registration_info=self.client_info(), extra_request_args={ 'scope': ['openid', 'profile', 'email'], }, ) return oidc
def test_authenticated_session(self): authn = OIDCAuthentication( self.app, provider_configuration_info={'issuer': ISSUER}, client_registration_info={'client_id': 'foo'}, ) client_mock = MagicMock() callback_mock = MagicMock() callback_mock.__name__ = 'test_callback' # required for Python 2 authn.client = client_mock id_token = IdToken(**{'sub': 'sub1', 'nonce': 'nonce', 'exp': 0}) with self.app.test_request_context('/'): flask.session['destination'] = '/' flask.session['access_token'] = 'test token' flask.session['id_token'] = id_token.to_dict() flask.session['id_token_jwt'] = id_token.to_jwt() authn.oidc_auth(callback_mock)() session = Session( flask_session=flask.session, client_registration_info=authn.client_registration_info) assert session.authenticated() is True
def test_logout(self): end_session_endpoint = 'https://provider.example.com/end_session' post_logout_uri = 'https://client.example.com/post_logout' authn = OIDCAuthentication(self.app, provider_configuration_info={ 'issuer': ISSUER, 'end_session_endpoint': end_session_endpoint }, client_registration_info={ 'client_id': 'foo', 'post_logout_redirect_uris': [post_logout_uri] }) id_token = IdToken(**{'sub': 'sub1', 'nonce': 'nonce'}) with self.app.test_request_context('/logout'): flask.session['access_token'] = 'abcde' flask.session['userinfo'] = {'foo': 'bar', 'abc': 'xyz'} flask.session['id_token'] = id_token.to_dict() flask.session['id_token_jwt'] = id_token.to_jwt() end_session_redirect = authn._logout() assert all( k not in flask.session for k in ['access_token', 'userinfo', 'id_token', 'id_token_jwt']) assert end_session_redirect.status_code == 303 assert end_session_redirect.headers['Location'].startswith( end_session_endpoint) parsed_request = dict( parse_qsl( urlparse(end_session_redirect.headers['Location']).query)) assert parsed_request['state'] == flask.session[ 'end_session_state'] assert parsed_request['id_token_hint'] == id_token.to_jwt() assert parsed_request[ 'post_logout_redirect_uri'] == post_logout_uri
def test_oidc_logout_handles_redirects_from_provider(self): end_session_endpoint = 'https://provider.example.com/end_session' post_logout_uri = 'https://client.example.com/post_logout' authn = OIDCAuthentication(self.app, provider_configuration_info={ 'issuer': ISSUER, 'end_session_endpoint': end_session_endpoint }, client_registration_info={ 'client_id': 'foo', 'post_logout_redirect_uris': [post_logout_uri] }) callback_mock = MagicMock() callback_mock.__name__ = 'test_callback' # required for Python 2 state = 'end_session_123' with self.app.test_request_context('/logout?state=' + state): flask.session['end_session_state'] = state authn.oidc_logout(callback_mock)() assert 'end_session_state' not in flask.session assert callback_mock.called
def test_oidc_logout_redirects_to_provider(self): end_session_endpoint = 'https://provider.example.com/end_session' post_logout_uri = 'https://client.example.com/post_logout' authn = OIDCAuthentication(self.app, provider_configuration_info={ 'issuer': ISSUER, 'end_session_endpoint': end_session_endpoint }, client_registration_info={ 'client_id': 'foo', 'post_logout_redirect_uris': [post_logout_uri] }) callback_mock = MagicMock() callback_mock.__name__ = 'test_callback' # required for Python 2 id_token = IdToken(**{'sub': 'sub1', 'nonce': 'nonce'}) with self.app.test_request_context('/logout'): flask.session['id_token_jwt'] = id_token.to_jwt() resp = authn.oidc_logout(callback_mock)() assert resp.status_code == 303 assert not callback_mock.called
def test_reauthenticate_silent_if_refresh_expired(self): authn = OIDCAuthentication( self.app, provider_configuration_info={'issuer': ISSUER}, client_registration_info={ 'client_id': 'foo', 'session_refresh_interval_seconds': 1 }, ) client_mock = MagicMock() callback_mock = MagicMock() callback_mock.__name__ = 'test_callback' # required for Python 2 authn.client = client_mock id_token = IdToken(**{'sub': 'sub1', 'nonce': 'nonce', 'exp': 0}) with self.app.test_request_context('/'): flask.session['destination'] = '/' flask.session['access_token'] = 'test token' flask.session['id_token'] = id_token.to_dict() flask.session['id_token_jwt'] = id_token.to_jwt() flask.session['last_authenticated'] = 1 authn.oidc_auth(callback_mock)() assert client_mock.construct_AuthorizationRequest.called is True assert callback_mock.called is False
def test_unauthenticated_session_with_refresh(self): authn = OIDCAuthentication( self.app, provider_configuration_info={'issuer': ISSUER}, client_registration_info={ 'client_id': 'foo', 'session_refresh_interval_seconds': 300 }, ) client_mock = MagicMock() callback_mock = MagicMock() callback_mock.__name__ = 'test_callback' # required for Python 2 authn.client = client_mock id_token = IdToken(**{'sub': 'sub1', 'nonce': 'nonce', 'exp': 0}) with self.app.test_request_context('/'): flask.session['destination'] = '/' authn.oidc_auth(callback_mock)() session = Session( flask_session=flask.session, client_registration_info=authn.client_registration_info) assert session.authenticated() is False
def test_logout(self): end_session_endpoint = 'https://provider.example.com/end_session' post_logout_uri = 'https://client.example.com/post_logout' authn = OIDCAuthentication(self.app, provider_configuration_info={'issuer': ISSUER, 'end_session_endpoint': end_session_endpoint}, client_registration_info={'client_id': 'foo', 'post_logout_redirect_uris': [post_logout_uri]}) id_token = IdToken(**{'sub': 'sub1', 'nonce': 'nonce'}) with self.app.test_request_context('/logout'): flask.session['access_token'] = 'abcde' flask.session['userinfo'] = {'foo': 'bar', 'abc': 'xyz'} flask.session['id_token'] = id_token.to_dict() flask.session['id_token_jwt'] = id_token.to_jwt() end_session_redirect = authn._logout() assert all(k not in flask.session for k in ['access_token', 'userinfo', 'id_token', 'id_token_jwt']) assert end_session_redirect.status_code == 303 assert end_session_redirect.headers['Location'].startswith(end_session_endpoint) parsed_request = dict(parse_qsl(urlparse(end_session_redirect.headers['Location']).query)) assert parsed_request['state'] == flask.session['end_session_state'] assert parsed_request['id_token_hint'] == id_token.to_jwt() assert parsed_request['post_logout_redirect_uri'] == post_logout_uri
def test_store_internal_redirect_uri_on_static_client_reg(self): responses.add(responses.GET, ISSUER + '/.well-known/openid-configuration', body=json.dumps( dict(issuer=ISSUER, token_endpoint=ISSUER + '/token')), content_type='application/json') authn = OIDCAuthentication(self.app, issuer=ISSUER, client_registration_info=dict( client_id='abc', client_secret='foo')) assert len(authn.client.registration_response['redirect_uris']) == 1 assert authn.client.registration_response['redirect_uris'][ 0] == 'http://localhost/redirect_uri'
def auth(self, app): """ Creates the OIDCAuthentication object to be used to protect routes. Authentication is requested with the following audiences: - lando-api: The LANDO_API_OIDC_IDENTIFIER environment variable will be included as an audience. This allows lando-api to verify that tokens created by lando-ui were intended to be used by the api. Authentication is requested with the following scopes: - openid - Permission to get a unique identifier for the user. This also permits querying Auth0 at https://OIDC_DOMAIN/userinfo for additional user information. - email - Permission to get the user's email address. - profile - Permission to get additional information about the user such as their real name, picture url, and LDAP information. """ oidc = OIDCAuthentication({"AUTH0": self.provider_configuration}, app=app) auth0 = oidc.clients["AUTH0"] oidc.clients["AUTH0"]._parse_response = parse_response_wrapper(auth0) oidc.clients["AUTH0"].userinfo_request = userinfo_request_wrapper(auth0) return oidc
def test_should_register_client_if_not_registered_before(self): registration_endpoint = self.PROVIDER_BASEURL + '/register' provider_metadata = ProviderMetadata( self.PROVIDER_BASEURL, self.PROVIDER_BASEURL + '/auth', self.PROVIDER_BASEURL + '/jwks', registration_endpoint=registration_endpoint) provider_configurations = { self.PROVIDER_NAME: ProviderConfiguration( provider_metadata=provider_metadata, client_registration_info=ClientRegistrationInfo()) } 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')) expected_registration_request = { 'redirect_uris': ['http://{}/redirect_uri'.format(self.CLIENT_DOMAIN)], 'post_logout_redirect_uris': ['http://{}/logout'.format(self.CLIENT_DOMAIN)] } assert registration_request == expected_registration_request
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
# Get app config from absolute file path if os.path.exists(os.path.join(os.getcwd(), "config.py")): app.config.from_pyfile(os.path.join(os.getcwd(), "config.py")) else: app.config.from_pyfile(os.path.join(os.getcwd(), "config.env.py")) app.config["GIT_REVISION"] = subprocess.check_output( ['git', 'rev-parse', '--short', 'HEAD']).decode('utf-8').rstrip() _config = ProviderConfiguration( app.config['OIDC_ISSUER'], client_metadata=ClientMetadata( app.config['OIDC_CLIENT_CONFIG']['client_id'], app.config['OIDC_CLIENT_CONFIG']['client_secret'])) auth = OIDCAuthentication({'default': _config}, app) # Get s3 bucket for use in functions and templates s3_bucket = get_bucket(app.config["S3_URL"], app.config["S3_KEY"], app.config["S3_SECRET"], app.config["BUCKET_NAME"]) # Database setup db = SQLAlchemy(app) migrate = flask_migrate.Migrate(app, db) # Import db models after instantiating db object from audiophiler.models import File, Harold, Auth # Create CSHLDAP connection ldap = CSHLDAP(app.config["LDAP_BIND_DN"], app.config["LDAP_BIND_PW"])
def test_reauthentication_necessary_with_None(self): authn = OIDCAuthentication(self.app, provider_configuration_info={'issuer': ISSUER}, client_registration_info={'client_id': 'foo'}) assert authn._reauthentication_necessary(None) is True
def test_reauthentication_necessary_with_valid_id_token(self): authn = OIDCAuthentication(self.app, provider_configuration_info={'issuer': ISSUER}, client_registration_info={'client_id': 'foo'}) id_token = {'iss': ISSUER} assert authn._reauthentication_necessary(id_token) is False