8000 [OID4VCI] Add a client policy to require a credential offer by tdiesler · Pull Request #47286 · keycloak/keycloak · GitHub
[go: up one dir, main page]

Skip to content

[OID4VCI] Add a client policy to require a credential offer - #47286

Merged
mposolda merged 7 commits into
keycloak:mainfrom
tdiesler:ghi44317
May 14, 2026
Merged

mposolda merged 7 commits into
keycloak:mainfrom
tdiesler:ghi44317

Conversation

@tdiesler
@tdiesler tdiesler commented Mar 19, 2026 •
Copy link
Copy Markdown
Contributor

closes #44317

We now have this policy ...

  "clientProfiles": {
    "profiles": [
      {
        "name": "oid4vci-client-profile",
        "executors": [
          {
            "executor": "oid4vci-policy-executor",
            "configuration": {}
          }
        ]
      }
    ]
  },

  "clientPolicies": {
    "policies": [
      {
        "name": "oid4vci-offer-required",
        "description": "Client policy to determine whether a credential offers is required",
        "enabled": true,
        "conditions": [
          {
            "condition": "client-attributes",
            "configuration": {
              "attributes": "[{\"key\":\"oid4vci.enabled\", \"value\":\"true\"}]"
            }
          }
        ],
        "profiles": [
          "oid4vci-client-profile"
        ]
      }
    ]
  },

which is triggered when a client is oid4vci enabled.

There is also a an attribute vc.policy.offer.required that can be set on the credential scope. The final outcome is determined by this decision matrix

Client Scope Required
- - no
- x yes
x - yes
x x yes
  • policy not installed => policy disabled
  • scope attr default => false

Hence, we use an OR with sensible defaults to determine whether an order is required. With the client scope not installed or disabled and the default attr value an offer would not be required (i.e. out-of-the-box) behaviour.

@tdiesler
tdiesler requested review from a team as code owners March 19, 2026 10:44
@tdiesler tdiesler changed the title [OID4VCI] Add an option to require a credential offer [OID4VCI] Add a client policy to require a credential offer Mar 19, 2026
@tdiesler
tdiesler force-pushed the ghi44317 branch 2 times, most recently from 10cfc1e to 2b8b1d2 Compare March 19, 2026 11:02
@keycloak-github-bot
Copy link
Copy Markdown

Unreported flaky test detected

If the flaky tests below are affected by the changes, please review and update the changes accordingly. Otherwise, a maintainer should report the flaky tests prior to merging the PR.

org.keycloak.testsuite.adapter.servlet.SAMLServletAdapterTest#salesPostSigTest

Keycloak CI - Adapter IT Strict Cookies

org.awaitility.core.ConditionTimeoutException: Assertion condition defined as a Lambda expression in org.keycloak.testsuite.util.URLAssert URL expected to begin with: https://localhost:8543/auth/realms/demo/login-action ; actual URL: https://localhost:8643/sales-post-sig/ within 10 seconds.
	at org.awaitility.core.ConditionAwaiter.await(ConditionAwaiter.java:167)
	at org.awaitility.core.AssertionCondition.await(AssertionCondition.java:119)
	at org.awaitility.core.AssertionCondition.await(AssertionCondition.java:31)
	at org.awaitility.core.ConditionFactory.until(ConditionFactory.java:1160)
...

Report flaky test

org.keycloak.testsuite.adapter.servlet.SAMLServletAdapterTest#disabledClientTest

Keycloak CI - Adapter IT Strict Cookies

org.openqa.selenium.TimeoutException: 
Expected condition failed: waiting for wrapped: text ('Login requester not enabled') to be present in text in element found by By.xpath: //body (tried for 5 second(s) with 500 milliseconds interval)
Build info: version: '4.28.1', revision: '73f5ad48a2'
System info: os.name: 'Linux', os.arch: 'amd64', os.version: '6.14.0-1017-azure', java.version: '21.0.10'
Driver info: org.openqa.selenium.firefox.FirefoxDriver_ByGraphene
...

