8000 Add a client policy executor for the JWT authorization grant settings by mahdi-al-hakim · Pull Request #51720 · keycloak/keycloak · GitHub
[go: up one dir, main page]

Skip to content

Add a client policy executor for the JWT authorization grant settings - #51720

Open
mahdi-al-hakim wants to merge 1 commit into
keycloak:mainfrom
mahdi-al-hakim:jwt-grant-dcr-attributes
Open

mahdi-al-hakim wants to merge 1 commit into
keycloak:mainfrom
mahdi-al-hakim:jwt-grant-dcr-attributes

Conversation

@mahdi-al-hakim
@mahdi-al-hakim mahdi-al-hakim commented Aug 13, 2026 •
Copy link
Copy Markdown

Closes #51718

Adds a client policy executor, jwt-authorization-grant-settings-disabled, that rejects requests changing the two client attributes controlling which assertions the JWT authorization grant accepts: oauth2.jwt.authorization.grant.idp and oauth2.jwt.authorization.grant.audience. It follows FullScopeDisabledExecutor and handles REGISTER and UPDATE, so dynamic client registration and the admin REST API are both covered.

Only a change is rejected. An update sends the whole client representation back, so a request repeating a value the client already has still passes and unrelated edits keep working. Enabling the grant on an existing client is rejected. Turning it off is allowed.

Combined with the client-updater-context condition the policy can be limited to the client's own requests, leaving administrators free to configure the client.

Client policies only bind the paths that fire client policy events, so realm import and partial import are not covered. Both need manage-realm, which can disable the policy anyway.

Docs updated in docs/guides/securing-apps/jwt-authorization-grant.adoc and docs/documentation/server_admin/topics/clients/client-policies.adoc, alongside the sibling executors.

Eleven integration tests in JWTAuthorizationGrantSettingsDisabledExecutorTest, all passing.

AI agents were used to help analyse the code paths and draft this change. I have reviewed and verified all of it.

@mahdi-al-hakim
mahdi-al-hakim requested a review from a team as a code owner August 13, 2026 23:46
Copilot AI balanced review requested due to automatic review settings August 13, 2026 23:46
Copilot AI left a comment
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Hardens dynamic client registration against client-controlled JWT authorization grant trust configuration.

Changes:

  • Restricts identity-provider and audience attributes to administrators.
  • Prevents clients from re-enabling the grant during updates.
  • Adds regression and authorization-path tests.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.

