[OID4VCI] Fix deprecated realm-scoped well-known endpoint access

Signed-off-by: Thomas Diesler <tdiesler@ibm.com>
This commit is contained in:
Thomas Diesler 2025-11-07 13:56:26 +01:00 committed by Marek Posolda
parent ebd4a6936a
commit 39264edf3f
9 changed files with 45 additions and 29 deletions

View File

@ -303,7 +303,7 @@ function RealmSettingsGeneralTabForm({
<FormattedLink <FormattedLink
href={`${addTrailingSlash( href={`${addTrailingSlash(
serverBaseUrl, serverBaseUrl,
)}realms/${realmName}/.well-known/openid-credential-issuer`} )}.well-known/openid-credential-issuer/realms/${realmName}`}
title={t("oid4vcIssuerMetadata")} title={t("oid4vcIssuerMetadata")}
/> />
</StackItem> </StackItem>

View File

@ -17,13 +17,18 @@
package org.keycloak.protocol.oid4vc.model; package org.keycloak.protocol.oid4vc.model;
import java.beans.Transient;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Objects; import java.util.Objects;
import org.keycloak.common.util.KeycloakUriBuilder;
import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonProperty;
import static org.keycloak.OID4VCConstants.WELL_KNOWN_OPENID_CREDENTIAL_ISSUER;
/** /**
* Represents a CredentialsOffer according to the OID4VCI Spec * Represents a CredentialsOffer according to the OID4VCI Spec
* {@see https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html#name-credential-offer} * {@see https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html#name-credential-offer}
@ -52,6 +57,23 @@ public class CredentialsOffer {
return this; return this;
} }
@Transient
public String getIssuerMetadataUrl() {
var metadataUrl = KeycloakUriBuilder
.fromUri(credentialIssuer)
.path("/.well-known/" + WELL_KNOWN_OPENID_CREDENTIAL_ISSUER);
var idx = credentialIssuer.indexOf("/realms");
if (idx > 0) {
var baseUrl = credentialIssuer.substring(0, idx);
var realmPath = credentialIssuer.substring(idx);
metadataUrl = KeycloakUriBuilder
.fromUri(baseUrl)
.path("/.well-known/" + WELL_KNOWN_OPENID_CREDENTIAL_ISSUER)
.path(realmPath);
}
return metadataUrl.buildAsString();
}
public List<String> getCredentialConfigurationIds() { public List<String> getCredentialConfigurationIds() {
return credentialConfigurationIds; return credentialConfigurationIds;
} }

View File

@ -96,7 +96,7 @@ public abstract class OID4VCAuthorizationCodeFlowTestBase extends OID4VCIssuerEn
Oid4vcTestContext ctx = new Oid4vcTestContext(); Oid4vcTestContext ctx = new Oid4vcTestContext();
// Get credential issuer metadata // Get credential issuer metadata
HttpGet getCredentialIssuer = new HttpGet(getRealmPath(TEST_REALM_NAME) + "/.well-known/openid-credential-issuer"); HttpGet getCredentialIssuer = new HttpGet(getRealmMetadataPath(TEST_REALM_NAME));
try (CloseableHttpResponse response = httpClient.execute(getCredentialIssuer)) { try (CloseableHttpResponse response = httpClient.execute(getCredentialIssuer)) {
assertEquals(HttpStatus.SC_OK, response.getStatusLine().getStatusCode()); assertEquals(HttpStatus.SC_OK, response.getStatusLine().getStatusCode());
String s = IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8); String s = IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8);

View File

@ -99,7 +99,7 @@ public class OID4VCAuthorizationCodeFlowWithPARTest extends OID4VCIssuerEndpoint
Oid4vcTestContext ctx = new Oid4vcTestContext(); Oid4vcTestContext ctx = new Oid4vcTestContext();
// Get credential issuer metadata // Get credential issuer metadata
HttpGet getCredentialIssuer = new HttpGet(getRealmPath(TEST_REALM_NAME) + "/.well-known/openid-credential-issuer"); HttpGet getCredentialIssuer = new HttpGet(getRealmMetadataPath(TEST_REALM_NAME));
try (CloseableHttpResponse response = httpClient.execute(getCredentialIssuer)) { try (CloseableHttpResponse response = httpClient.execute(getCredentialIssuer)) {
assertEquals(HttpStatus.SC_OK, response.getStatusLine().getStatusCode()); assertEquals(HttpStatus.SC_OK, response.getStatusLine().getStatusCode());
String s = IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8); String s = IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8);

View File

@ -121,7 +121,7 @@ public abstract class OID4VCAuthorizationDetailsFlowTestBase extends OID4VCIssue
ctx.credentialsOffer = JsonSerialization.readValue(s, CredentialsOffer.class); ctx.credentialsOffer = JsonSerialization.readValue(s, CredentialsOffer.class);
} }
HttpGet getIssuerMetadata = new HttpGet(ctx.credentialsOffer.getCredentialIssuer() + "/.well-known/openid-credential-issuer"); HttpGet getIssuerMetadata = new HttpGet(ctx.credentialsOffer.getIssuerMetadataUrl());
try (CloseableHttpResponse response = httpClient.execute(getIssuerMetadata)) { try (CloseableHttpResponse response = httpClient.execute(getIssuerMetadata)) {
assertEquals(HttpStatus.SC_OK, response.getStatusLine().getStatusCode()); assertEquals(HttpStatus.SC_OK, response.getStatusLine().getStatusCode());
String s = IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8); String s = IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8);

View File

@ -68,7 +68,6 @@ import org.keycloak.models.UserSessionModel;
import org.keycloak.models.oid4vci.CredentialScopeModel; import org.keycloak.models.oid4vci.CredentialScopeModel;
import org.keycloak.protocol.oid4vc.issuance.OID4VCAuthorizationDetailsResponse; import org.keycloak.protocol.oid4vc.issuance.OID4VCAuthorizationDetailsResponse;
import org.keycloak.protocol.oid4vc.issuance.OID4VCIssuerEndpoint; import org.keycloak.protocol.oid4vc.issuance.OID4VCIssuerEndpoint;
import org.keycloak.protocol.oid4vc.issuance.OID4VCIssuerWellKnownProviderFactory;
import org.keycloak.protocol.oid4vc.issuance.TimeProvider; import org.keycloak.protocol.oid4vc.issuance.TimeProvider;
import org.keycloak.protocol.oid4vc.issuance.credentialbuilder.CredentialBuilder; import org.keycloak.protocol.oid4vc.issuance.credentialbuilder.CredentialBuilder;
import org.keycloak.protocol.oid4vc.issuance.credentialbuilder.JwtCredentialBuilder; import org.keycloak.protocol.oid4vc.issuance.credentialbuilder.JwtCredentialBuilder;
@ -92,12 +91,10 @@ import org.keycloak.representations.idm.RoleRepresentation;
import org.keycloak.representations.idm.UserRepresentation; import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.services.managers.AppAuthManager; import org.keycloak.services.managers.AppAuthManager;
import org.keycloak.services.managers.AuthenticationManager; import org.keycloak.services.managers.AuthenticationManager;
import org.keycloak.services.resources.RealmsResource;
import org.keycloak.testsuite.Assert; import org.keycloak.testsuite.Assert;
import org.keycloak.testsuite.admin.ApiUtil; import org.keycloak.testsuite.admin.ApiUtil;
import org.keycloak.testsuite.runonserver.RunOnServerException; import org.keycloak.testsuite.runonserver.RunOnServerException;
import org.keycloak.testsuite.util.AdminClientUtil; import org.keycloak.testsuite.util.AdminClientUtil;
import org.keycloak.testsuite.util.oauth.OAuthClient;
import org.keycloak.util.JsonSerialization; import org.keycloak.util.JsonSerialization;
import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.core.type.TypeReference;
@ -473,11 +470,8 @@ public abstract class OID4VCIssuerEndpointTest extends OID4VCTest {
String testCredentialConfigurationId = clientScope.getAttributes().get(CredentialScopeModel.CONFIGURATION_ID); String testCredentialConfigurationId = clientScope.getAttributes().get(CredentialScopeModel.CONFIGURATION_ID);
try (Client client = AdminClientUtil.createResteasyClient()) { try (Client client = AdminClientUtil.createResteasyClient()) {
UriBuilder builder = UriBuilder.fromUri(OAuthClient.AUTH_SERVER_ROOT); String metadataUrl = getRealmMetadataPath(TEST_REALM_NAME);
URI oid4vciDiscoveryUri = RealmsResource.wellKnownProviderUrl(builder) WebTarget oid4vciDiscoveryTarget = client.target(metadataUrl);
.build(TEST_REALM_NAME,
OID4VCIssuerWellKnownProviderFactory.PROVIDER_ID);
WebTarget oid4vciDiscoveryTarget = client.target(oid4vciDiscoveryUri);
// 1. Get authoriZation code without scope specified by wallet // 1. Get authoriZation code without scope specified by wallet
// 2. Using the code to get accesstoken // 2. Using the code to get accesstoken
@ -528,7 +522,13 @@ public abstract class OID4VCIssuerEndpointTest extends OID4VCTest {
} }
protected String getRealmPath(String realm) { protected String getRealmPath(String realm) {
return suiteContext.getAuthServerInfo().getContextRoot().toString() + "/auth/realms/" + realm; return suiteContext.getAuthServerInfo().getContextRoot() + "/auth/realms/" + realm;
}
protected String getRealmMetadataPath(String realm) {
var contextRoot = suiteContext.getAuthServerInfo().getContextRoot();
// [TODO] This should be contextRoot/.well-known/openid-credential-issuer/auth/realms/...
return contextRoot + "/auth/.well-known/openid-credential-issuer/realms/" + realm;
} }
protected void requestCredential(String token, protected void requestCredential(String token,
@ -558,7 +558,7 @@ public abstract class OID4VCIssuerEndpointTest extends OID4VCTest {
} }
public CredentialIssuer getCredentialIssuerMetadata() { public CredentialIssuer getCredentialIssuerMetadata() {
final String endpoint = getRealmPath(TEST_REALM_NAME) + "/.well-known/openid-credential-issuer"; final String endpoint = getRealmMetadataPath(TEST_REALM_NAME);
HttpGet getMetadataRequest = new HttpGet(endpoint); HttpGet getMetadataRequest = new HttpGet(endpoint);
try (CloseableHttpResponse metadataResponse = httpClient.execute(getMetadataRequest)) { try (CloseableHttpResponse metadataResponse = httpClient.execute(getMetadataRequest)) {
assertEquals(HttpStatus.SC_OK, metadataResponse.getStatusLine().getStatusCode()); assertEquals(HttpStatus.SC_OK, metadataResponse.getStatusLine().getStatusCode());

View File

@ -19,7 +19,6 @@ package org.keycloak.testsuite.oid4vc.issuance.signing;
import java.io.IOException; import java.io.IOException;
import java.io.Serializable; import java.io.Serializable;
import java.net.URI;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
@ -31,7 +30,6 @@ import java.util.Optional;
import jakarta.ws.rs.client.Client; import jakarta.ws.rs.client.Client;
import jakarta.ws.rs.client.WebTarget; import jakarta.ws.rs.client.WebTarget;
import jakarta.ws.rs.core.Response; import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.core.UriBuilder;
import org.keycloak.common.util.MultivaluedHashMap; import org.keycloak.common.util.MultivaluedHashMap;
import org.keycloak.common.util.Time; import org.keycloak.common.util.Time;
@ -52,7 +50,6 @@ import org.keycloak.protocol.ProtocolMapper;
import org.keycloak.protocol.oid4vc.OID4VCLoginProtocolFactory; import org.keycloak.protocol.oid4vc.OID4VCLoginProtocolFactory;
import org.keycloak.protocol.oid4vc.issuance.OID4VCIssuerEndpoint; import org.keycloak.protocol.oid4vc.issuance.OID4VCIssuerEndpoint;
import org.keycloak.protocol.oid4vc.issuance.OID4VCIssuerWellKnownProvider; import org.keycloak.protocol.oid4vc.issuance.OID4VCIssuerWellKnownProvider;
import org.keycloak.protocol.oid4vc.issuance.OID4VCIssuerWellKnownProviderFactory;
import org.keycloak.protocol.oid4vc.issuance.mappers.OID4VCMapper; import org.keycloak.protocol.oid4vc.issuance.mappers.OID4VCMapper;
import org.keycloak.protocol.oid4vc.model.Claim; import org.keycloak.protocol.oid4vc.model.Claim;
import org.keycloak.protocol.oid4vc.model.ClaimDisplay; import org.keycloak.protocol.oid4vc.model.ClaimDisplay;
@ -68,7 +65,6 @@ import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.representations.idm.ClientScopeRepresentation; import org.keycloak.representations.idm.ClientScopeRepresentation;
import org.keycloak.representations.idm.ProtocolMapperRepresentation; import org.keycloak.representations.idm.ProtocolMapperRepresentation;
import org.keycloak.representations.idm.RealmRepresentation; import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.services.resources.RealmsResource;
import org.keycloak.testsuite.arquillian.SuiteContext; import org.keycloak.testsuite.arquillian.SuiteContext;
import org.keycloak.testsuite.client.KeycloakTestingClient; import org.keycloak.testsuite.client.KeycloakTestingClient;
import org.keycloak.testsuite.util.AdminClientUtil; import org.keycloak.testsuite.util.AdminClientUtil;
@ -133,7 +129,7 @@ public class OID4VCIssuerWellKnownProviderTest extends OID4VCIssuerEndpointTest
@Test @Test
public void testUnsignedMetadata() { public void testUnsignedMetadata() {
try (CloseableHttpClient httpClient = HttpClientBuilder.create().build()) { try (CloseableHttpClient httpClient = HttpClientBuilder.create().build()) {
String wellKnownUri = OAuthClient.AUTH_SERVER_ROOT + "/realms/" + TEST_REALM_NAME + "/.well-known/openid-credential-issuer"; String wellKnownUri = getRealmMetadataPath(TEST_REALM_NAME);
String expectedIssuer = getRealmPath(TEST_REALM_NAME); String expectedIssuer = getRealmPath(TEST_REALM_NAME);
// Configure realm for unsigned metadata // Configure realm for unsigned metadata
@ -173,7 +169,7 @@ public class OID4VCIssuerWellKnownProviderTest extends OID4VCIssuerEndpointTest
@Test @Test
public void testSignedMetadata() { public void testSignedMetadata() {
try (CloseableHttpClient httpClient = HttpClientBuilder.create().build()) { try (CloseableHttpClient httpClient = HttpClientBuilder.create().build()) {
String wellKnownUri = OAuthClient.AUTH_SERVER_ROOT + "/realms/" + TEST_REALM_NAME + "/.well-known/openid-credential-issuer"; String wellKnownUri = getRealmMetadataPath(TEST_REALM_NAME);
String expectedIssuer = getRealmPath(TEST_REALM_NAME); String expectedIssuer = getRealmPath(TEST_REALM_NAME);
// Configure realm for signed metadata // Configure realm for signed metadata
@ -249,7 +245,7 @@ public class OID4VCIssuerWellKnownProviderTest extends OID4VCIssuerEndpointTest
@Test @Test
public void testUnsignedMetadataWhenSignedDisabled() { public void testUnsignedMetadataWhenSignedDisabled() {
try (CloseableHttpClient httpClient = HttpClientBuilder.create().build()) { try (CloseableHttpClient httpClient = HttpClientBuilder.create().build()) {
String wellKnownUri = OAuthClient.AUTH_SERVER_ROOT + "/realms/" + TEST_REALM_NAME + "/.well-known/openid-credential-issuer"; String wellKnownUri = getRealmMetadataPath(TEST_REALM_NAME);
String expectedIssuer = getRealmPath(TEST_REALM_NAME); String expectedIssuer = getRealmPath(TEST_REALM_NAME);
// Disable signed metadata // Disable signed metadata
@ -279,7 +275,7 @@ public class OID4VCIssuerWellKnownProviderTest extends OID4VCIssuerEndpointTest
@Test @Test
public void testSignedMetadataWithInvalidLifespan() { public void testSignedMetadataWithInvalidLifespan() {
try (CloseableHttpClient httpClient = HttpClientBuilder.create().build()) { try (CloseableHttpClient httpClient = HttpClientBuilder.create().build()) {
String wellKnownUri = OAuthClient.AUTH_SERVER_ROOT + "/realms/" + TEST_REALM_NAME + "/.well-known/openid-credential-issuer"; String wellKnownUri = getRealmMetadataPath(TEST_REALM_NAME);
String expectedIssuer = getRealmPath(TEST_REALM_NAME); String expectedIssuer = getRealmPath(TEST_REALM_NAME);
// Configure invalid lifespan // Configure invalid lifespan
@ -309,7 +305,7 @@ public class OID4VCIssuerWellKnownProviderTest extends OID4VCIssuerEndpointTest
@Test @Test
public void testSignedMetadataWithInvalidAlgorithm() { public void testSignedMetadataWithInvalidAlgorithm() {
try (CloseableHttpClient httpClient = HttpClientBuilder.create().build()) { try (CloseableHttpClient httpClient = HttpClientBuilder.create().build()) {
String wellKnownUri = OAuthClient.AUTH_SERVER_ROOT + "/realms/" + TEST_REALM_NAME + "/.well-known/openid-credential-issuer"; String wellKnownUri = getRealmMetadataPath(TEST_REALM_NAME);
String expectedIssuer = getRealmPath(TEST_REALM_NAME); String expectedIssuer = getRealmPath(TEST_REALM_NAME);
// Configure invalid algorithm // Configure invalid algorithm
@ -456,10 +452,8 @@ public class OID4VCIssuerWellKnownProviderTest extends OID4VCIssuerEndpointTest
@Test @Test
public void testIssuerMetadataIncludesEncryptionSupport() throws IOException { public void testIssuerMetadataIncludesEncryptionSupport() throws IOException {
try (Client client = AdminClientUtil.createResteasyClient()) { try (Client client = AdminClientUtil.createResteasyClient()) {
UriBuilder builder = UriBuilder.fromUri(OAuthClient.AUTH_SERVER_ROOT); String wellKnownUri = getRealmMetadataPath(TEST_REALM_NAME);
URI oid4vciDiscoveryUri = RealmsResource.wellKnownProviderUrl(builder) WebTarget oid4vciDiscoveryTarget = client.target(wellKnownUri);
.build(TEST_REALM_NAME, OID4VCIssuerWellKnownProviderFactory.PROVIDER_ID);
WebTarget oid4vciDiscoveryTarget = client.target(oid4vciDiscoveryUri);
try (Response discoveryResponse = oid4vciDiscoveryTarget.request().get()) { try (Response discoveryResponse = oid4vciDiscoveryTarget.request().get()) {
CredentialIssuer oid4vciIssuerConfig = JsonSerialization.readValue( CredentialIssuer oid4vciIssuerConfig = JsonSerialization.readValue(

View File

@ -451,7 +451,7 @@ public class OID4VCJWTIssuerEndpointTest extends OID4VCIssuerEndpointTest {
CredentialsOffer credentialsOffer = JsonSerialization.readValue(s, CredentialsOffer.class); CredentialsOffer credentialsOffer = JsonSerialization.readValue(s, CredentialsOffer.class);
// 3. Get the issuer metadata // 3. Get the issuer metadata
HttpGet getIssuerMetadata = new HttpGet(credentialsOffer.getCredentialIssuer() + "/.well-known/openid-credential-issuer"); HttpGet getIssuerMetadata = new HttpGet(credentialsOffer.getIssuerMetadataUrl());
CloseableHttpResponse issuerMetadataResponse = httpClient.execute(getIssuerMetadata); CloseableHttpResponse issuerMetadataResponse = httpClient.execute(getIssuerMetadata);
assertEquals(HttpStatus.SC_OK, issuerMetadataResponse.getStatusLine().getStatusCode()); assertEquals(HttpStatus.SC_OK, issuerMetadataResponse.getStatusLine().getStatusCode());
s = IOUtils.toString(issuerMetadataResponse.getEntity().getContent(), StandardCharsets.UTF_8); s = IOUtils.toString(issuerMetadataResponse.getEntity().getContent(), StandardCharsets.UTF_8);

View File

@ -323,7 +323,7 @@ public class OID4VCSdJwtIssuingEndpointTest extends OID4VCIssuerEndpointTest {
CredentialsOffer credentialsOffer = JsonSerialization.readValue(s, CredentialsOffer.class); CredentialsOffer credentialsOffer = JsonSerialization.readValue(s, CredentialsOffer.class);
// 3. Get the issuer metadata // 3. Get the issuer metadata
HttpGet getIssuerMetadata = new HttpGet(credentialsOffer.getCredentialIssuer() + "/.well-known/openid-credential-issuer"); HttpGet getIssuerMetadata = new HttpGet(credentialsOffer.getIssuerMetadataUrl());
CloseableHttpResponse issuerMetadataResponse = httpClient.execute(getIssuerMetadata); CloseableHttpResponse issuerMetadataResponse = httpClient.execute(getIssuerMetadata);
assertEquals(HttpStatus.SC_OK, issuerMetadataResponse.getStatusLine().getStatusCode()); assertEquals(HttpStatus.SC_OK, issuerMetadataResponse.getStatusLine().getStatusCode());
s = IOUtils.toString(issuerMetadataResponse.getEntity().getContent(), StandardCharsets.UTF_8); s = IOUtils.toString(issuerMetadataResponse.getEntity().getContent(), StandardCharsets.UTF_8);