Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added authenticated dynamic client registration #123

Conversation

infohash
Copy link
Contributor

@infohash infohash commented Jan 30, 2022

Identity Providers support two ways how new clients can be registered through Client Registration Service.

  1. Authenticated requests - Request to register new client must contain either Initial Access Token or Bearer Token.

  2. Anonymous requests - Request to register new client doesn’t need to contain any token at all.

Features:

  1. Support for authenticated requests by using Initial Access Token.

  2. Register multiple redirect URIs when the client is dynamically created.


Refer to Client Registration and Keycloak's Client Registration Service for more info.

Verified

This commit was signed with the committer’s verified signature.
Identity Providers support two ways how new clients can be registered through Client Registration Service.

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.

This feature adds support for Authenticated requests.

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
…_config is passed from OIDCAuthentication

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
This reverts commit 84d0419.

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
…rect_uri_config is passed from OIDCAuthentication"

This reverts commit 3ae93d4.
@infohash
Copy link
Contributor Author

infohash commented Jan 31, 2022

def register(self, extra_registration_params=None):
client_metadata = self._provider_configuration.register_client([self._redirect_uri], extra_registration_params)

When the client is dynamically registered, the user may want to register multiple redirect_uris but in this line we are adding only one here.

registration_request['redirect_uris'] = redirect_uris

Then this line adds only one redirect_uri in the registration request. This is also causing redirect to logout view to fail because post_logout_redirect_uri is not in the list of Identity Provider's Valid Redirect URIs.

Partially verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
We cannot verify signatures from co-authors, and some of the co-authors attributed to this commit require their commits to be signed.
User can add custom redirect uris for dynamic client registration through ClientRegistrationInfo class. This feature also ensures atleast one redirect uri is always registered with IdP.
@infohash
Copy link
Contributor Author

infohash commented Feb 1, 2022

This feature can dynamically register all kinds of redirect URIs through Dynamic Client Registration so to register custom post logout redirect URIs in #124 dynamically, #123 is required.

Unverified

This user has not yet uploaded their public signing key.
Comply with 4.4.2.  Access Token Request of Client Credentials Grant in  RFC 6749
@zamzterz
Copy link
Owner

zamzterz commented Feb 7, 2022

Yet another great contribution! 🙌

I've just had a brief look at the changes, but to get a better overview (and make it easier to review and track the changes) could you please split it into separate PRs?
There's at least 3 different features/changes in this single PR which makes it hard to untangle what/how the changes are related:

  1. Support initial access token for registration
  2. Support configuring multiple redirect_uris when using dynamic registration
  3. Adding support for scope parameter when using client credentials grant method.

@infohash
Copy link
Contributor Author

infohash commented Feb 7, 2022

This will break lot of code if I split this. 1 & 2 are related to each other. I can write a review on my code to reference which are 1 & 2. Perhaps if you view from Files changed section, it'll be much easier to review changes.

"""
client_credentials_payload = {
'grant_type': 'client_credentials'
}
if scope:
client_credentials_payload['scope'] = ' '.join(scope)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A minor change, client credentials flow as per ouath specs can also take scope as an argument which takes string as a parameter delimited by spaces to separate multiple scopes. E.g. scope='read write'. Just to make it easier for the user, I'm taking it as a list and convert it into the string for the payload.

@zamzterz
Copy link
Owner

zamzterz commented Feb 7, 2022

While your comments help, I'd strongly prefer it to be split to follow the principle of "1 PR per change".
This PR is quite big (11 changed files) and only some of those actually relate to the title issue of this PR (include initial access token with client registration request).

It will help with the reviewing process (making it go faster) to have smaller chunks of changes to review.

@infohash
Copy link
Contributor Author

infohash commented Feb 7, 2022

I'm not sure if 1 & 2 are separable. Even their test cases are common. They may look different but they are part of the same flow that collects all redirect uris from the user code & then send them in the registration request. I implemented the 1st one but during testing I found it incomplete because it was registering only one redirect uri which lead to invalid_redirect_uri error. Then I worked on 2nd to complete the flow.

I should rather update PR description and title so it doesn't look like 2 different independent features.

I can separate 3rd one, it's completely exclusive.

@zamzterz
Copy link
Owner

zamzterz commented Feb 8, 2022

I might be misunderstanding something, but I don't see how 1 & 2 are related 🙂
Yes, they are both part of client registration but

  1. Adding support for including an access token in the request is only the addition of registration_token in ClientRegistrationInfo (which is then picked up by underlying pyoidc lib), while
  2. Which redirect URIs are to be registered with the provider is independent of whether the request is authenticated (via token) or not.

Is it something specific for your use case/setup (which I gather is using Keycloak)?

@infohash
Copy link
Contributor Author

infohash commented Feb 8, 2022

I think about new features and raise pull request. Keycloak is happen to be the one I'm using to test my features.

It's not worth the time to rework 1 & 2 separately because their test cases also have to be reverted and reworked. It broke existing test cases as well which I had to address them before writing new ones. It will duplicate my work to fix test cases again if I separate them.

I can draft a pull request for review purpose only. For merge, this pull request is the accurate one.

@zamzterz
Copy link
Owner

zamzterz commented Feb 9, 2022

Ok, leave it be for now. 🙂

I'm having a pretty busy week, but I'll try to review it later this week. 👍

Copy link
Owner

@zamzterz zamzterz left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great addition, thanks for taking the time to put this together! 🎉

I think there's some unnecessary inconsistencies with how the redirect URIs are handled, both in implementation and behavior:

  • specifying redirect_uris in client registration info should be passed unmodified to the provider
  • post_logout_redirect_uris should not be mixed with redirect_uris
  • determining whether redirect URIs have been specified should happen in OIDCAuthentication._register_client

Other than that it's mostly minor questions 🙂

@@ -102,3 +218,9 @@ def test_copy_should_overwrite_existing_value(self):
data = OIDCData(abc='xyz')
copy_data = data.copy(qwe='rty', abc='123')
assert copy_data == {'abc': '123', 'qwe': 'rty'}

def test_del_and_len(self):
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this test is needed since those methods are directly delegated to the underlying storage.
(That's why there's only the case for OIDCData.__str__ above - it's the only method except copy which has some custom behavior.)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It has increased the coverage.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above, it's unrelated to this PR and I really don't think it's necessary to test builtin dict-behavior.

@@ -31,6 +31,7 @@ def test_redirect_uri_config(self):
redirect_uri_config = RedirectUriConfig.from_config(config)
assert redirect_uri_config.full_uri == 'https://myexample.com:6000/callback'
assert redirect_uri_config.endpoint == 'callback'
assert repr(redirect_uri_config) == f'({redirect_uri_config.full_uri}, {redirect_uri_config.endpoint})'
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unrelated to this PR and I'm not sure it's needed to test the string representation for this class (which isn't intended to be used for anything).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To increase the test coverage. It's at 98.86% now.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While I appreciate your effort, striving for 100 % coverage is not really necessary. 🙂
I think it's pushing against the limits of the trade-off of having more test code to maintain.

'registration_client_uri': 'https://op.example.com/register/client1',
'registration_access_token': 'registration_access_token1'
}
responses.add(responses.POST, registration_endpoint, json=client_registration_response)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why this change?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To mock client registration response.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What was the difference needed for mocking the response like that instead of using the existing CLIENT_METADATA?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The response must have redirect_uris.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The response is not used/verified in this test.
So this would be an unrelated change that is not needed.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The response still has to be mocked to initialize ClientMetadata by using the parameters taken from the response. Then only it can be considered that assert facade.is_registered() is True is actually true.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Then only it can be considered that assert facade.is_registered() is True is actually true.

Not sure I'm following - the tests passed before this change and the actual values in the response does not matter.
What makes this change necessary for this tests?

1. Improved provider_configuration's register_client
2. Added and moved test cases.
@infohash
Copy link
Contributor Author

Requested changes are done.

Copy link
Owner

@zamzterz zamzterz left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is getting real close now!

The only main thing left is the mixing of redirect_uris and post_logout_redirect_uris (both in registration request and response).
I think this extension should opt for standards compliance so that special handling should not be included (and specifically Keycloak has an open issue for fixing this in their provider implementation).

'registration_client_uri': 'https://op.example.com/register/client1',
'registration_access_token': 'registration_access_token1'
}
responses.add(responses.POST, registration_endpoint, json=client_registration_response)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The response is not used/verified in this test.
So this would be an unrelated change that is not needed.

@infohash
Copy link
Contributor Author

infohash commented Mar 4, 2022

Have you abandoned this project?

@zamzterz
Copy link
Owner

zamzterz commented Mar 6, 2022

Have you abandoned this project?

Sorry for the long turnaround time on these PRs! 😞
I have not abandoned this project, but it is more in a state of maintenance than active development.
And the time I have to spend on it is unfortunately very limited right now (mainly some small windows during weekends as you might have noticed).

Copy link
Owner

@zamzterz zamzterz left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for sticking with it! 👏

This looks ready to go with the minor fix of reverting the use of requests_session in ProviderConfiguration.register_client

@zamzterz zamzterz merged commit cb6a487 into zamzterz:main Mar 7, 2022
@infohash infohash deleted the feature/authenticated-dynamic-client-registration branch March 7, 2022 21:42
@infohash
Copy link
Contributor Author

infohash commented Mar 8, 2022

Until keycloak approves the feature to register post logout redirect uris keycloak/keycloak#9950, there's a workaround to register post_logout_redirect_uris with ClientMetadata. For anyone who ends up here with the issue:

...
auth = OIDCAuthentication({'default': provider_config})
client = auth.clients['default']
with app.app_context():
    auth._register_client(client)
client._provider_configuration._client_metadata[
    'post_logout_redirect_uris'] = ['https://client.example.com/logout', 'https://client.example.com/logout2']
client.register()

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants