Skip to content

Commit

Permalink
Added authenticated dynamic client registration (#123)
Browse files Browse the repository at this point in the history
Identity Providers support two ways how new clients can be registered through Dyncamic Client Registration:

* Authenticated requests - Request to register new client must contain either Initial Access Token or Bearer Token.
* Anonymous requests - Request to register new client doesn’t need to contain any token at all.

Also adds support for custom redirect_uris and post_logout_redirect_uris for dynamic client registration through ClientRegistrationInfo class.
  • Loading branch information
infohash authored Mar 7, 2022
1 parent 82e7df6 commit cb6a487
Show file tree
Hide file tree
Showing 9 changed files with 326 additions and 76 deletions.
23 changes: 22 additions & 1 deletion docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,31 @@ To dynamically register a new client for your application, the required client r
```python
from flask_pyoidc.provider_configuration import ProviderConfiguration, ClientRegistrationInfo

client_registration_info = ClientRegistrationInfo(client_name='Test App', contacts=['[email protected]'])
client_registration_info = ClientRegistrationInfo(client_name='Test App', contacts=['[email protected]'],
redirect_uris=['https://client.example.com/redirect',
'https://client.example.com/redirect2'],
post_logout_redirect_uris=['https://client.example.com/logout',
'https://client.example.com/logout2]
registration_token='initial_access_token')
provider_config = ProviderConfiguration(client_registration_info=client_registration_info, [provider_configuration])
```

**Note: To register all `redirect_uris` and `post_logout_redirect_uris` with the provider,
you must provide them as a list in their respective keyword arguments.**

Identity Providers support two ways how new clients can be registered through Dynamic Client Registration:

1. Authenticated requests - the registration request must contain an "initial access token" obtained from your
identity provider.
If you want to use this method then you must provide `registration_token` keyword argument to `ClientRegistrationInfo`.

2. Anonymous requests - the registration request doesn't need to contain any token.

You can set any Client Metadata parameters for `ClientRegistrationInfo` during the registration. For a complete list of
keyword arguments, see [Client Metadata](https://openid.net/specs/openid-connect-registration-1_0.html#ClientMetadata).
Also refer to the
[Client Registration Request example](https://openid.net/specs/openid-connect-registration-1_0.html#RegistrationRequest).

## Provider configuration

### Dynamic provider configuration
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 @@ -110,15 +110,25 @@ def default_post_logout_redirect_uris():
return [url_for_logout_view]
return []

client_registration_args = {}
post_logout_redirect_uris = client._provider_configuration._client_registration_info.get('post_logout_redirect_uris')
if post_logout_redirect_uris:
client_registration_args['post_logout_redirect_uris'] = post_logout_redirect_uris
else:
client_registration_args['post_logout_redirect_uris'] = default_post_logout_redirect_uris()

logger.debug('registering with post_logout_redirect_uris=%s', client_registration_args['post_logout_redirect_uris'])
client.register(client_registration_args)
# Check if the user has passed list of redirect_uris in ClientRegistrationInfo.
if not client._provider_configuration._client_registration_info.get('redirect_uris'):
# If not, create it.
client._provider_configuration._client_registration_info[
'redirect_uris'] = [self._redirect_uri_config.full_uri]
# Check if the user has passed post_logout_redirect_uris in ClientRegistrationInfo.
post_logout_redirect_uris = client._provider_configuration._client_registration_info.get(
'post_logout_redirect_uris')
if not post_logout_redirect_uris:
# If not passed, try to resolve it by using logout view function.
_default_post_logout_redirect_uris = default_post_logout_redirect_uris()
# Set this as an attribute of ClientRegistrationInfo.
client._provider_configuration._client_registration_info[
'post_logout_redirect_uris'] = _default_post_logout_redirect_uris
logger.debug(
f'''registering with post_logout_redirect_uris = {
client._provider_configuration._client_registration_info[
'post_logout_redirect_uris']}''')
client.register()

def _authenticate(self, client, interactive=True):
if not client.is_registered():
Expand Down
68 changes: 45 additions & 23 deletions src/flask_pyoidc/provider_configuration.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import collections.abc
import logging

from oic.oic import Client
import requests

logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -57,19 +58,41 @@ def to_dict(self):


class ProviderMetadata(OIDCData):
def __init__(self, issuer=None, authorization_endpoint=None, jwks_uri=None, **kwargs):
"""
Args:
issuer (str): OP Issuer Identifier.
authorization_endpoint (str): URL of the OP's Authorization Endpoint
jwks_uri (str): URL of the OP's JSON Web Key Set
kwargs (dict): key-value pairs corresponding to
`OpenID Provider Metadata`_
.. _OpenID Provider Metadata:
https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderMetadata

def __init__(self,
issuer=None,
authorization_endpoint=None,
jwks_uri=None,
token_endpoint=None,
userinfo_endpoint=None,
introspection_endpoint=None,
registration_endpoint=None,
**kwargs):
"""OpenID Providers have metadata describing their configuration.
Parameters
----------
issuer: str, Optional
OP Issuer Identifier.
authorization_endpoint: str, Optional
URL of the OP's OAuth 2.0 Authorization Endpoint.
jwks_uri: str, Optional
URL of the OP's JSON Web Key Set [JWK] document.
token_endpoint: str, Optional
URL of the OP's OAuth 2.0 Token Endpoint.
userinfo_endpoint: str, Optional
URL of the OP's UserInfo Endpoint.
introspection_endpoint: str, Optional
URL of the OP's token introspection endpoint.
registration_endpoint: str, Optional
URL of the OP's Dynamic Client Registration Endpoint.
**kwargs : dict, Optional
Extra arguments to [OpenID Provider Metadata](https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderMetadata)
"""
super(ProviderMetadata, self).__init__(issuer=issuer, authorization_endpoint=authorization_endpoint, jwks_uri=jwks_uri, **kwargs)
super().__init__(issuer=issuer, authorization_endpoint=authorization_endpoint,
token_endpoint=token_endpoint, userinfo_endpoint=userinfo_endpoint,
jwks_uri=jwks_uri, introspection_endpoint=introspection_endpoint,
registration_endpoint=registration_endpoint, **kwargs)


class ClientRegistrationInfo(OIDCData):
Expand Down Expand Up @@ -100,7 +123,7 @@ class ProviderConfiguration:
session_refresh_interval_seconds (int): Number of seconds between updates of user data (tokens, user data, etc.)
fetched from the provider. If `None` is specified, no silent updates should be made user data will be made.
userinfo_endpoint_method (str): HTTP method ("GET" or "POST") to use when making the UserInfo Request. If
`None` is specifed, no UserInfo Request will be made.
`None` is specified, no UserInfo Request will be made.
"""

DEFAULT_REQUEST_TIMEOUT = 5
Expand Down Expand Up @@ -164,22 +187,21 @@ def ensure_provider_metadata(self):
def registered_client_metadata(self):
return self._client_metadata

def register_client(self, redirect_uris, extra_parameters=None):
def register_client(self, client: Client):

if not self._client_metadata:
if 'registration_endpoint' not in self._provider_metadata:
if not self._provider_metadata['registration_endpoint']:
raise ValueError("Can't use dynamic client registration, provider metadata is missing "
"'registration_endpoint'.")

registration_request = self._client_registration_info.to_dict()
registration_request['redirect_uris'] = redirect_uris
if extra_parameters:
registration_request.update(extra_parameters)

resp = self.requests_session \
.post(self._provider_metadata['registration_endpoint'],
json=registration_request,
timeout=self.DEFAULT_REQUEST_TIMEOUT)
self._client_metadata = ClientMetadata(redirect_uris=redirect_uris, **resp.json())
# Send request to register the client dynamically.
registration_response = client.register(
url=self._provider_metadata['registration_endpoint'],
**registration_request)
self._client_metadata = ClientMetadata(
**registration_response.to_dict())
logger.debug('Received registration response: client_id=' + self._client_metadata['client_id'])

return self._client_metadata
4 changes: 2 additions & 2 deletions src/flask_pyoidc/pyoidc_facade.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,8 +70,8 @@ def __init__(self, provider_configuration, redirect_uri):
def is_registered(self):
return bool(self._provider_configuration.registered_client_metadata)

def register(self, extra_registration_params=None):
client_metadata = self._provider_configuration.register_client([self._redirect_uri], extra_registration_params)
def register(self):
client_metadata = self._provider_configuration.register_client(self._client)
logger.debug('client registration response: %s', client_metadata)
self._client.store_registration_info(RegistrationResponse(**client_metadata.to_dict()))

Expand Down
1 change: 0 additions & 1 deletion src/flask_pyoidc/redirect_uri_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,4 +60,3 @@ def _parse_legacy_config(config):
full_uri = scheme + '://' + redirect_domain + '/' + endpoint

return full_uri, endpoint

Loading

0 comments on commit cb6a487

Please sign in to comment.