Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Conversation

@tdiesler
Copy link
Contributor

@tdiesler tdiesler commented Nov 17, 2025

closes #44116

This PR introduces CredentialOfferState that is decoupled from the any login session. It is acted upon getCredentialOffer and (later) upon getCredential. To create a credential offer, one must have the credential-offer-create role i.e. this is an issuer admin task and not something anybody can do. The credential offer is potentially bound to a target clientId and/or a target userId. Other constraints could also be enforced through this mechanism.

Once, the credential-offer-uri has been created successfully and (somehow) delivered to the wallet. There is no pre-requisite of any existing login session to redeem the credential-offer.

Credential Offer Validity Matrix

pre-auth clientId userId Valid Notes
no no no yes Generic offer; any logged-in user may redeem.
no no yes yes Offer restricted to a specific user.
no yes no yes Bound to client; user determined at login.
no yes yes yes Bound to both client and user.
---------- ---------- -------- ------- -----------------------------------------------------
yes no no no Pre-auth requires a user subject; missing userId.
yes yes no no Same as above; userId required.
yes no yes yes Pre-auth for a specific user; client unconstrained.
yes yes yes yes Fully constrained: user + client.

In the pre-auth offer case, the AccessTokenResponse does contain one AuthorizationDetail with both properties (i.e. credential_identifiers, credential_configuration_id). This is now natively supported in AccessTokenResponse.

The spec talks about what the wallet has to use in the CredentialRequest. It must use a credential identifier in the pre-auth case - this is now enforced in the endpoint.

Using the credential_configuration_id in the CredentialRequest is still supported.
Using the credential scope as credential identifier is (imho) a hack that was used by many test cases - support for that has been removed and tests have been migrated to credential_configuration_id.

The credential endpoint now does this ...

        // When the CredentialRequest contains a credential identifier the caller must have gone through the
        // CredentialOffer process or otherwise have set up a valid CredentialOfferState

        if (credentialRequestVO.getCredentialIdentifier() != null) {
            var credId = credentialRequestVO.getCredentialIdentifier();

            // Retrieve the associated credential offer state
            //
            var offerStorage = session.getProvider(CredentialOfferStorage.class);
            var offerState = offerStorage.findOfferStateByCredentialId(session, credId);
            if (offerState == null) {
                var errorMessage = "No credential offer state for credential id: " + credId;
                throw new BadRequestException(getErrorResponse(UNKNOWN_CREDENTIAL_IDENTIFIER, errorMessage));
            }

            var authDetails = offerState.getAuthorizationDetails();
            String credConfigId = (String) authDetails.getOtherClaims().get("credential_configuration_id");

            if (credConfigId == null) {
                var errorMessage = "Cannot credential configuration mapping for: " + credId;
                throw new BadRequestException(getErrorResponse(UNKNOWN_CREDENTIAL_CONFIGURATION, errorMessage));
            }

In short, a credential identifier can (currently) only be obtained through a pre-auth credential offer.

The tests that have a credential identifier, now also use it.

@keycloak-github-bot
Copy link

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.LevelOfAssuranceFlowTest#essentialClaimNotReachedFails

Keycloak CI - Forms IT (chrome)

java.lang.AssertionError: Expected LoginPage but was localhost (https://localhost:8543/auth/realms/test/protocol/openid-connect/auth?scope=openid&claims=%7B%22id_token%22%3A%7B%22acr%22%3A%7B%22essential%22%3Atrue%2C%22values%22%3A%5B%224%22%5D%7D%7D%7D&response_type=code&redirect_uri=https%3A%2F%2Flocalhost%3A8543%2Fauth%2Frealms%2Fmaster%2Fapp%2Fauth&client_id=test-app)
	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:103)
...

Report flaky test

Copy link

@keycloak-github-bot keycloak-github-bot bot left a comment

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

Copy link
Contributor

@Captain-P-Goldfish Captain-P-Goldfish left a comment

Choose a reason for hiding this comment

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

I am not yet finished with the review but I do not have time anymore ^^°

@tdiesler tdiesler force-pushed the ghi44116 branch 4 times, most recently from 148a067 to 0e2d39c Compare November 18, 2025 09:31
@tdiesler
Copy link
Contributor Author

@Captain-P-Goldfish Thanks Pascal, for reviewing this

@tdiesler tdiesler force-pushed the ghi44116 branch 2 times, most recently from 92f6b41 to fd6fba0 Compare November 18, 2025 10:17
@tdiesler
Copy link
Contributor Author

Is there a way to trigger re-testing other than rebase and forced push?

@keycloak-github-bot
Copy link

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.federation.ldap.LDAPGroupMapperTest#test01_ldapOnlyGroupMappings

Keycloak CI - Base IT (5)

java.lang.AssertionError: expected:<4> but was:<3>
	at org.junit.Assert.fail(Assert.java:89)
	at org.junit.Assert.failNotEquals(Assert.java:835)
	at org.junit.Assert.assertEquals(Assert.java:647)
	at org.junit.Assert.assertEquals(Assert.java:633)
...

Report flaky test

org.keycloak.testsuite.federation.ldap.LDAPGroupMapperTest#test02_readOnlyGroupMappings

Keycloak CI - Base IT (5)

java.lang.AssertionError: expected:<5> but was:<7>
	at org.junit.Assert.fail(Assert.java:89)
	at org.junit.Assert.failNotEquals(Assert.java:835)
	at org.junit.Assert.assertEquals(Assert.java:647)
	at org.junit.Assert.assertEquals(Assert.java:633)
...

Report flaky test

org.keycloak.testsuite.federation.ldap.LDAPGroupMapperTest#test09_emptyMemberOnDeletionWorks

Keycloak CI - Base IT (5)

java.lang.AssertionError: expected:<1> but was:<6>
	at org.junit.Assert.fail(Assert.java:89)
	at org.junit.Assert.failNotEquals(Assert.java:835)
	at org.junit.Assert.assertEquals(Assert.java:647)
	at org.junit.Assert.assertEquals(Assert.java:633)
...
java.lang.AssertionError: expected:<1> but was:<3>
	at org.junit.Assert.fail(Assert.java:89)
	at org.junit.Assert.failNotEquals(Assert.java:835)
	at org.junit.Assert.assertEquals(Assert.java:647)
	at org.junit.Assert.assertEquals(Assert.java:633)
...

Report flaky test

Copy link

@keycloak-github-bot keycloak-github-bot bot left a comment

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

Copy link
Contributor

@IngridPuppet IngridPuppet left a comment

Choose a reason for hiding this comment

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

Hello @tdiesler - Thank you for the substantial update to the OpenID4VCI flow. I checked the PR and left a few comments for consideration. Please could you check them?

My main concern would be that support for retrieving credentials with only the credential_identifier parameter at the Credential Endpoint was seemingly dropped, judging from the changes in tests.

@tdiesler
Copy link
Contributor Author

@IngridPuppet thank you so much for your detailed review - great suggestions, comments and lots of TLC for detail :-)

