From c499f8445a0ede248587f54b25bb635aed956411 Mon Sep 17 00:00:00 2001
From: Thomas Diesler
Date: Wed, 8 Apr 2026 15:55:22 +0200
Subject: [PATCH] [OID4VCI] Revisit invalid authorization requests handling
Signed-off-by: Thomas Diesler
---
.../tests/oid4vc/OID4VCBasicWallet.java | 22 ++++++++++++++++--
.../tests/oid4vc/OID4VCPublicClientTest.java | 10 ++++++--
...ID4VCAuthorizationDetailsFlowTestBase.java | 8 +++----
.../testsuite/util/oauth/LoginUrlBuilder.java | 23 +++++++++++++++++++
4 files changed, 54 insertions(+), 9 deletions(-)
diff --git a/tests/base/src/test/java/org/keycloak/tests/oid4vc/OID4VCBasicWallet.java b/tests/base/src/test/java/org/keycloak/tests/oid4vc/OID4VCBasicWallet.java
index 2ff8f8fddcd2..a6cf74e2baa0 100644
--- a/tests/base/src/test/java/org/keycloak/tests/oid4vc/OID4VCBasicWallet.java
+++ b/tests/base/src/test/java/org/keycloak/tests/oid4vc/OID4VCBasicWallet.java
@@ -550,12 +550,30 @@ public AuthorizationEndpointRequest scope(String... scopes) {
return this;
}
- public void openLoginForm() {
+ public boolean openLoginForm() {
loginForm.open();
+ String currUrl = oauth.getDriver().getCurrentUrl();
+ return currUrl != null && !currUrl.contains("error=") && !currUrl.contains("error_description=");
+ }
+
+ public AuthorizationEndpointRequest fillLoginForm(String username, String password) {
+ oauth.fillLoginForm(username, password);
+ return this;
+ }
+
+ public AuthorizationEndpointResponse parseLoginResponse() {
+ return oauth.parseLoginResponse();
}
public AuthorizationEndpointResponse send(String username, String password) {
- return loginForm.doLogin(username, password);
+ openLoginForm();
+ fillLoginForm(username, password);
+ return parseLoginResponse();
+ }
+
+ public AuthorizationEndpointResponse send() {
+ openLoginForm();
+ return parseLoginResponse();
}
}
}
diff --git a/tests/base/src/test/java/org/keycloak/tests/oid4vc/OID4VCPublicClientTest.java b/tests/base/src/test/java/org/keycloak/tests/oid4vc/OID4VCPublicClientTest.java
index f5b6889011c6..9eaa6d32d733 100644
--- a/tests/base/src/test/java/org/keycloak/tests/oid4vc/OID4VCPublicClientTest.java
+++ b/tests/base/src/test/java/org/keycloak/tests/oid4vc/OID4VCPublicClientTest.java
@@ -27,6 +27,7 @@
import org.keycloak.protocol.oid4vc.model.VerifiableCredential;
import org.keycloak.representations.JsonWebToken;
import org.keycloak.testframework.annotations.KeycloakIntegrationTest;
+import org.keycloak.tests.oid4vc.OID4VCBasicWallet.AuthorizationEndpointRequest;
import org.keycloak.tests.oid4vc.OID4VCIssuerTestBase.VCTestServerConfig;
import org.keycloak.testsuite.util.oauth.AccessTokenResponse;
import org.keycloak.testsuite.util.oauth.AuthorizationEndpointResponse;
@@ -39,6 +40,7 @@
import static org.keycloak.OID4VCConstants.OPENID_CREDENTIAL;
import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
@@ -124,8 +126,12 @@ public void testAuthorizationRequestNoPkce() {
// Send AuthorizationRequest without required PKCE
//
- oauth.loginForm().scope(ctx.getScope()).open();
- AuthorizationEndpointResponse authResponse = oauth.parseLoginResponse();
+ AuthorizationEndpointRequest authRequest = wallet
+ .authorizationRequest()
+ .scope(ctx.getScope());
+
+ assertFalse(authRequest.openLoginForm(), "Error expected");
+ AuthorizationEndpointResponse authResponse = authRequest.parseLoginResponse();
assertNull(authResponse.getCode(), "Expected no auth code");
assertEquals("invalid_request", authResponse.getError());
diff --git a/tests/base/src/test/java/org/keycloak/tests/oid4vc/issuance/signing/OID4VCAuthorizationDetailsFlowTestBase.java b/tests/base/src/test/java/org/keycloak/tests/oid4vc/issuance/signing/OID4VCAuthorizationDetailsFlowTestBase.java
index 1864c43550e9..d7c5e513a45d 100644
--- a/tests/base/src/test/java/org/keycloak/tests/oid4vc/issuance/signing/OID4VCAuthorizationDetailsFlowTestBase.java
+++ b/tests/base/src/test/java/org/keycloak/tests/oid4vc/issuance/signing/OID4VCAuthorizationDetailsFlowTestBase.java
@@ -192,12 +192,10 @@ private void runAuthorizationDetailsTest(
try {
AuthorizationEndpointRequest authRequest = authRequestSupplier.get();
- authRequest.openLoginForm();
- String currUrl = oauth.getDriver().getCurrentUrl();
- if (currUrl != null && !currUrl.contains("error=") && !currUrl.contains("error_description=")) {
- oauth.fillLoginForm(ctx.getHolder(), TEST_PASSWORD);
+ if (authRequest.openLoginForm()) {
+ authRequest.fillLoginForm(ctx.getHolder(), TEST_PASSWORD);
}
- AuthorizationEndpointResponse authResponse = oauth.parseLoginResponse();
+ AuthorizationEndpointResponse authResponse = authRequest.parseLoginResponse();
if (authResponse.getError() != null)
throw new IllegalStateException(authResponse.getErrorDescription());
diff --git a/tests/utils-shared/src/main/java/org/keycloak/testsuite/util/oauth/LoginUrlBuilder.java b/tests/utils-shared/src/main/java/org/keycloak/testsuite/util/oauth/LoginUrlBuilder.java
index e769e581e711..02aa50cc9414 100644
--- a/tests/utils-shared/src/main/java/org/keycloak/testsuite/util/oauth/LoginUrlBuilder.java
+++ b/tests/utils-shared/src/main/java/org/keycloak/testsuite/util/oauth/LoginUrlBuilder.java
@@ -130,6 +130,29 @@ protected void initRequest() {
}
}
+ /**
+ * Composite login method for the Authorization Code Flow
+ *
+ *
+ * - It builds and opens the authorization request url
+ * - Fills the login form with user credentials (i.e. username, password)
+ * - Parses the authorization response
+ *
+ *
+ * This method is intended to be used only for the purpose of the basic login flow when the server is expected to open a login form.
+ *
+ * For more complex scenarios like:
+ *
+ * - SSO login (user being automatically authenticated without the need to provide a username/password
+ * - Automatic redirect to the client with the error as result of an invalid authorization request
+ * - The call not being redirected back to the client either due to an incorrect username/password or some other screen being displayed
+ *
+ *
+ * calls to level API will be needed.
+ *
+ * In short, the caller should always know whether they expect a login-form to be shown or not.
+ * For details, there is this discussion.
+ */
public AuthorizationEndpointResponse doLogin(String username, String password) {
open();
client.fillLoginForm(username, password);