Report flaky test

@keycloak-github-bot keycloak-github-bot Bot left a comment
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Unreported flaky test detected, please review

@keycloak-github-bot keycloak-github-bot Bot left a comment
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Unreported flaky test detected, please review

@keycloak-github-bot
Copy link
Copy Markdown

Unreported flaky test detected

If the flaky tests below are affected by the changes, please review and update the changes accordingly. Otherwise, a maintainer should report the flaky tests prior to merging the PR.

org.keycloak.testsuite.forms.AuthenticatorSubflowsTest2#testSubflow2

Keycloak CI - Forms IT (chrome)

java.lang.AssertionError: Expected AppPage but was PushTheButton (https://localhost:8543/auth/realms/test/login-actions/authenticate?execution=bf348c91-c033-44b2-a4e1-ac3871640154&client_id=test-app&tab_id=WFiTheBKDfE&client_data=eyJydSI6Imh0dHBzOi8vbG9jYWxob3N0Ojg1NDMvYXV0aC9yZWFsbXMvbWFzdGVyL2FwcC9hdXRoIiwicnQiOiJjb2RlIn0)
	at org.junit.Assert.fail(Assert.java:89)
	at org.junit.Assert.assertTrue(Assert.java:42)
	at org.keycloak.testsuite.pages.AbstractPage.assertCurrent(AbstractPage.java:39)
	at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104)
...

Report flaky test

@tdiesler
tdiesler force-pushed the ghi44317 branch 3 times, most recently from 71e6f55 to 83497e0 Compare March 23, 2026 14:51
@tdiesler
tdiesler force-pushed the ghi44317 branch 6 times, most recently from 363597d to 777a198 Compare April 28, 2026 07:46
@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.

@tdiesler Thanks for the updates.

PR looks better and it is good that it introduces the switch in the UI. However the PR is still doing 2 things, which is not ideal:

  • Introduced credential offer required switch and related logic
  • Introducing client policies

It can be good if the PR rather does just first thing and omit client policies for now as I've mentioned in one of the previous review comments . In general, it is always better if PRs are small as possible and focus on single thing rather than addressing multiple things. So the client policies might be possibly done as a follow-up if needed.

Besides that, The integration with client policies in this PR is done in very strange way. The OID4VCAuthorizationDetailsProcessor checks that offer is present, however exactly same check is done also by the client policy executor. Client policies are typically used in Keycloak as additional layer of protection. The executors usually have their own configuration (for things like offer required for example) instead of reading configuration from somewhere else (EG. client scope in your case) and duplicating the checked logic in same way like done by OID4VCAuthorizationDetailsProcessor. I suggest to take a look at existing client policy executors, their configuration and client policy tests

@tdiesler
Copy link
Copy Markdown
Contributor Author

Introducing client policies

Remember, originally there were no client policies involved - only a switch on the client scope. The argument was, that this is no good because "there is already an established way to handle this: client policies". The client policy (as an additional dimension) was added because of that request.

Unfortunately, the logic cannot be in the policy executor alone, because it is never called when the policy is disabled. In the past that may have been different (i.e. the executor was called and could see whether the policy was active or not).

Anyway, removing the client policy is no big deal - I can do that no problem. For symmetry there would then be no client policies for pre-auth and tx-code either. Are you sure you want to go back to client scope attributes alone?

@tdiesler
tdiesler commented May 1, 2026 •
Copy link
Copy Markdown
Contributor Author

@mposolda This is hopefully now done as you suggested. The client policy is integrated at the authorization endpoint through the standard mechanism. The client scope dimension is also integrated there through the standard checker.

@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.

@tdiesler Thanks! Client policies look good. I've added few comments inline. The biggest one is probably related to AuthorizationEndpointChecker (AFAIK this is new thing in your PR as previously, the checks for enforcing credential-offer were triggered from OID4VCAuthorizationDetailsProcessor if I remember correctly)