My main concern would be that support for retrieving credentials with only the credential_identifier parameter at the Credential Endpoint was seemingly dropped, judging from the changes in tests.

Using the credential scope as credential identifier is (imho) a hack that was used by many test cases - support for that has been removed and tests have been migrated to credential_configuration_id.

Requesting a credential with only credential_identifier is actually the preferred way of doing things. We have a comment like this in the OID4VCIssuerEndpoint endpoint ...

        // When the CredentialRequest contains a credential identifier the caller must have gone through the
        // CredentialOffer process or otherwise have set up a valid CredentialOfferState

@tdiesler tdiesler force-pushed the ghi44116 branch 3 times, most recently from a97de63 to 02ea986 Compare November 24, 2025 17:12
Copy link
Contributor

@IngridPuppet IngridPuppet left a comment

Choose a reason for hiding this comment

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

Thank you for addressing my concerns and providing clarification. The PR looks good to me.

Copy link
Contributor

@mposolda mposolda left a comment

Choose a reason for hiding this comment

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

@tdiesler Thanks for the PR and sorry for the late review.

@IngridPuppet @Captain-P-Goldfish Thanks for your reviews!

I've added 2 inline comments. Can you please check them? Also it seems that HoKTest failure might be a regression of your PR as it doesn't fail for any other PRs sent to Keycloak main. In your PR, you did some changes unrelated to oid4vci (like EG. marking one of the clients to have direct-grants enabled by default or renaming endpoints on TestingResource) and in theory, some of these can cause the test failure. Maybe just rebase on top of latest keycloak main helps.

errorMessage, Response.Status.BAD_REQUEST);
}

UserSessionModel userSession = session.sessions().createUserSession(null, realm, userModel, userModel.getUsername(),
Copy link
Contributor

Choose a reason for hiding this comment

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

Question: Is it strictly required to create new userSession (and new clientSession) for this use-case? For example for some similar use-cases (like for example OAuth2 client credentials grant implemented in ClientCredentialsGrantType), we are using "transient" user session.

This means that tokens are not attached to the real userSession, which is persisted into the DB. But tokens can be still used to be sent to Keycloak endpoints (like introspection-endpoint or admin REST API or anything else. I believe that oid4VCI credentials endpoint should work as well).

In short: Real user session is needed just if token refreshes are supposed to work. But it seems that tokens issued from "pre-authorized" grant doesn't need to be refreshed?

Copy link
Contributor Author

@tdiesler tdiesler Nov 27, 2025

Choose a reason for hiding this comment

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

The client exchanges the pre-auth code for an AccessToken it then calls the /credential endpoint with that AccessToken. For the TRANSIENT case authentication fails with invalid token here ...

                UserSessionUtil.UserSessionValidationResult validationResult = UserSessionUtil.findValidSessionForAccessToken(session, realm, token, client, invalidUserSessionCallback);
                if (validationResult.getError() != null) {
                    return null;
                }

It would be expected to have no valid user session, because ...

  • the Issuer created the the Pre-Auth CredentialOffer for a given target user (which might not have a user session)
  • the target user then exchanges the pre-auth code for an AccessToken (i.e. no other login required)
  • the issuer is expected to provide the pre-auth credential without the user ever needing to login

For clarity, I added the AuthorizationDetails to the pre-auth AccessToken as well (and not just to the AccessTokenResponse)

I left the the persistent user session in place (for now)

        UserSessionModel userSession = session.sessions().createUserSession(null, realm, userModel, userModel.getUsername(),
                null, "pre-authorized-code", false, null,
                null, UserSessionModel.SessionPersistenceState.PERSISTENT);

Perhaps there is a way to work with a TRANSIENT user session. This could then be an improvement for later imho.

Copy link
Contributor

Choose a reason for hiding this comment

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

@tdiesler Yes, I think we can make it working with userSession not being created, but agree that this could be a follow-up. Created #44534 for that. Will try to take a look when I have some time.

Thanks again for all the work on the PR!

@tdiesler tdiesler marked this pull request as draft November 27, 2025 07:50
@tdiesler tdiesler marked this pull request as ready for review November 27, 2025 08:03
@tdiesler
Copy link
Contributor Author

retest this please

@mposolda mposolda merged commit 54bf920 into keycloak:main Nov 27, 2025
82 checks passed
@tdiesler tdiesler deleted the ghi44116 branch November 27, 2025 15:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[OID4VCI] Credential Offer must be created by Issuer not Holder

4 participants