Skip to content

[OID4VCI] Revisit and fix OAuthClient.authorizationRequest()#45985

Closed
tdiesler wants to merge 1 commit intokeycloak:mainfrom
tdiesler:ghi45979
Closed

[OID4VCI] Revisit and fix OAuthClient.authorizationRequest()#45985
tdiesler wants to merge 1 commit intokeycloak:mainfrom
tdiesler:ghi45979

Conversation

@tdiesler
Copy link
Contributor

@tdiesler tdiesler commented Feb 3, 2026

closes #45979

@tdiesler tdiesler requested review from a team as code owners February 3, 2026 15:01
@tdiesler tdiesler changed the title Ghi45979 [OID4VCI] Revisit and fix OAuthClient.authorizationRequest() Feb 3, 2026
@tdiesler tdiesler force-pushed the ghi45979 branch 12 times, most recently from 502ba59 to 28eb777 Compare February 11, 2026 11:25
@tdiesler tdiesler force-pushed the ghi45979 branch 2 times, most recently from 59df973 to 6fd3612 Compare February 13, 2026 13:21
@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.model.infinispan.EmbeddedInfinispanSplitBrainTest#testLocalCacheClearedOnMergeEvent

Keycloak CI - Store Model Tests

java.lang.AssertionError
	at org.junit.Assert.fail(Assert.java:87)
	at org.junit.Assert.assertTrue(Assert.java:42)
	at org.junit.Assert.assertTrue(Assert.java:53)
	at org.keycloak.testsuite.model.infinispan.EmbeddedInfinispanSplitBrainTest.awaitLatch(EmbeddedInfinispanSplitBrainTest.java:112)
...
java.lang.AssertionError
	at org.junit.Assert.fail(Assert.java:87)
	at org.junit.Assert.assertTrue(Assert.java:42)
	at org.junit.Assert.assertTrue(Assert.java:53)
	at org.keycloak.testsuite.model.infinispan.EmbeddedInfinispanSplitBrainTest.awaitLatch(EmbeddedInfinispanSplitBrainTest.java:112)
...

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

@tdiesler tdiesler force-pushed the ghi45979 branch 2 times, most recently from 1fca3ae to 74a8fe3 Compare February 13, 2026 16:07
@tdiesler tdiesler force-pushed the ghi45979 branch 3 times, most recently from 98a3e1f to 3c620a1 Compare February 18, 2026 17:13
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. Added some comments inline. Could you check?


import static org.keycloak.OAuth2Constants.SCOPE_OPENID;

public class AuthorizationRequestRequest extends AbstractHttpGetRequest<AuthorizationRequestRequest, AuthorizationRequestResponse> {
Copy link
Contributor

@mposolda mposolda Feb 19, 2026

Choose a reason for hiding this comment

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

It will be good to avoid introducing another class, but rather would be good to re-use existing LoginUrlBuilder and add support to it for the new parameters like issuer_state and authorization_details . As it will be good to avoid having many duplicated "request" classes in the codebase, which are doing same (or very similar) thing.

Similar point: Would be also good to avoid introducing org.keycloak.protocol.oid4vc.model.AuthorizationRequest in the "services" module. Don't see the need of this class (especially considering that we have already AuthorizationEndpointRequest already in that module, but this is not used by the testsuite and hence is likely out of scope of this PR).

Copy link
Contributor Author

@tdiesler tdiesler Feb 19, 2026

Choose a reason for hiding this comment

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

In #45851 I introduce proper error handling like this ...

    public AuthorizationRequestResponse send() {
        initRequest();
        String endpointUrl = authRequest.toRequestUrl(getEndpoint());
        client.driver.navigate().to(endpointUrl);
        AuthPageState authPageState = waitForLoginOrError();
        switch (authPageState) {
            case ERROR_REDIRECT -> {
                return new AuthorizationRequestResponse(client);
            }
            case ERROR_PAGE -> {
                Map<String, String> params = new HashMap<>();
                params.put("error", "invalid_request");
                client.driver.findElements(By.id("kc-error-message")).stream()
                        .map(d -> d.getText().trim().split("\\n")[0])
                        .forEach(s -> params.put("error_description", s));
                return new AuthorizationRequestResponse(params);
            }
            // AuthPageState.LOGIN
            default -> {
                client.fillLoginForm(username, password);
                return new AuthorizationRequestResponse(client);
            }
        }

Currently, we cannot test the error scenario with AuthorizationRequests - it waits for 10sec and then you get a timeout. the error message is lost.

Later still, in #44657 I do a complex IDToken Authorization (when clientId is a DID).

I thought it be good to do this in our oauth.oid4vci() package first, before I change anything in the already existing API that effects everyone. Can please keep this for a little while longer, until we are certain taht we can promote it to the oauth API?

The pattern is this ...

    public AuthorizationRequestRequest authorizationRequest() {
        return authorizationRequest(new AuthorizationRequest());
    }

    public AuthorizationRequestRequest authorizationRequest(AuthorizationRequest authRequest) {
        return new AuthorizationRequestRequest(client, authRequest);
    }

    public Oid4vcCredentialRequest credentialRequest() {
        return credentialRequest(new CredentialRequest());
    }

    public Oid4vcCredentialRequest credentialRequest(CredentialRequest credRequest) {
        return new Oid4vcCredentialRequest(client, credRequest);
    }

i.e. you can either build the request object outside the API (you might also already have it) or populate it through the API. In any case, I would like to have an AuthorizationRequest object so that it can be logged, stored in a context or otherwise processed - it encapsulates what was actually sent when making the request.

AuthorizationRequest supports PKCE and issuer_state and lives in the oid4vci model package. Later when it comes to IDToken Authorization (as required by EBSI) there is other stuff that might need to go in it. Here as well, please can I keep it, until we decide whether to merge oid4vci stuff into the oauth AuthorizationEndpointRequest.

Copy link
Contributor

Choose a reason for hiding this comment

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

@tdiesler It will be good to avoid introducing multiple objects for the same thing. The OID4VCI should use LoginUrlBuilder directly instead of introducing it's own object for the same thing. It will be beneficial if LoginUrlBuilder has support for the things like authorization_details anyway.

Currently, we cannot test the error scenario with AuthorizationRequests - it waits for 10sec and then you get a timeout. the error message is lost.

Existing Keycloak tests work in a way that you know if you expect "success" or "error" . For example when you expect error, you may call something like:

errorPage.assertCurrent();

Or eventually, there are similar things to check if the login-page is displayed with some error on the page etc.

I suppose something like public AuthorizationRequestResponse send(expectedOutcome) { should be used instead as the caller should know if it expects "success" page or "error" page.

Later still, in #44657 I do a complex IDToken Authorization (when clientId is a DID).

This is different thing, so might be rather good to introduce related building blocks once/if we add support for this. I see the IDToken authorization handshake rather as out-of-scope of OID4VCI preview feature as it is specific to EBSI and not related to OID4VCI itself.

@tdiesler tdiesler force-pushed the ghi45979 branch 3 times, most recently from 5f10ed4 to 3d77d1b Compare February 23, 2026 11:27
@tdiesler
Copy link
Contributor Author

Sending an Authorization request with low level LoginForm API looks like this

        oauth.loginForm().scope(credScopeName).authorizationDetails(authDetail).open();
        oauth.fillLoginForm(appUsername, "password");
        AuthorizationEndpointResponse authResponse = oauth.parseLoginResponse();

It is awkward, imho

We can preserve the use of LoginForm (i.e. not send an HttpGetRequest from another AuthorizationRequestRequest) and at the time align with the general OAuthClient API like this ...

        AuthorizationEndpointResponse authResponse = oauth
                .authorizationRequest()
                .scope(credScopeName)
                .authorizationDetails(authDetail)
                .send(appUsername, "password");
        String authCode = authResponse.getCode();

Note, this would ...

  • not be oid4vci specific
  • authorization_details is added to LoginForm
  • the response object is the same as with the LoginForm API
  • using the LoginForm API works as before (i.e. no change to code that uses it)

Would that be acceptable?

@tdiesler tdiesler force-pushed the ghi45979 branch 4 times, most recently from 5d42fb9 to dabc584 Compare February 24, 2026 23:28
@tdiesler tdiesler marked this pull request as draft February 25, 2026 07:56
@tdiesler tdiesler marked this pull request as ready for review February 25, 2026 08:29
@tdiesler tdiesler force-pushed the ghi45979 branch 2 times, most recently from 35cfee7 to 0615a5a Compare February 25, 2026 09:20
@mposolda
Copy link
Contributor

@tdiesler Thanks for the updates. It is probably subjective what is awkward. I am not very sure for a new way with AuthorizationEndpointRequest to be much better and moreover:

  • We would have multiple ways to achieve the same thing with the API
  • The way of AuthorizationEndpointRequest supports only very specific case when login happens with username/password and redirects to the client straight after. Does not support more complex scenarios (EG. if additional authentication like OTP is required, or login with social, flows with forget password etc)
  • AFAIK it is by design that some parts (like clientId) are configured in the OAuth client rather than for the specific request. As it helps with the subsequent requests that stuff like clientId is configured on OAuthClient and does not need to be configured again for the particular test
  • The clientSecret is certainly not needed on AuthorizationEndpointRequest ? Client authentication usually happens in the later stage (during token-request rather than during OIDC authentication request).

I will check with the rest of the team as would like more feedback on OAuth client API used in many tests.

AFAIK the minimal needed would be to add authorizationDetails (and eventually also issuerState if you prefer) to the LoginUrlBuilder .

@mposolda mposolda self-assigned this Feb 26, 2026
@mposolda mposolda requested a review from stianst February 26, 2026 08:37
@tdiesler tdiesler force-pushed the ghi45979 branch 2 times, most recently from 8227f7f to c192f19 Compare February 26, 2026 08:59
@tdiesler
Copy link
Contributor Author

tdiesler commented Feb 26, 2026

  • The way of AuthorizationEndpointRequest supports only very specific case when login happens with username/password and redirects to the client straight after. Does not support more complex scenarios (EG. if additional authentication like OTP is required, or login with social, flows with forget password etc)

It allows us to write code like this

        AuthorizationEndpointResponse authResponse = oauth
                .authorizationRequest()
                .scope(credScopeName)
                .authorizationDetails(authDetail)
                .send(holder, "password");
        String authCode = authResponse.getCode();
        assertNotNull(authCode, "No authCode");

If you remember, I originally did not wan to have it on oauth but on oauth.oid4cv() so it does not get exposed to erveryone before we are sure that it is ready.

In a follow up, I want to add support for improved error handling

    public AuthorizationEndpointResponse send(String username, String password) {
        initRequest();
        loginForm.open();
        AuthPageState authPageState = waitForLoginOrError();
        switch (authPageState) {
            case ERROR_REDIRECT -> {
                return new AuthorizationEndpointResponse(client);
            }
            case ERROR_PAGE -> {
                Map<String, String> params = new HashMap<>();
                params.put("error", "invalid_request");
                client.driver.findElements(By.id("kc-error-message")).stream()
                        .map(d -> d.getText().trim().split("\\n")[0])
                        .forEach(s -> params.put("error_description", s));
                return new AuthorizationEndpointResponse(params);
            }
            // AuthPageState.LOGIN
            default -> {
                client.fillLoginForm(username, password);
                return new AuthorizationEndpointResponse(client);
            }
        }
    }

also preferable for oid4vc only at first.

Still later, I want to add IDToken Authorization #44657

We need this abstraction for a number of reasons and ideally we would have it for oid4vc only and later promote it to oauth when it needed.

Does not support more complex scenarios (EG. if additional authentication like OTP is required, or login with social, flows with forget password etc)

Yes, these are not supported nor needed in our current testsuite migration effort

  • AFAIK it is by design that some parts (like clientId) are configured in the OAuth client rather than for the specific request. As it helps with the subsequent requests that stuff like clientId is configured on OAuthClient and does not need to be configured again for the particular test

That remains the default.

  • The clientSecret is certainly not needed on AuthorizationEndpointRequest ? Client authentication usually happens in the later stage (during token-request rather than during OIDC authentication request).

Yes, I'm sorry that needs to be removed.

AFAIK the minimal needed would be to add authorizationDetails (and eventually also issuerState if you prefer) to the LoginUrlBuilder .

Yes, that is already done so


How about, I move it back to oid4vc? It would not do any harm there and could unblock the testsuite migration PR.

Signed-off-by: Thomas Diesler <tdiesler@ibm.com>
@stianst
Copy link
Contributor

stianst commented Feb 26, 2026

This is the right way of doing this:

        oauth.loginForm().scope(credScopeName).authorizationDetails(authDetail).open();
        oauth.fillLoginForm(appUsername, "password");
        AuthorizationEndpointResponse authResponse = oauth.parseLoginResponse();

Just do it that way and you're all good?

That basically goes hand in hand with OAuth (app sends authz request, the user fills the login form aka authenticates, and you move on with oauth stuff).

User may already be authenticated, may not use password only, may need to have some required actions, etc. That is basically the reason it is done the way it is currently.

@tdiesler tdiesler closed this Feb 26, 2026
@tdiesler tdiesler deleted the ghi45979 branch February 26, 2026 13:35
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] Revisit and fix OAuthClient.authorizationRequest()

3 participants