File Description
AbstractClientRegistrationProvider.java Enforces JWT grant configuration restrictions.
ClientRegistrationAuth.java Exposes bearer-token detection within the package.
ClientRegistrationTest.java Tests protected attributes and authorization paths.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +353 to +355
String requested = attributes.get(OIDCConfigAttributes.JWT_AUTHORIZATION_GRANT_ENABLED);
if (!Boolean.parseBoolean(requested)) {
return;
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Good catch, added as disableJWTAuthorizationGrantViaRegistrationTokenShouldSucceed. An administrator turns the grant on, then the client turns it back off through its registration access token, and the test asserts both that the update succeeds and that the stored value is false. It fails if that branch is tightened to reject every write of the attribute.

I kept it on the default registration endpoint rather than going through OIDC grant_types, because this test class only uses that endpoint and adding the OIDC representation machinery here would be out of place. The value the client sends is the one DescriptionConverter writes for a registration request without the grant type in grant_types, and there is a comment in the test saying so. Happy to move it to an OIDC flavoured test if you would rather see the full round trip.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

That test went away with the revert. The equivalent now does the full round trip through the OpenID Connect endpoint, which is what was suggested here.

It is testClientCanDropTheGrantTypeButNotAddItBack. A client is registered with the grant, an administrator adds the allow lists, and the client then drops the grant from its own grant_types using its registration access token. That succeeds and the stored value ends up false. Putting it back is refused, and the allow lists the administrator set are still there.

A second test registers a client that asks for the grant while the policy is active, so the grant_types path is exercised and not only the default endpoint.

@mahdi-al-hakim
mahdi-al-hakim force-pushed the jwt-grant-dcr-attributes branch from 2c6bc34 to dc4f5cd Compare August 14, 2026 00:26
Copilot AI review requested due to automatic review settings August 14, 2026 00:26
Copilot AI left a comment
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated no new comments.

@mahdi-al-hakim
Copy link
Copy Markdown
Author

The second guard, the one that stops a client turning the grant back on, rested on a code read. I ran it against a stock 26.7.1 without this change, so the state it protects is something observed rather than argued.

An administrator configures the client and the grant works. The administrator sets oauth2.jwt.authorization.grant.enabled to false and the grant stops. The identity provider and audience allow lists are still on the client. The client then sets the flag back to true using only its own registration access token, and the same assertion is accepted again.

arm                                                        result     detail
------------------------------------------------------------------------------------------------
A  positive control, admin configured, grant on            HTTP 200   access token for 'victim'
B  control, ADMINISTRATOR sets enabled=false               HTTP 400   {'error': 'invalid_grant', 'error_description': 'JWT
   stored idp allow list after disabling                              'agidp'
   stored audience allow list after disabling                         '[{"key": "agidp", "value": "account"}]'
C1 client sets enabled=true with only its own RAT          HTTP 200   accepted
C2 the same grant after the client re-enabled it           HTTP 200   access token for 'victim'
------------------------------------------------------------------------------------------------
CONFIRMED: an administrator disabled the grant, the allow lists stayed on the client, and the client re-enabled it with only its own registration access token and obtained an access token for 'victim'

credential accounting, every request in this run:

Arm A is there so a failure cannot be read as the rig never working, and arm B so it cannot be read as the disable having no effect. C1 and C2 use only the registration access token and the client secret. The script creates its own realms, deletes them at teardown and voids instead of passing if either control misbehaves. Happy to attach it if that is useful.

@mposolda mposolda self-assigned this Aug 14, 2026
@mposolda mposolda left a comment
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@mahdi-al-hakim Thanks for the PR. I've added inline comment. Can you please check?

* providers the client accepts assertions from, and which token audiences are valid for it, so they
* are administrator configuration and must not be set by the client itself.
*/
private static final Set<String> JWT_AUTHORIZATION_GRANT_TRUST_ATTRIBUTES = Set.of(
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Referencing the JWT_AUTHORIZATION_GRANT_TRUST_ATTRIBUTES directly in this file is not good approach and it lacks flexibility as for some deployments (where client administrators are trustful), it is fine to allow updating this.

Please see my comment here about possible approach to solve this (AFAIK same was replied to you by @lhanusov yesterday on security mailing list) : #51718 (comment) .

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

You are right, and I have reverted that approach completely. The PR no longer modifies any existing Java file, so nothing references those attribute names inside AbstractClientRegistrationProvider any more.

I took option 1 from your list and added a client policy executor, jwt-authorization-grant-settings-disabled. It follows FullScopeDisabledExecutor, handles REGISTER and UPDATE, and therefore covers both dynamic client registration and the admin REST API.

On the flexibility, the useful part is the scoping rather than just leaving the executor out. On its own it applies to whoever the policy matches, administrators included. Combined with the client-updater-context condition restricted to ByRegistrationAccessToken, ByInitialAccessToken and ByAnonymous, it refuses the client and leaves administrators free to configure it, which is the deployment you described. There is a test for that composition and the guide shows it. Being explicit about the cost: this is opt in, so the issue stays open in the default configuration until someone builds the policy. A flag on the identity provider is the option that could ship enabled, and I am happy to do that instead or as well.

On backwards compatibility, the executor rejects only a change, never the presence of a value. It compares the proposed value against ClientCRUDContext.getTargetClient(), which is the stored client on update and null on register. That matters because an update sends the whole representation back, so a client that already has Custom audience mapping re-sends it on every unrelated edit and must not be rejected for it. There is a test that turns the policy on over a client that already has both settings, edits an unrelated field, and asserts the settings survive and the edit succeeds.

Documentation is on the page you linked and in the admin guide, matching where the sibling executors are documented. On your page the executor is described in the existing Client policies and JWT Authorization Grant list next to downscope-assertion-grant-enforcer and jwt-claim-enforcer, and the Custom audience mapping note now ends with a pointer to it. The executor list in client-policies.adoc gets a bullet as well.

Two things worth your view.

I know #46532 removed a JWTAuthorizationGrantAudienceExecutor from this area in favour of the client level attribute. I read this as a different concern, since it governs who may configure the setting rather than how audiences are validated at grant time, and it is option 1 from your list. If that history means you would rather not add another executor here, the identity provider flag is a small change and I am happy to do that instead.

I have handled oauth2.jwt.authorization.grant.enabled rather than leaving it. It cannot be treated like the other two, because DescriptionConverter writes it from the standard grant_types field and rejecting every write would reject a spec compliant registration. So the executor only refuses turning it on for a client that already exists, where the allow lists configured earlier are still present and the flag alone is enough to make the grant work again. Turning it off stays allowed, so a client can still drop the grant type from its own registration.

Copilot AI review requested due to automatic review settings August 14, 2026 11:12
@mahdi-al-hakim
mahdi-al-hakim force-pushed the jwt-grant-dcr-attributes branch from dc4f5cd to a3ed4f8 Compare August 14, 2026 11:12
Copilot AI left a comment
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated no new comments.

Suppressed comments (1)

tests/base/src/test/java/org/keycloak/tests/client/policies/JWTAuthorizationGrantSettingsDisabledExecutorTest.java:236

  • This only exercises the admin REST update. The compatibility branch that lets a client disable an administrator-enabled grant through its registration access token/OIDC grant_types is not covered, so please restore a dynamic-registration regression test for that path.
        // turning it off is still permitted
        updateClientByAdmin(realm, cId,
                c -> c.getAttributes().put(OIDCConfigAttributes.JWT_AUTHORIZATION_GRANT_ENABLED, "false"));

@mahdi-al-hakim
mahdi-al-hakim force-pushed the jwt-grant-dcr-attributes branch from a3ed4f8 to 26d6c1a Compare August 14, 2026 14:03
Copilot AI review requested due to automatic review settings August 14, 2026 14:03
Copilot AI left a comment
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated no new comments.

@mahdi-al-hakim
Copy link
Copy Markdown
Author

@mposolda I have addressed your review by switching to the client policy executor approach. I have re-requested review. Let me know if there is anything else you would like changed.

@mahdi-al-hakim
mahdi-al-hakim force-pushed the jwt-grant-dcr-attributes branch from 26d6c1a to d8882f8 Compare August 29, 2026 01:33
Copilot AI review requested due to automatic review settings August 29, 2026 01:33
@mahdi-al-hakim
mahdi-al-hakim commented Aug 29, 2026 •
Copy link
Copy Markdown
Author

@mposolda Rebased on main and trimmed the description.

On reusing the client attributes condition: it matches on the client's stored attributes, so on register there is nothing to match yet and on update it sees the old value rather than the one being set. It also only votes on whether a policy applies, so rejecting the change still needs an executor.

The WebAuthn IT failure before the rebase was #52074, the flaky PasskeysUsernameFormTest, fixed by #52077 on 27 August. The branch predated that fix. The External links check is red on main as well and is not part of the documentation status check.

Copilot AI left a comment
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated no new comments.

@mahdi-al-hakim mahdi-al-hakim changed the title Prevent clients from configuring the JWT authorization grant Add a client policy executor for the JWT authorization grant settings Aug 29, 2026
@graziang
graziang requested a balanced review from Copilot October 13, 2026 08:44
Copilot AI left a comment
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔵 Needs a closer look

The security-sensitive policy spans multiple client registration and administration paths and warrants final human validation.

1 open finding

🧠 Review effort: Balanced

@lhanusov
Copy link
Copy Markdown
Contributor

@mahdi-al-hakim please, can you rebase it once again? then I will do a final review and ask for merge, thanks for your help and patience 👍

@mahdi-al-hakim
mahdi-al-hakim force-pushed the jwt-grant-dcr-attributes branch from d8882f8 to ef973df Compare October 23, 2026 12:00
Copilot AI balanced review requested due to automatic review settings October 23, 2026 12:00
Copilot AI left a comment
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Changes recommended

A Unicode-whitespace mismatch between validation and storage permits protected attribute writes despite the policy.

2 open issues

Review effort: Balanced

Copilot AI balanced review requested due to automatic review settings October 23, 2026 12:34
@mahdi-al-hakim
mahdi-al-hakim force-pushed the 10BC8 jwt-grant-dcr-attributes branch from ef973df to 2959ea1 Compare October 23, 2026 12:34
Copilot AI left a comment
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Changes recommended

Two integration tests use internal resource IDs in public client-ID lookups, causing exceptions before their persistence checks complete.

3 open issues
1 resolved since last review

Review effort: Balanced

Copilot AI balanced review requested due to automatic review settings October 23, 2026 13:47
@mahdi-al-hakim
mahdi-al-hakim force-pushed the jwt-grant-dcr-attributes branch from 2959ea1 to 6a72cf8 Compare October 23, 2026 13:47
Copilot AI left a comment
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔵 Needs a closer look

Security-sensitive enforcement of assertion trust settings across administrative and dynamic registr 8 ation pa 10BC8 ths warrants final human validation.

1 open issue
2 resolved since last review

Review effort: Balanced

@mahdi-al-hakim
mahdi-al-hakim force-pushed the jwt-grant-dcr-attributes branch from 6a72cf8 to 7bd7559 Compare October 23, 2026 14:28
Copilot AI balanced review requested due to automatic review settings October 23, 2026 14:28
Copilot AI left a comment
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Changes recommended

The administrator-enabled-grant regression test also disables service accounts, triggering a registration guard before reaching its assertions.

2 open issues

Review effort: Balanced

The identity provider allow list and the token audience allow list decide
which assertions the JWT authorization grant accepts for a client, so a
deployment may not want client administrators to change them. The new
executor rejects a request that sets or changes either attribute, on both
dynamic client registration and the admin REST API, and only where a
client policy puts it.

Only a change is rejected, so an existing configuration is preserved and
an unrelated edit that sends the whole client representation back is
still allowed.

Closes keycloak#51718

Signed-off-by: mahdi-al-hakim <mahdi.a.alhakim@gmail.com>
Copilot AI balanced review requested due to automatic review settings October 23, 2026 14:57
@mahdi-al-hakim
mahdi-al-hakim force-pushed the jwt-grant-dcr-attributes branch from 7bd7559 to f58d46c Compare October 23, 2026 14:57
Copilot AI left a comment
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔵 Needs a closer look

Although no confirmed defects were identified, this security-sensitive change warrants human validation of administrator and dynamic registration behavior, including integration-test results.

1 open issue
1 resolved since last review

Review effort: Balanced

@mahdi-al-hakim
Copy link
Copy Markdown
Author

@lhanusov @mposolda Rebased onto the latest main, conflicts resolved. I also addressed the Copilot review comments from this round (whitespace handling in the executor and the registration-access-token test). Ready for your final review, let me know if anything else is needed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

JWT authorization grant: the token audience allowlist is a client attribute the client can set itself (26.7.0)

4 participants

2BAF
0