JWT VC Issuer Metadata /.well-known/jwt-vc-issuer to comply with SD-JWT VC Specification (#29635)
closes #29634 Signed-off-by: Francis Pouatcha <francis.pouatcha@adorsys.com> Co-authored-by: DYLANE BENGONO <85441363+bengo237@users.noreply.github.com>
This commit is contained in:
parent
68d9dcecb5
commit
4317a474d1
7 changed files with 320 additions and 31 deletions
|
@ -0,0 +1,61 @@
|
|||
/*
|
||||
* Copyright 2024 Red Hat, Inc. and/or its affiliates
|
||||
* and other contributors as indicated by the @author tags.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.keycloak.protocol.oid4vc.issuance;
|
||||
|
||||
import jakarta.ws.rs.core.UriInfo;
|
||||
import org.keycloak.jose.jwk.JSONWebKeySet;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.protocol.oid4vc.model.JWTVCIssuerMetadata;
|
||||
import org.keycloak.protocol.oidc.utils.JWKSServerUtils;
|
||||
import org.keycloak.services.Urls;
|
||||
import org.keycloak.urls.UrlType;
|
||||
import org.keycloak.wellknown.WellKnownProvider;
|
||||
|
||||
/**
|
||||
* {@link WellKnownProvider} implementation for JWT VC Issuer metadata at endpoint /.well-known/jwt-vc-issuer
|
||||
* <p/>
|
||||
* {@see https://www.ietf.org/archive/id/draft-ietf-oauth-sd-jwt-vc-03.html#name-jwt-vc-issuer-metadata}
|
||||
*
|
||||
* @author <a href="mailto:francis.pouatcha@adorsys.com">Francis Pouatcha</a>
|
||||
*/
|
||||
public class JWTVCIssuerWellKnownProvider implements WellKnownProvider {
|
||||
private final KeycloakSession session;
|
||||
|
||||
public JWTVCIssuerWellKnownProvider(KeycloakSession session) {
|
||||
this.session = session;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
// no-op
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getConfig() {
|
||||
UriInfo frontendUriInfo = session.getContext().getUri(UrlType.FRONTEND);
|
||||
RealmModel realm = session.getContext().getRealm();
|
||||
|
||||
JWTVCIssuerMetadata config = new JWTVCIssuerMetadata();
|
||||
config.setIssuer(Urls.realmIssuer(frontendUriInfo.getBaseUri(), realm.getName()));
|
||||
|
||||
JSONWebKeySet jwks = JWKSServerUtils.getRealmJwks(session, realm);
|
||||
config.setJwks(jwks);
|
||||
|
||||
return config;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,61 @@
|
|||
/*
|
||||
* Copyright 2024 Red Hat, Inc. and/or its affiliates
|
||||
* and other contributors as indicated by the @author tags.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.keycloak.protocol.oid4vc.issuance;
|
||||
|
||||
import org.keycloak.Config;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.KeycloakSessionFactory;
|
||||
import org.keycloak.protocol.oid4vc.OID4VCEnvironmentProviderFactory;
|
||||
import org.keycloak.wellknown.WellKnownProvider;
|
||||
import org.keycloak.wellknown.WellKnownProviderFactory;
|
||||
|
||||
/**
|
||||
* {@link WellKnownProviderFactory} implementation for JWT VC Issuer metadata at endpoint /.well-known/jwt-vc-issuer
|
||||
*
|
||||
* {@see https://www.ietf.org/archive/id/draft-ietf-oauth-sd-jwt-vc-03.html#name-jwt-vc-issuer-metadata}
|
||||
*
|
||||
* @author <a href="mailto:francis.pouatcha@adorsys.com">Francis Pouatcha</a>
|
||||
*/
|
||||
public class JWTVCIssuerWellKnownProviderFactory implements WellKnownProviderFactory, OID4VCEnvironmentProviderFactory {
|
||||
|
||||
public static final String PROVIDER_ID = "jwt-vc-issuer";
|
||||
|
||||
@Override
|
||||
public WellKnownProvider create(KeycloakSession session) {
|
||||
return new JWTVCIssuerWellKnownProvider(session);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init(Config.Scope config) {
|
||||
// no-op
|
||||
}
|
||||
|
||||
@Override
|
||||
public void postInit(KeycloakSessionFactory factory) {
|
||||
// no-op
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
// no-op
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return PROVIDER_ID;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,53 @@
|
|||
/*
|
||||
* Copyright 2024 Red Hat, Inc. and/or its affiliates
|
||||
* and other contributors as indicated by the @author tags.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.keycloak.protocol.oid4vc.model;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import org.keycloak.jose.jwk.JSONWebKeySet;
|
||||
import org.keycloak.jose.jwk.JWK;
|
||||
|
||||
/**
|
||||
* JWT VC Issuer metadata for endpoint /.well-known/jwt-vc-issuer
|
||||
* <p/>
|
||||
* {@see https://www.ietf.org/archive/id/draft-ietf-oauth-sd-jwt-vc-03.html#name-jwt-vc-issuer-metadata}
|
||||
*
|
||||
* @author <a href="mailto:francis.pouatcha@adorsys.com">Francis Pouatcha</a>
|
||||
*/
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
public class JWTVCIssuerMetadata {
|
||||
@JsonProperty("issuer")
|
||||
private String issuer;
|
||||
@JsonProperty("jwks")
|
||||
private JSONWebKeySet jwks;
|
||||
|
||||
public String getIssuer() {
|
||||
return issuer;
|
||||
}
|
||||
|
||||
public void setIssuer(String issuer) {
|
||||
this.issuer = issuer;
|
||||
}
|
||||
|
||||
public JSONWebKeySet getJwks() {
|
||||
return jwks;
|
||||
}
|
||||
|
||||
public void setJwks(JSONWebKeySet jwks) {
|
||||
this.jwks = jwks;
|
||||
}
|
||||
}
|
|
@ -17,20 +17,13 @@
|
|||
|
||||
package org.keycloak.protocol.oidc;
|
||||
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import org.jboss.resteasy.reactive.NoCache;
|
||||
import org.keycloak.http.HttpRequest;
|
||||
import org.keycloak.OAuthErrorException;
|
||||
import org.keycloak.common.ClientConnection;
|
||||
import org.keycloak.crypto.KeyType;
|
||||
import org.keycloak.events.EventBuilder;
|
||||
import org.keycloak.forms.login.LoginFormsProvider;
|
||||
import org.keycloak.jose.jwk.JSONWebKeySet;
|
||||
import org.keycloak.jose.jwk.JWK;
|
||||
import org.keycloak.jose.jwk.JWKBuilder;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.protocol.oidc.endpoints.AuthorizationEndpoint;
|
||||
|
@ -41,13 +34,12 @@ import org.keycloak.protocol.oidc.endpoints.TokenEndpoint;
|
|||
import org.keycloak.protocol.oidc.endpoints.TokenRevocationEndpoint;
|
||||
import org.keycloak.protocol.oidc.endpoints.UserInfoEndpoint;
|
||||
import org.keycloak.protocol.oidc.ext.OIDCExtProvider;
|
||||
import org.keycloak.protocol.oidc.utils.JWKSServerUtils;
|
||||
import org.keycloak.services.CorsErrorResponseException;
|
||||
import org.keycloak.services.cors.Cors;
|
||||
import org.keycloak.services.resources.RealmsResource;
|
||||
import org.keycloak.services.util.CacheControlUtil;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
import jakarta.ws.rs.GET;
|
||||
import jakarta.ws.rs.NotFoundException;
|
||||
import jakarta.ws.rs.OPTIONS;
|
||||
|
@ -209,27 +201,7 @@ public class OIDCLoginProtocolService {
|
|||
public Response certs() {
|
||||
checkSsl();
|
||||
|
||||
JWK[] jwks = session.keys().getKeysStream(realm)
|
||||
.filter(k -> k.getStatus().isEnabled() && k.getPublicKey() != null)
|
||||
.map(k -> {
|
||||
JWKBuilder b = JWKBuilder.create().kid(k.getKid()).algorithm(k.getAlgorithmOrDefault());
|
||||
List<X509Certificate> certificates = Optional.ofNullable(k.getCertificateChain())
|
||||
.filter(certs -> !certs.isEmpty())
|
||||
.orElseGet(() -> Collections.singletonList(k.getCertificate()));
|
||||
if (k.getType().equals(KeyType.RSA)) {
|
||||
return b.rsa(k.getPublicKey(), certificates, k.getUse());
|
||||
} else if (k.getType().equals(KeyType.EC)) {
|
||||
return b.ec(k.getPublicKey(), k.getUse());
|
||||
} else if (k.getType().equals(KeyType.OKP)) {
|
||||
return b.okp(k.getPublicKey(), k.getUse());
|
||||
}
|
||||
return null;
|
||||
})
|
||||
.filter(Objects::nonNull)
|
||||
.toArray(JWK[]::new);
|
||||
|
||||
JSONWebKeySet keySet = new JSONWebKeySet();
|
||||
keySet.setKeys(jwks);
|
||||
JSONWebKeySet keySet = JWKSServerUtils.getRealmJwks(session, realm);
|
||||
|
||||
Response.ResponseBuilder responseBuilder = Response.ok(keySet).cacheControl(CacheControlUtil.getDefaultCacheControl());
|
||||
return Cors.builder().allowedOrigins("*").auth().add(responseBuilder);
|
||||
|
|
|
@ -0,0 +1,61 @@
|
|||
/*
|
||||
* Copyright 2024 Red Hat, Inc. and/or its affiliates
|
||||
* and other contributors as indicated by the @author tags.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.keycloak.protocol.oidc.utils;
|
||||
|
||||
import org.keycloak.crypto.KeyType;
|
||||
import org.keycloak.jose.jwk.JSONWebKeySet;
|
||||
import org.keycloak.jose.jwk.JWK;
|
||||
import org.keycloak.jose.jwk.JWKBuilder;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.wellknown.WellKnownProvider;
|
||||
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author <a href="mailto:francis.pouatcha@adorsys.com">Francis Pouatcha</a>
|
||||
*/public class JWKSServerUtils {
|
||||
public static JSONWebKeySet getRealmJwks(KeycloakSession session, RealmModel realm){
|
||||
JWK[] jwks = session.keys().getKeysStream(realm)
|
||||
.filter(k -> k.getStatus().isEnabled() && k.getPublicKey() != null)
|
||||
.map(k -> {
|
||||
JWKBuilder b = JWKBuilder.create().kid(k.getKid()).algorithm(k.getAlgorithmOrDefault());
|
||||
List<X509Certificate> certificates = Optional.ofNullable(k.getCertificateChain())
|
||||
.filter(certs -> !certs.isEmpty())
|
||||
.orElseGet(() -> Collections.singletonList(k.getCertificate()));
|
||||
if (k.getType().equals(KeyType.RSA)) {
|
||||
return b.rsa(k.getPublicKey(), certificates, k.getUse());
|
||||
} else if (k.getType().equals(KeyType.EC)) {
|
||||
return b.ec(k.getPublicKey(), k.getUse());
|
||||
} else if (k.getType().equals(KeyType.OKP)) {
|
||||
return b.okp(k.getPublicKey(), k.getUse());
|
||||
}
|
||||
return null;
|
||||
})
|
||||
.filter(Objects::nonNull)
|
||||
.toArray(JWK[]::new);
|
||||
|
||||
JSONWebKeySet keySet = new JSONWebKeySet();
|
||||
keySet.setKeys(jwks);
|
||||
return keySet;
|
||||
}
|
||||
}
|
|
@ -18,4 +18,5 @@
|
|||
org.keycloak.protocol.oidc.OIDCWellKnownProviderFactory
|
||||
org.keycloak.authorization.config.UmaWellKnownProviderFactory
|
||||
org.keycloak.protocol.oid4vc.issuance.OID4VCIssuerWellKnownProviderFactory
|
||||
org.keycloak.protocol.oauth2.OAuth2WellKnownProviderFactory
|
||||
org.keycloak.protocol.oid4vc.issuance.JWTVCIssuerWellKnownProviderFactory
|
||||
org.keycloak.protocol.oauth2.OAuth2WellKnownProviderFactory
|
||||
|
|
|
@ -0,0 +1,80 @@
|
|||
/*
|
||||
* Copyright 2024 Red Hat, Inc. and/or its affiliates
|
||||
* and other contributors as indicated by the @author tags.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.keycloak.testsuite.oid4vc.issuance.signing;
|
||||
|
||||
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.junit.Test;
|
||||
import org.keycloak.common.util.MultivaluedHashMap;
|
||||
import org.keycloak.jose.jwk.JSONWebKeySet;
|
||||
import org.keycloak.protocol.oid4vc.issuance.JWTVCIssuerWellKnownProviderFactory;
|
||||
import org.keycloak.protocol.oid4vc.model.JWTVCIssuerMetadata;
|
||||
import org.keycloak.representations.idm.RealmRepresentation;
|
||||
import org.keycloak.services.resources.RealmsResource;
|
||||
import org.keycloak.testsuite.util.AdminClientUtil;
|
||||
import org.keycloak.testsuite.util.OAuthClient;
|
||||
import org.keycloak.util.JsonSerialization;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static junit.framework.Assert.assertTrue;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
* @author <a href="mailto:francis.pouatcha@adorsys.com">Francis Pouatcha</a>
|
||||
*/public class JWTVCIssuerWellKnownProviderTest extends OID4VCTest {
|
||||
|
||||
@Test
|
||||
public void getConfig() throws IOException {
|
||||
String expectedIssuer = suiteContext.getAuthServerInfo().getContextRoot().toString() + "/auth/realms/" + TEST_REALM_NAME;
|
||||
|
||||
try (Client client = AdminClientUtil.createResteasyClient()) {
|
||||
UriBuilder builder = UriBuilder.fromUri(OAuthClient.AUTH_SERVER_ROOT);
|
||||
URI jwtIssuerUri = RealmsResource.wellKnownProviderUrl(builder).build("test", JWTVCIssuerWellKnownProviderFactory.PROVIDER_ID);
|
||||
WebTarget jwtIssuerTarget = client.target(jwtIssuerUri);
|
||||
|
||||
try (Response jwtIssuerResponse = jwtIssuerTarget.request().get()) {
|
||||
JWTVCIssuerMetadata jwtvcIssuerMetadata = JsonSerialization.readValue(jwtIssuerResponse.readEntity(String.class), JWTVCIssuerMetadata.class);
|
||||
assertEquals("The correct issuer should be included.", expectedIssuer, jwtvcIssuerMetadata.getIssuer());
|
||||
JSONWebKeySet jwks = jwtvcIssuerMetadata.getJwks();
|
||||
assertNotNull("The key set shall not be null", jwks.getKeys());
|
||||
assertTrue("The key set shall not be empty", jwks.getKeys().length > 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void configureTestRealm(RealmRepresentation testRealm) {
|
||||
if (testRealm.getComponents() != null) {
|
||||
testRealm.getComponents().add("org.keycloak.keys.KeyProvider", getRsaKeyProvider(RSA_KEY));
|
||||
testRealm.getComponents().add("org.keycloak.protocol.oid4vc.issuance.signing.VerifiableCredentialsSigningService", getJwtSigningProvider(RSA_KEY));
|
||||
} else {
|
||||
testRealm.setComponents(new MultivaluedHashMap<>(
|
||||
Map.of("org.keycloak.keys.KeyProvider", List.of(getRsaKeyProvider(RSA_KEY)),
|
||||
"org.keycloak.protocol.oid4vc.issuance.signing.VerifiableCredentialsSigningService", List.of(getJwtSigningProvider(RSA_KEY))
|
||||
)));
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue