Skip to content

Commit

Permalink
Allow 'post_logout_redirect_uri' to be configured in client metadata. (
Browse files Browse the repository at this point in the history
  • Loading branch information
zamzterz authored Oct 15, 2019
1 parent ddb9d0e commit 883ea8b
Show file tree
Hide file tree
Showing 4 changed files with 59 additions and 18 deletions.
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
28 changes: 19 additions & 9 deletions src/flask_pyoidc/flask_pyoidc.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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())
Expand Down
4 changes: 4 additions & 0 deletions src/flask_pyoidc/pyoidc_facade.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
37 changes: 28 additions & 9 deletions tests/test_flask_pyoidc.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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

Expand Down Expand Up @@ -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))
Expand All @@ -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):
Expand Down

0 comments on commit 883ea8b

Please sign in to comment.