[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
href={`${addTrailingSlash(
serverBaseUrl,
)}realms/${realmName}/.well-known/openid-credential-issuer`}
)}.well-known/openid-credential-issuer/realms/${realmName}`}
title={t("oid4vcIssuerMetadata")}
/>
</StackItem>

View File

@ -17,13 +17,18 @@
package org.keycloak.protocol.oid4vc.model;
import java.beans.Transient;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import org.keycloak.common.util.KeycloakUriBuilder;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import static org.keycloak.OID4VCConstants.WELL_KNOWN_OPENID_CREDENTIAL_ISSUER;
/**
* Represents a CredentialsOffer according to the OID4VCI Spec
* {@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;
}
@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() {
return credentialConfigurationIds;
}

View File

@ -96,7 +96,7 @@ public abstract class OID4VCAuthorizationCodeFlowTestBase extends OID4VCIssuerEn
Oid4vcTestContext ctx = new Oid4vcTestContext();
// 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)) {
assertEquals(HttpStatus.SC_OK, response.getStatusLine().getStatusCode());
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();
// 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)) {
assertEquals(HttpStatus.SC_OK, response.getStatusLine().getStatusCode());
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);
}
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)) {
assertEquals(HttpStatus.SC_OK, response.getStatusLine().getStatusCode());
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.protocol.oid4vc.issuance.OID4VCAuthorizationDetailsResponse;
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.credentialbuilder.CredentialBuilder;
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.services.managers.AppAuthManager;
import org.keycloak.services.managers.AuthenticationManager;
import org.keycloak.services.resources.RealmsResource;
import org.keycloak.testsuite.Assert;
import org.keycloak.testsuite.admin.ApiUtil;
import org.keycloak.testsuite.runonserver.RunOnServerException;
import org.keycloak.testsuite.util.AdminClientUtil;
import org.keycloak.testsuite.util.oauth.OAuthClient;
import org.keycloak.util.JsonSerialization;
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);
try (Client client = AdminClientUtil.createResteasyClient()) {
UriBuilder builder = UriBuilder.fromUri(OAuthClient.AUTH_SERVER_ROOT);
URI oid4vciDiscoveryUri = RealmsResource.wellKnownProviderUrl(builder)
.build(TEST_REALM_NAME,
OID4VCIssuerWellKnownProviderFactory.PROVIDER_ID);
WebTarget oid4vciDiscoveryTarget = client.target(oid4vciDiscoveryUri);
String metadataUrl = getRealmMetadataPath(TEST_REALM_NAME);
WebTarget oid4vciDiscoveryTarget = client.target(metadataUrl);
// 1. Get authoriZation code without scope specified by wallet
// 2. Using the code to get accesstoken
@ -528,7 +522,13 @@ public abstract class OID4VCIssuerEndpointTest extends OID4VCTest {
}
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,
@ -558,7 +558,7 @@ public abstract class OID4VCIssuerEndpointTest extends OID4VCTest {
}
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);
try (CloseableHttpResponse metadataResponse = httpClient.execute(getMetadataRequest)) {
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.Serializable;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
@ -31,7 +30,6 @@ import java.util.Optional;
import jakarta.ws.rs.client.Client;
import jakarta.ws.rs.client.WebTarget;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.core.UriBuilder;
import org.keycloak.common.util.MultivaluedHashMap;
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.issuance.OID4VCIssuerEndpoint;
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.model.Claim;
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.ProtocolMapperRepresentation;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.services.resources.RealmsResource;
import org.keycloak.testsuite.arquillian.SuiteContext;
import org.keycloak.testsuite.client.KeycloakTestingClient;
import org.keycloak.testsuite.util.AdminClientUtil;
@ -133,7 +129,7 @@ public class OID4VCIssuerWellKnownProviderTest extends OID4VCIssuerEndpointTest
@Test
public void testUnsignedMetadata() {
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);
// Configure realm for unsigned metadata
@ -173,7 +169,7 @@ public class OID4VCIssuerWellKnownProviderTest extends OID4VCIssuerEndpointTest
@Test
public void testSignedMetadata() {
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);
// Configure realm for signed metadata
@ -249,7 +245,7 @@ public class OID4VCIssuerWellKnownProviderTest extends OID4VCIssuerEndpointTest
@Test
public void testUnsignedMetadataWhenSignedDisabled() {
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);
// Disable signed metadata
@ -279,7 +275,7 @@ public class OID4VCIssuerWellKnownProviderTest extends OID4VCIssuerEndpointTest
@Test
public void testSignedMetadataWithInvalidLifespan() {
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);
// Configure invalid lifespan
@ -309,7 +305,7 @@ public class OID4VCIssuerWellKnownProviderTest extends OID4VCIssuerEndpointTest
@Test
public void testSignedMetadataWithInvalidAlgorithm() {
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);
// Configure invalid algorithm
@ -456,10 +452,8 @@ public class OID4VCIssuerWellKnownProviderTest extends OID4VCIssuerEndpointTest
@Test
public void testIssuerMetadataIncludesEncryptionSupport() throws IOException {
try (Client client = AdminClientUtil.createResteasyClient()) {
UriBuilder builder = UriBuilder.fromUri(OAuthClient.AUTH_SERVER_ROOT);
URI oid4vciDiscoveryUri = RealmsResource.wellKnownProviderUrl(builder)
.build(TEST_REALM_NAME, OID4VCIssuerWellKnownProviderFactory.PROVIDER_ID);
WebTarget oid4vciDiscoveryTarget = client.target(oid4vciDiscoveryUri);
String wellKnownUri = getRealmMetadataPath(TEST_REALM_NAME);
WebTarget oid4vciDiscoveryTarget = client.target(wellKnownUri);
try (Response discoveryResponse = oid4vciDiscoveryTarget.request().get()) {
CredentialIssuer oid4vciIssuerConfig = JsonSerialization.readValue(

View File

@ -451,7 +451,7 @@ public class OID4VCJWTIssuerEndpointTest extends OID4VCIssuerEndpointTest {
CredentialsOffer credentialsOffer = JsonSerialization.readValue(s, CredentialsOffer.class);
// 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);
assertEquals(HttpStatus.SC_OK, issuerMetadataResponse.getStatusLine().getStatusCode());
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);
// 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);
assertEquals(HttpStatus.SC_OK, issuerMetadataResponse.getStatusLine().getStatusCode());
s = IOUtils.toString(issuerMetadataResponse.getEntity().getContent(), StandardCharsets.UTF_8);