@@ -564,6 +564,16 @@
"configuration": {}
}
]
},
{
"name": "oid4vci-client-profile",
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.

Will be good to avoid adding this to default config for now, since it is experimental feature.

For testing, the client profile can be manually created in the particular test.

@tdiesler tdiesler May 6, 2026 •
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

oid4vci is experimental too - it already has no effect unless a client policy that references this profile is created. When it comes to docs, it'd probably be better to just have to explain how the policy is created.

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.

Yes, I mean that oid4vci is experimental feature. The client_policies is not experimental, but already supported feature.

We should remove this though until oid4vci becomes supported as referencing the client-profile, which uses experimental feature is not ideal as people will see this when creating client-policy in the admin console even if the feature may not be supported.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

done

@@ -338,6 +350,47 @@ public void checkParDPoPParams() throws AuthorizationCheckException {
}
}

public void checkCredentialScope() throws AuthorizationCheckException {
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.

Is it possible to make sure that AuthorizationEndpointChecker does not have direct dependency on OID4VCI specific logic?

If needed, it can be good to introduce some new SPI like AuthorizationCheckProvider SPI and have implementation (which will be enabled just when OID4VCI feature enabled for the server) and trigger this new SPI from AuthorizationEndpoint

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Yes, I thought about this. Similar to other places we could check dependent on the client protocol. Can we do that in a follow-up?

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.

Would prefer to do it in this PR instead of the follow-up as it would be good to introduce any OID4VCI related code to the classes like AuthorizationEndpointChecker

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

There are various ways to do this ...

  1. Check the AuthorizationRequest (i.e. fail early), which would require the AuthorizationEndpointChecker to have some sort of plugin mechanism - perhaps by client scope protocol
  2. Check the TokenRequest, there is already a plugin mechanism that we can use (i.e. put the logic back into OID4VCAuthorizationDetailsProcessor)
  3. Check the CredentialRequest (i.e. fail late), this is late but the most natural for OID4VCI

If you prefer option 1 and fail as early as possible - is there perhaps already something I could use out-of-the-box? If not, how would I go about it? Perhaps some protocol specific provider?

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.

The option (2) to add this check to OID4VCAuthorizationDetailsProcessor is OK. AFAIK that was in some previous variant of your PR. If you go this way, then maybe
CredentialClientPolicyExecutor can be updated to act on TOKEN_REQUEST instead of AUTHORIZATION_REQUEST event, so that client policy is checked at the same request (token request) like the credential-scope option? But if you prefer to leave client policy on the AUTHORIZATION_REQUEST that also works.

For the option (1), there is nothing yet, so yes, I am thinking about some specific provider. Something like:

public interface AuthorizationEndpointAddOnCheck extends Provider {

    // Context is to retrieve variables available in AuthorizationEndpointChecker
    void check(AuthorizationEndpointCheckerContext context) throws AuthorizationCheckException;
}

And then in AuthorizationEndpointChecker trigger something like:

AuthorizationEndpointCheckerContext context  new AuthorizationEndpointCheckerContext(this);
try {
    Set<AuthorizationEndpointAddOnCheck> additionalChecks = session.getAllProviders(AuthorizationEndpointAddOnCheck.class);
    for (AuthorizationEndpointAddOnCheck check : additionalChecks) {
        check.check(context); 
    }
} catch (AuthorizationCheckException ex) {
  ... handle exception ...
}

I am not sure about option (3) as we're likely going to add soon the support for refresh-tokens ( #48397 ) and I think this may not work well with that. As in case that refreshed access token would be used in the credential-request, the credential offer will not be available.

@keycloak-github-bot
Copy link
Copy Markdown

Unreported flaky test detected

If the flaky tests below are affected by the changes, please review and update the changes accordingly. Otherwise, a maintainer should report the flaky tests prior to merging the PR.

org.keycloak.testsuite.forms.BruteForceTest#testNoFailureResetForPermanentLockout

Keycloak CI - Base IT (5)

org.opentest4j.AssertionFailedError: Expected error event ==> expected: <true> but was: <false>
	at org.junit.jupiter.api.AssertionFailureBuilder.build(AssertionFailureBuilder.java:151)
	at org.junit.jupiter.api.AssertionFailureBuilder.buildAndThrow(AssertionFailureBuilder.java:132)
	at org.junit.jupiter.api.AssertTrue.failNotTrue(AssertTrue.java:63)
	at org.junit.jupiter.api.AssertTrue.assertTrue(AssertTrue.java:36)
...

Report flaky test

@keycloak-github-bot keycloak-github-bot Bot left a comment
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Unreported flaky test detected, please review

@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.

@tdiesler Thanks for the updates. Replied to some of your comments

@@ -564,6 +564,16 @@
"configuration": {}
}
]
},
{
"name": "oid4vci-client-profile",
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.

Yes, I mean that oid4vci is experimental feature. The client_policies is not experimental, but already supported feature.

We should remove this though until oid4vci becomes supported as referencing the client-profile, which uses experimental feature is not ideal as people will see this when creating client-policy in the admin console even if the feature may not be supported.

@@ -338,6 +350,47 @@ public void checkParDPoPParams() throws AuthorizationCheckException {
}
}

