OID4VCClientRegistrationProvider.java
/*
* 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;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.DELETE;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.PUT;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import org.jboss.logging.Logger;
import org.keycloak.models.KeycloakSession;
import static org.keycloak.protocol.oid4vc.issuance.OID4VCIssuerWellKnownProvider.VC_KEY;
import org.keycloak.protocol.oid4vc.model.OID4VCClient;
import org.keycloak.protocol.oid4vc.model.SupportedCredentialConfiguration;
import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.services.ErrorResponseException;
import org.keycloak.services.clientregistration.AbstractClientRegistrationProvider;
import org.keycloak.services.clientregistration.DefaultClientRegistrationContext;
import java.net.URI;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;
/**
* Provides the client-registration functionality for OID4VC-clients.
*
* @author <a href="https://github.com/wistefan">Stefan Wiedemann</a>
*/
public class OID4VCClientRegistrationProvider extends AbstractClientRegistrationProvider {
private static final Logger LOGGER = Logger.getLogger(OID4VCClientRegistrationProvider.class);
public OID4VCClientRegistrationProvider(KeycloakSession session) {
super(session);
}
@POST
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public Response createOID4VCClient(OID4VCClient client) {
ClientRepresentation clientRepresentation = toClientRepresentation(client);
validate(clientRepresentation);
ClientRepresentation cr = create(
new DefaultClientRegistrationContext(session, clientRepresentation, this));
URI uri = session.getContext().getUri().getAbsolutePathBuilder().path(cr.getClientId()).build();
return Response.created(uri).entity(cr).build();
}
@PUT
@Path("{clientId}")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public Response updateOID4VCClient(@PathParam("clientId") String clientDid, OID4VCClient client) {
client.setClientDid(clientDid);
ClientRepresentation clientRepresentation = toClientRepresentation(client);
validate(clientRepresentation);
clientRepresentation = update(clientDid,
new DefaultClientRegistrationContext(session, clientRepresentation, this));
return Response.ok(clientRepresentation).build();
}
@DELETE
@Path("{clientId}")
public Response deleteOID4VCClient(@PathParam("clientId") String clientDid) {
delete(clientDid);
return Response.noContent().build();
}
/**
* Validates the clientRepresentation to fulfill the requirement of an OID4VC client
*/
public static void validate(ClientRepresentation client) {
String did = client.getClientId();
if (did == null) {
throw new ErrorResponseException("no_did", "A client did needs to be configured for OID4VC clients",
Response.Status.BAD_REQUEST);
}
if (!did.startsWith("did:")) {
throw new ErrorResponseException("invalid_did", "The client id is not a did.",
Response.Status.BAD_REQUEST);
}
}
/**
* Translate an incoming {@link OID4VCClient} into a keycloak native {@link ClientRepresentation}.
*
* @param oid4VCClient pojo, containing the oid4vc client parameters
* @return a clientRepresentation
*/
protected static ClientRepresentation toClientRepresentation(OID4VCClient oid4VCClient) {
ClientRepresentation clientRepresentation = new ClientRepresentation();
clientRepresentation.setProtocol(OID4VCLoginProtocolFactory.PROTOCOL_ID);
clientRepresentation.setId(Optional.ofNullable(oid4VCClient.getId()).orElse(UUID.randomUUID().toString()));
clientRepresentation.setClientId(oid4VCClient.getClientDid());
// only add non-null parameters
Optional.ofNullable(oid4VCClient.getDescription()).ifPresent(clientRepresentation::setDescription);
Optional.ofNullable(oid4VCClient.getName()).ifPresent(clientRepresentation::setName);
Map<String, String> clientAttributes = oid4VCClient.getSupportedVCTypes()
.stream()
.map(SupportedCredentialConfiguration::toDotNotation)
.flatMap(dotNotated -> dotNotated.entrySet().stream())
.collect(Collectors.toMap(entry -> VC_KEY + "." + entry.getKey(), Map.Entry::getValue, (e1, e2) -> e1));
if (!clientAttributes.isEmpty()) {
clientRepresentation.setAttributes(clientAttributes);
}
LOGGER.debugf("Generated client representation {}.", clientRepresentation);
return clientRepresentation;
}
public static OID4VCClient fromClientAttributes(String clientId, Map<String, String> clientAttributes) {
OID4VCClient oid4VCClient = new OID4VCClient()
.setClientDid(clientId);
Set<String> supportedCredentialIds = new HashSet<>();
Map<String, String> attributes = new HashMap<>();
clientAttributes
.entrySet()
.forEach(entry -> {
if (!entry.getKey().startsWith(VC_KEY)) {
return;
}
String key = entry.getKey().substring((VC_KEY + ".").length());
supportedCredentialIds.add(key.split("\\.")[0]);
attributes.put(key, entry.getValue());
});
List<SupportedCredentialConfiguration> supportedCredentialConfigurations = supportedCredentialIds
.stream()
.map(id -> SupportedCredentialConfiguration.fromDotNotation(id, attributes))
.toList();
return oid4VCClient.setSupportedVCTypes(supportedCredentialConfigurations);
}
}