mirror of
https://github.com/keycloak/keycloak.git
synced 2026-01-10 15:32:05 -03:30
[OID4VCI] Adjust Credential Issuer Metadata endpoint, return issuer metadata at /.well-known/openid-credential-issuer/realms/{realm} (#42577)
Closes #41589 Signed-off-by: Awambeng <awambengrodrick@gmail.com>
This commit is contained in:
parent
8a94bd90f9
commit
20f9306b78
@ -17,6 +17,7 @@
|
||||
|
||||
package org.keycloak.protocol.oid4vc.issuance;
|
||||
|
||||
import jakarta.ws.rs.core.UriBuilder;
|
||||
import jakarta.ws.rs.core.UriInfo;
|
||||
import org.apache.http.HttpHeaders;
|
||||
import org.keycloak.common.util.Time;
|
||||
@ -26,6 +27,7 @@ import org.keycloak.crypto.KeyUse;
|
||||
import org.keycloak.crypto.KeyWrapper;
|
||||
import org.keycloak.crypto.SignatureProvider;
|
||||
import org.keycloak.crypto.SignatureSignerContext;
|
||||
import org.keycloak.http.HttpResponse;
|
||||
import org.keycloak.jose.jwe.JWEConstants;
|
||||
import org.keycloak.jose.jwk.JSONWebKeySet;
|
||||
import org.keycloak.jose.jwk.JWK;
|
||||
@ -38,13 +40,15 @@ import org.keycloak.models.oid4vci.CredentialScopeModel;
|
||||
import org.keycloak.protocol.oid4vc.OID4VCLoginProtocolFactory;
|
||||
import org.keycloak.protocol.oid4vc.issuance.credentialbuilder.CredentialBuilder;
|
||||
import org.keycloak.protocol.oid4vc.model.CredentialIssuer;
|
||||
import java.net.URI;
|
||||
|
||||
import org.keycloak.protocol.oid4vc.model.CredentialRequestEncryptionMetadata;
|
||||
import org.keycloak.protocol.oid4vc.model.CredentialResponseEncryptionMetadata;
|
||||
import org.keycloak.protocol.oid4vc.model.CredentialRequestEncryptionMetadata;
|
||||
import org.keycloak.protocol.oid4vc.model.SupportedCredentialConfiguration;
|
||||
import org.keycloak.representations.JsonWebToken;
|
||||
import org.keycloak.protocol.oidc.utils.JWKSServerUtils;
|
||||
import org.keycloak.services.Urls;
|
||||
import org.keycloak.services.resources.ServerMetadataResource;
|
||||
import org.keycloak.urls.UrlType;
|
||||
import org.keycloak.util.JsonSerialization;
|
||||
import org.keycloak.utils.MediaType;
|
||||
@ -129,6 +133,9 @@ public class OID4VCIssuerWellKnownProvider implements WellKnownProvider {
|
||||
}
|
||||
}
|
||||
|
||||
// Add deprecation headers/logs if the old realm-scoped route was used
|
||||
addDeprecationHeadersIfOldRoute(keycloakSession);
|
||||
|
||||
return new CredentialIssuer()
|
||||
.setCredentialIssuer(getIssuer(context))
|
||||
.setCredentialEndpoint(getCredentialsEndpoint(context))
|
||||
@ -155,6 +162,7 @@ public class OID4VCIssuerWellKnownProvider implements WellKnownProvider {
|
||||
LOGGER.debugf("Falling back to JSON response due to signed metadata failure for realm: %s", realm.getName());
|
||||
}
|
||||
}
|
||||
|
||||
return issuer;
|
||||
}
|
||||
|
||||
@ -523,4 +531,36 @@ public class OID4VCIssuerWellKnownProvider implements WellKnownProvider {
|
||||
return CryptoUtils.getSupportedAsymmetricSignatureAlgorithms(session);
|
||||
}
|
||||
|
||||
/**
|
||||
* Attach OID4VCI-specific deprecation headers (and a server WARN) when the old
|
||||
* realm-scoped route is used.
|
||||
* old: /realms/{realm}/.well-known/openid-credential-issuer
|
||||
* new: /.well-known/openid-credential-issuer/realms/{realm}
|
||||
*/
|
||||
private void addDeprecationHeadersIfOldRoute(KeycloakSession session) {
|
||||
String requestPath = session.getContext().getUri().getRequestUri().getPath();
|
||||
if (requestPath == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
int idxRealms = requestPath.indexOf("/realms/");
|
||||
int idxWellKnown = requestPath.indexOf("/.well-known/");
|
||||
boolean isOldRoute = idxRealms >= 0 && idxWellKnown > idxRealms;
|
||||
if (!isOldRoute) {
|
||||
return;
|
||||
}
|
||||
|
||||
UriBuilder base = session.getContext().getUri().getBaseUriBuilder();
|
||||
String logKey = session.getContext().getRealm().getName();
|
||||
URI successor = ServerMetadataResource.wellKnownOAuthProviderUrl(base)
|
||||
.build(Oid4VciConstants.WELL_KNOWN_OPENID_CREDENTIAL_ISSUER, logKey);
|
||||
|
||||
HttpResponse httpResponse = session.getContext().getHttpResponse();
|
||||
httpResponse.setHeader("Warning", "299 - \"Deprecated endpoint; use " + successor + "\"");
|
||||
httpResponse.setHeader("Deprecation", "true");
|
||||
httpResponse.setHeader("Link", "<" + successor + ">; rel=\"successor-version\"");
|
||||
|
||||
LOGGER.warnf("Deprecated realm-scoped well-known endpoint accessed for OID4VCI in realm '%s'. Use %s instead.", logKey, successor);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -29,8 +29,10 @@ import jakarta.ws.rs.core.UriBuilder;
|
||||
import jakarta.ws.rs.ext.Provider;
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.protocol.oid4vc.issuance.OID4VCIssuerWellKnownProviderFactory;
|
||||
import org.keycloak.protocol.oauth2.OAuth2WellKnownProviderFactory;
|
||||
import org.keycloak.services.cors.Cors;
|
||||
import static org.keycloak.utils.MediaType.APPLICATION_JWT;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@ -54,7 +56,7 @@ public class ServerMetadataResource {
|
||||
|
||||
@GET
|
||||
@Path("{provider}/realms/{realm}")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@Produces({MediaType.APPLICATION_JSON, APPLICATION_JWT})
|
||||
public Response getOAuth2AuthorizationServerWellKnown(final @PathParam("provider") String providerName,
|
||||
final @PathParam("realm") String name) {
|
||||
if (!isValidProvider(providerName)) throw new NotFoundException();
|
||||
@ -68,6 +70,7 @@ public class ServerMetadataResource {
|
||||
private boolean isValidProvider(String providerName) {
|
||||
// you can add codes here considering the current status of the implementation (preview, experimental).
|
||||
if (OAuth2WellKnownProviderFactory.PROVIDER_ID.equals(providerName)) return true;
|
||||
if (OID4VCIssuerWellKnownProviderFactory.PROVIDER_ID.equals(providerName)) return true;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@ -23,6 +23,7 @@ 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.apache.http.Header;
|
||||
import org.apache.http.HttpHeaders;
|
||||
import org.apache.http.HttpStatus;
|
||||
import org.apache.http.client.methods.CloseableHttpResponse;
|
||||
@ -702,6 +703,65 @@ public class OID4VCIssuerWellKnownProviderTest extends OID4VCIssuerEndpointTest
|
||||
testBatchSizeValidation(testingClient, "invalid", false, null);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testOldOidcDiscoveryCompliantWellKnownUrlWithDeprecationHeaders() {
|
||||
try (CloseableHttpClient httpClient = HttpClientBuilder.create().build()) {
|
||||
// Old OIDC Discovery compliant URL
|
||||
String oldWellKnownUri = OAuthClient.AUTH_SERVER_ROOT + "/realms/" + TEST_REALM_NAME + "/.well-known/openid-credential-issuer";
|
||||
String expectedIssuer = getRealmPath(TEST_REALM_NAME);
|
||||
|
||||
HttpGet getMetadata = new HttpGet(oldWellKnownUri);
|
||||
getMetadata.addHeader(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON);
|
||||
|
||||
try (CloseableHttpResponse response = httpClient.execute(getMetadata)) {
|
||||
// Status & Content-Type
|
||||
assertEquals("Old well-known URL should return 200 OK",
|
||||
HttpStatus.SC_OK, response.getStatusLine().getStatusCode());
|
||||
|
||||
String contentType = response.getFirstHeader(HttpHeaders.CONTENT_TYPE).getValue();
|
||||
assertTrue("Content-Type should be application/json",
|
||||
contentType.startsWith(MediaType.APPLICATION_JSON));
|
||||
|
||||
// Headers
|
||||
Header warning = response.getFirstHeader("Warning");
|
||||
Header deprecation = response.getFirstHeader("Deprecation");
|
||||
Header link = response.getFirstHeader("Link");
|
||||
|
||||
assertNotNull("Should have deprecation warning header", warning);
|
||||
assertTrue("Warning header should contain deprecation message",
|
||||
warning.getValue().contains("Deprecated endpoint"));
|
||||
|
||||
assertNotNull("Should have deprecation header", deprecation);
|
||||
assertEquals("Deprecation header should be 'true'", "true", deprecation.getValue());
|
||||
|
||||
assertNotNull("Should have successor link header", link);
|
||||
assertTrue("Link header should contain successor-version",
|
||||
link.getValue().contains("successor-version"));
|
||||
|
||||
// Response body
|
||||
String json = EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8);
|
||||
CredentialIssuer issuer = JsonSerialization.readValue(json, CredentialIssuer.class);
|
||||
|
||||
assertNotNull("Response should be a CredentialIssuer object", issuer);
|
||||
|
||||
assertEquals("credential_issuer should be set",
|
||||
expectedIssuer, issuer.getCredentialIssuer());
|
||||
assertEquals("credential_endpoint should be correct",
|
||||
expectedIssuer + "/protocol/oid4vc/credential", issuer.getCredentialEndpoint());
|
||||
assertEquals("nonce_endpoint should be correct",
|
||||
expectedIssuer + "/protocol/oid4vc/nonce", issuer.getNonceEndpoint());
|
||||
assertEquals("deferred_credential_endpoint should be correct",
|
||||
expectedIssuer + "/protocol/oid4vc/deferred_credential", issuer.getDeferredCredentialEndpoint());
|
||||
|
||||
assertNotNull("authorization_servers should be present", issuer.getAuthorizationServers());
|
||||
assertNotNull("credential_response_encryption should be present", issuer.getCredentialResponseEncryption());
|
||||
assertNotNull("batch_credential_issuance should be present", issuer.getBatchCredentialIssuance());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("Failed to process old well-known URL response: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
private void testBatchSizeValidation(KeycloakTestingClient testingClient, String batchSize, boolean shouldBePresent, Integer expectedValue) {
|
||||
testingClient
|
||||
.server(TEST_REALM_NAME)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user