public void checkCredentialScope() throws AuthorizationCheckException {
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.

Would prefer to do it in this PR instead of the follow-up as it would be good to introduce any OID4VCI related code to the classes like AuthorizationEndpointChecker

@keycloak-github-bot keycloak-github-bot Bot left a comment
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Unreported flaky test detected, please review

@keycloak-github-bot
Copy link
Copy Markdown

Unreported flaky test detected

If the flaky tests below are affected by the changes, please review and update the changes accordingly. Otherwise, a maintainer should report the flaky tests prior to merging the PR.

org.keycloak.testsuite.adapter.servlet.SAMLServletAdapterTest#employeeSigPostNoIdpKeyTestKeyIdAsKeyNameInKeyInfo

Keycloak CI - Adapter IT Strict Cookies

java.lang.NullPointerException: Cannot invoke "String.contains(java.lang.CharSequence)" because the return value of "org.openqa.selenium.WebDriver.getPageSource()" is null
	at org.keycloak.testsuite.adapter.servlet.SAMLServletAdapterTest.assertForbiddenLogin(SAMLServletAdapterTest.java:558)
	at org.keycloak.testsuite.adapter.servlet.SAMLServletAdapterTest.testSuccessfulAndUnauthorizedLogin(SAMLServletAdapterTest.java:591)
	at org.keycloak.testsuite.adapter.servlet.SAMLServletAdapterTest.testSuccessfulAndUnauthorizedLogin(SAMLServletAdapterTest.java:584)
	at org.keycloak.testsuite.adapter.servlet.SAMLServletAdapterTest.testSuccessfulAndUnauthorizedLogin(SAMLServletAdapterTest.java:580)
...

Report flaky test

org.keycloak.testsuite.adapter.servlet.SAMLServletAdapterTest#employeeSigPostNoIdpKeyTestNoKeyNameInKeyInfo

Keycloak CI - Adapter IT Strict Cookies

org.opentest4j.AssertionFailedError: expected: <true> but was: <false>
	at org.junit.jupiter.api.AssertionFailureBuilder.build(AssertionFailureBuilder.java:151)
	at org.junit.jupiter.api.AssertionFailureBuilder.buildAndThrow(AssertionFailureBuilder.java:132)
	at org.junit.jupiter.api.AssertTrue.failNotTrue(AssertTrue.java:63)
	at org.junit.jupiter.api.AssertTrue.assertTrue(AssertTrue.java:36)
...
org.opentest4j.AssertionFailedError: expected: <true> but was: <false>
	at org.junit.jupiter.api.AssertionFailureBuilder.build(AssertionFailureBuilder.java:151)
	at org.junit.jupiter.api.AssertionFailureBuilder.buildAndThrow(AssertionFailureBuilder.java:132)
	at org.junit.jupiter.api.AssertTrue.failNotTrue(AssertTrue.java:63)
	at org.junit.jupiter.api.AssertTrue.assertTrue(AssertTrue.java:36)
...

Report flaky test

@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.

@tdiesler Thanks for the update! Commented on the pending review comment in AuthorizationEndpointChecker

@@ -338,6 +350,47 @@ public void checkParDPoPParams() throws AuthorizationCheckException {
}
}

