From 883ea8b86465cdf8f5f7372ee226130a9ce0d27a Mon Sep 17 00:00:00 2001 From: Samuel Gulliksson Date: Tue, 15 Oct 2019 20:37:49 +0200 Subject: [PATCH] Allow 'post_logout_redirect_uri' to be configured in client metadata. (#50) --- README.md | 8 +++++++ src/flask_pyoidc/flask_pyoidc.py | 28 +++++++++++++++-------- src/flask_pyoidc/pyoidc_facade.py | 4 ++++ tests/test_flask_pyoidc.py | 37 +++++++++++++++++++++++-------- 4 files changed, 59 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index 39601ae..da490b4 100644 --- a/README.md +++ b/README.md @@ -175,6 +175,14 @@ def logout(): return 'You\'ve been successfully logged out!' ``` +If the logout view is mounted under a custom endpoint (other than the default, which is +[the name of the view function](http://flask.pocoo.org/docs/1.0/api/#flask.Flask.route)), or if using Blueprints, you +must specify the full URL in the Flask-pyoidc configuration using `post_logout_redirect_uris`: +```python +ClientMetadata(..., post_logout_redirect_uris=['https://example.com/post_logout']) # if using static client registration +ClientRegistrationInfo(..., post_logout_redirect_uris=['https://example.com/post_logout']) # if using dynamic client registration +``` + This extension also supports [RP-Initiated Logout](http://openid.net/specs/openid-connect-session-1_0.html#RPLogout), if the provider allows it. Make sure the `end_session_endpoint` is defined in the provider metadata to enable notifying the provider when the user logs out. diff --git a/src/flask_pyoidc/flask_pyoidc.py b/src/flask_pyoidc/flask_pyoidc.py index 132ddc6..3b4b643 100644 --- a/src/flask_pyoidc/flask_pyoidc.py +++ b/src/flask_pyoidc/flask_pyoidc.py @@ -75,17 +75,27 @@ def init_app(self, app): for (name, configuration) in self._provider_configurations.items() } - def _get_post_logout_redirect_uri(self): - if self._logout_view: - return url_for(self._logout_view.__name__, _external=True) - return None + def _get_post_logout_redirect_uri(self, client): + if client.post_logout_redirect_uris: + return client.post_logout_redirect_uris[0] + return self._get_url_for_logout_view() + + def _get_url_for_logout_view(self): + return url_for(self._logout_view.__name__, _external=True) if self._logout_view else None def _register_client(self, client): + def default_post_logout_redirect_uris(): + url_for_logout_view = self._get_url_for_logout_view() + if url_for_logout_view: + return [url_for_logout_view] + return [] + client_registration_args = {} - post_logout_redirect_uri = self._get_post_logout_redirect_uri() - if post_logout_redirect_uri: - logger.debug('registering with post_logout_redirect_uri=%s', post_logout_redirect_uri) - client_registration_args['post_logout_redirect_uris'] = [post_logout_redirect_uri] + post_logout_redirect_uris = client._provider_configuration._client_registration_info.get('post_logout_redirect_uris', + default_post_logout_redirect_uris()) + if post_logout_redirect_uris: + logger.debug('registering with post_logout_redirect_uris=%s', post_logout_redirect_uris) + client_registration_args['post_logout_redirect_uris'] = post_logout_redirect_uris client.register(client_registration_args) def _authenticate(self, client, interactive=True): @@ -212,7 +222,7 @@ def _logout(self): flask.session['end_session_state'] = rndstr() end_session_request = EndSessionRequest(id_token_hint=id_token_jwt, - post_logout_redirect_uri=self._get_post_logout_redirect_uri(), + post_logout_redirect_uri=self._get_post_logout_redirect_uri(client), state=flask.session['end_session_state']) logger.debug('send endsession request: %s', end_session_request.to_json()) diff --git a/src/flask_pyoidc/pyoidc_facade.py b/src/flask_pyoidc/pyoidc_facade.py index 5d8adf3..aea1214 100644 --- a/src/flask_pyoidc/pyoidc_facade.py +++ b/src/flask_pyoidc/pyoidc_facade.py @@ -171,6 +171,10 @@ def provider_end_session_endpoint(self): provider_metadata = self._provider_configuration.ensure_provider_metadata() return provider_metadata.get('end_session_endpoint') + @property + def post_logout_redirect_uris(self): + return self._client.registration_response.get('post_logout_redirect_uris') + def _parse_response(self, response_params, success_response_cls, error_response_cls): if 'error' in response_params: response = error_response_cls(**response_params) diff --git a/tests/test_flask_pyoidc.py b/tests/test_flask_pyoidc.py index a2eca3a..f1dddf5 100644 --- a/tests/test_flask_pyoidc.py +++ b/tests/test_flask_pyoidc.py @@ -124,15 +124,22 @@ def test_expected_auth_response_mode_is_set(self, response_type, expected): self.assert_auth_redirect(auth_redirect) @responses.activate - def test_should_register_client_if_not_registered_before(self): + @pytest.mark.parametrize('post_logout_redirect_uris', [ + None, + ['https://example.com/post_logout'] + ]) + 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_registration_info=ClientRegistrationInfo(**client_metadata)) } authn = OIDCAuthentication(provider_configurations) authn.init_app(self.app) @@ -149,9 +156,10 @@ def test_should_register_client_if_not_registered_before(self): self.assert_auth_redirect(auth_redirect) registration_request = json.loads(responses.calls[0].request.body.decode('utf-8')) + 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': ['http://{}/logout'.format(self.CLIENT_DOMAIN)] + 'post_logout_redirect_uris': expected_post_logout_redirect_uris } assert registration_request == expected_registration_request @@ -335,22 +343,31 @@ def test_session_expiration_set_to_configured_lifetime(self, time_mock, utc_time cookie_lifetime = (parsed_expiration - datetime.utcnow()).total_seconds() assert cookie_lifetime == pytest.approx(session_lifetime, abs=1) - def test_logout_redirects_to_provider_if_end_session_endpoint_is_configured(self): + @pytest.mark.parametrize('post_logout_redirect_uri', [ + None, + 'https://example.com/post_logout' + ]) + def test_logout_redirects_to_provider_if_end_session_endpoint_is_configured(self, post_logout_redirect_uri): end_session_endpoint = 'https://provider.example.com/end_session' - authn = self.init_app(provider_metadata_extras={'end_session_endpoint': end_session_endpoint}) + client_metadata = {} + if post_logout_redirect_uri: + client_metadata['post_logout_redirect_uris'] = [post_logout_redirect_uri] + + authn = self.init_app(provider_metadata_extras={'end_session_endpoint': end_session_endpoint}, + client_metadata_extras=client_metadata) logout_view_mock = self.get_view_mock() id_token = IdToken(**{'sub': 'sub1', 'nonce': 'nonce'}) # register logout view - self.app.add_url_rule('/logout', view_func=authn.oidc_logout(logout_view_mock)) + view_func = authn.oidc_logout(logout_view_mock) + self.app.add_url_rule('/logout', view_func=view_func) with self.app.test_request_context('/logout'): UserSession(flask.session, self.PROVIDER_NAME).update('test_access_token', id_token.to_dict(), id_token.to_jwt(), {'sub': 'user1'}) - - end_session_redirect = authn.oidc_logout(logout_view_mock)() + end_session_redirect = view_func() # ensure user session has been cleared assert all(k not in flask.session for k in UserSession.KEYS) parsed_request = dict(parse_qsl(urlparse(end_session_redirect.headers['Location']).query)) @@ -359,7 +376,9 @@ def test_logout_redirects_to_provider_if_end_session_endpoint_is_configured(self assert end_session_redirect.status_code == 303 assert end_session_redirect.location.startswith(end_session_endpoint) assert IdToken().from_jwt(parsed_request['id_token_hint']) == id_token - assert parsed_request['post_logout_redirect_uri'] == 'http://{}/logout'.format(self.CLIENT_DOMAIN) + + expected_post_logout_redirect_uri = post_logout_redirect_uri if post_logout_redirect_uri else 'http://{}/logout'.format(self.CLIENT_DOMAIN) + assert parsed_request['post_logout_redirect_uri'] == expected_post_logout_redirect_uri assert not logout_view_mock.called def test_logout_handles_provider_without_end_session_endpoint(self):