public void checkCredentialScope() throws AuthorizationCheckException {
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.

The option (2) to add this check to OID4VCAuthorizationDetailsProcessor is OK. AFAIK that was in some previous variant of your PR. If you go this way, then maybe
CredentialClientPolicyExecutor can be updated to act on TOKEN_REQUEST instead of AUTHORIZATION_REQUEST event, so that client policy is checked at the same request (token request) like the credential-scope option? But if you prefer to leave client policy on the AUTHORIZATION_REQUEST that also works.

For the option (1), there is nothing yet, so yes, I am thinking about some specific provider. Something like:

public interface AuthorizationEndpointAddOnCheck extends Provider {

    // Context is to retrieve variables available in AuthorizationEndpointChecker
    void check(AuthorizationEndpointCheckerContext context) throws AuthorizationCheckException;
}

And then in AuthorizationEndpointChecker trigger something like:

AuthorizationEndpointCheckerContext context  new AuthorizationEndpointCheckerContext(this);
try {
    Set<AuthorizationEndpointAddOnCheck> additionalChecks = session.getAllProviders(AuthorizationEndp
10BC8
ointAddOnCheck.class);
    for (AuthorizationEndpointAddOnCheck check : additionalChecks) {
        check.check(context); 
    }
} catch (AuthorizationCheckException ex) {
  ... handle exception ...
}

I am not sure about option (3) as we're likely going to add soon the support for refresh-tokens ( #48397 ) and I think this may not work well with that. As in case that refreshed access token would be used in the credential-request, the credential offer will not be available.

@tdiesler
Copy link
Copy Markdown
Contributor Author

I added an AuthorizationEndpointCheckProvider abstraction

@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.

@tdiesler Nice, Thanks!

Could you please rebase on top of latest main? Hopefully that is the last thing for this PR as that should fix the test failures - (The "Javascript UI" failure was an issue in Keycloak main and it was fixed yesterday. So rebase should help to fix it. The failure in "Base testsuite group 5" also seems unrelated to your changes).

@mposolda
Copy link
Copy Markdown
Contributor

@tdiesler I was able to rebase your branch "remotely". So nothing more needed. Hope to merge if tests are OK.

@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.

@tdiesler There is test failure in OID4VCActionTest caused by renaming of clients. Should be easy to fix by update of the "Expected error message". Hopefully that is the last step :-) Thanks

tdiesler added 7 commits May 14, 2026 15:32
Signed-off-by: Thomas Diesler <tdiesler@proton.me>
Signed-off-by: Thomas Diesler <tdiesler@proton.me>
Signed-off-by: Thomas Diesler <tdiesler@proton.me>

- no functional change

- CredentialScopeModelUtils => CredentialScopeUtils
- AuthorizationRequestContext.getparsedResponseType() => getParsedResponseType()
- AuthorizationEndpointRequest.getRedirectUriParam() => getRedirectUri()
Thanks @mposolda

Signed-off-by: Thomas Diesler <tdiesler@proton.me>
Signed-off-by: Thomas Diesler <tdiesler@proton.me>
Signed-off-by: Thomas Diesler <tdiesler@proton.me>
Signed-off-by: Thomas Diesler <tdiesler@proton.me>
@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.

@tdiesler Thanks!

@stianst stianst mentioned this pull request Jun 1, 2026
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.

[OID4VCI] Add a client policy to require a credential offer

3 participants

0