Support credentials issuance through oid4vci (#27931)
closes #25940 Signed-off-by: Stefan Wiedemann <wistefan@googlemail.com>
This commit is contained in:
parent
7e034dbbe0
commit
b08c644601
59 changed files with 4706 additions and 164 deletions
|
@ -0,0 +1,169 @@
|
|||
/*
|
||||
* 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 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);
|
||||
|
||||
private static final String VC_KEY = "vc";
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,68 @@
|
|||
/*
|
||||
* 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 org.keycloak.Config;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.KeycloakSessionFactory;
|
||||
import org.keycloak.provider.ProviderConfigProperty;
|
||||
import org.keycloak.services.clientregistration.ClientRegistrationProvider;
|
||||
import org.keycloak.services.clientregistration.ClientRegistrationProviderFactory;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Implementation of the {@link ClientRegistrationProviderFactory} to integrate the OID4VC protocols with
|
||||
* Keycloak's client-registration.
|
||||
* <p>
|
||||
* {@see https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html}
|
||||
*
|
||||
* @author <a href="https://github.com/wistefan">Stefan Wiedemann</a>
|
||||
*/
|
||||
public class OID4VCClientRegistrationProviderFactory implements ClientRegistrationProviderFactory, OID4VCEnvironmentProviderFactory {
|
||||
|
||||
@Override
|
||||
public ClientRegistrationProvider create(KeycloakSession session) {
|
||||
return new OID4VCClientRegistrationProvider(session);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init(Config.Scope config) {
|
||||
// no config required
|
||||
}
|
||||
|
||||
@Override
|
||||
public void postInit(KeycloakSessionFactory factory) {
|
||||
// nothing to do post init
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
// no resources to close
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return OID4VCLoginProtocolFactory.PROTOCOL_ID;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ProviderConfigProperty> getConfigMetadata() {
|
||||
return List.of();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
/*
|
||||
* 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 org.keycloak.Config;
|
||||
import org.keycloak.common.Profile;
|
||||
import org.keycloak.provider.EnvironmentDependentProviderFactory;
|
||||
|
||||
/**
|
||||
* Interface for all OID4VC related provider factories, to ensure usage of the same feature flag.
|
||||
*/
|
||||
public interface OID4VCEnvironmentProviderFactory extends EnvironmentDependentProviderFactory {
|
||||
|
||||
@Override
|
||||
default boolean isSupported(Config.Scope config) {
|
||||
return Profile.isFeatureEnabled(Profile.Feature.OID4VC_VCI);
|
||||
}
|
||||
|
||||
@Override
|
||||
default boolean isSupported() {
|
||||
return Profile.isFeatureEnabled(Profile.Feature.OID4VC_VCI);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,173 @@
|
|||
/*
|
||||
* 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 com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.Config;
|
||||
import org.keycloak.component.ComponentModel;
|
||||
import org.keycloak.events.EventBuilder;
|
||||
import org.keycloak.models.ClientModel;
|
||||
import org.keycloak.models.ClientScopeModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.KeycloakSessionFactory;
|
||||
import org.keycloak.models.ProtocolMapperModel;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.utils.KeycloakModelUtils;
|
||||
import org.keycloak.protocol.LoginProtocol;
|
||||
import org.keycloak.protocol.LoginProtocolFactory;
|
||||
import org.keycloak.protocol.oid4vc.issuance.OID4VCIssuerEndpoint;
|
||||
import org.keycloak.protocol.oid4vc.issuance.OffsetTimeProvider;
|
||||
import org.keycloak.protocol.oid4vc.issuance.VCIssuerException;
|
||||
import org.keycloak.protocol.oid4vc.issuance.mappers.OID4VCSubjectIdMapper;
|
||||
import org.keycloak.protocol.oid4vc.issuance.mappers.OID4VCTargetRoleMapper;
|
||||
import org.keycloak.protocol.oid4vc.issuance.mappers.OID4VCUserAttributeMapper;
|
||||
import org.keycloak.protocol.oid4vc.issuance.signing.VCSigningServiceProviderFactory;
|
||||
import org.keycloak.protocol.oid4vc.issuance.signing.VerifiableCredentialsSigningService;
|
||||
import org.keycloak.protocol.oid4vc.model.Format;
|
||||
import org.keycloak.provider.ProviderFactory;
|
||||
import org.keycloak.representations.idm.ClientRepresentation;
|
||||
import org.keycloak.services.managers.AppAuthManager;
|
||||
|
||||
import java.util.EnumMap;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* Factory for creating all OID4VC related endpoints and the default mappers.
|
||||
*
|
||||
* @author <a href="https://github.com/wistefan">Stefan Wiedemann</a>
|
||||
*/
|
||||
public class OID4VCLoginProtocolFactory implements LoginProtocolFactory, OID4VCEnvironmentProviderFactory {
|
||||
|
||||
private static final Logger LOGGER = Logger.getLogger(OID4VCLoginProtocolFactory.class);
|
||||
|
||||
public static final String PROTOCOL_ID = "oid4vc";
|
||||
|
||||
private static final String ISSUER_DID_REALM_ATTRIBUTE_KEY = "issuerDid";
|
||||
private static final String CODE_LIFESPAN_REALM_ATTRIBUTE_KEY = "preAuthorizedCodeLifespanS";
|
||||
private static final int DEFAULT_CODE_LIFESPAN_S = 30;
|
||||
|
||||
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
|
||||
private static final String CLIENT_ROLES_MAPPER = "client-roles";
|
||||
private static final String USERNAME_MAPPER = "username";
|
||||
private static final String SUBJECT_ID_MAPPER = "subject-id";
|
||||
private static final String EMAIL_MAPPER = "email";
|
||||
private static final String LAST_NAME_MAPPER = "last-name";
|
||||
private static final String FIRST_NAME_MAPPER = "first-name";
|
||||
|
||||
private Map<String, ProtocolMapperModel> builtins = new HashMap<>();
|
||||
|
||||
@Override
|
||||
public void init(Config.Scope config) {
|
||||
builtins.put(CLIENT_ROLES_MAPPER, OID4VCTargetRoleMapper.create("id", "client roles"));
|
||||
builtins.put(SUBJECT_ID_MAPPER, OID4VCSubjectIdMapper.create("subject id", "id"));
|
||||
builtins.put(USERNAME_MAPPER, OID4VCUserAttributeMapper.create(USERNAME_MAPPER, "username", "username", false));
|
||||
builtins.put(EMAIL_MAPPER, OID4VCUserAttributeMapper.create(EMAIL_MAPPER, "email", "email", false));
|
||||
builtins.put(FIRST_NAME_MAPPER, OID4VCUserAttributeMapper.create(FIRST_NAME_MAPPER, "firstName", "firstName", false));
|
||||
builtins.put(LAST_NAME_MAPPER, OID4VCUserAttributeMapper.create(LAST_NAME_MAPPER, "lastName", "familyName", false));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void postInit(KeycloakSessionFactory factory) {
|
||||
// no-op
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
// no-op
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, ProtocolMapperModel> getBuiltinMappers() {
|
||||
return builtins;
|
||||
}
|
||||
|
||||
private void addServiceFromComponent(Map<Format, VerifiableCredentialsSigningService> signingServices, KeycloakSession keycloakSession, ComponentModel componentModel) {
|
||||
ProviderFactory<VerifiableCredentialsSigningService> factory = keycloakSession
|
||||
.getKeycloakSessionFactory()
|
||||
.getProviderFactory(VerifiableCredentialsSigningService.class, componentModel.getProviderId());
|
||||
if (factory instanceof VCSigningServiceProviderFactory sspf) {
|
||||
signingServices.put(sspf.supportedFormat(), sspf.create(keycloakSession, componentModel));
|
||||
} else {
|
||||
throw new IllegalArgumentException(String.format("The component %s is not a VerifiableCredentialsSigningServiceProviderFactory", componentModel.getProviderId()));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object createProtocolEndpoint(KeycloakSession keycloakSession, EventBuilder event) {
|
||||
|
||||
Map<Format, VerifiableCredentialsSigningService> signingServices = new EnumMap<>(Format.class);
|
||||
RealmModel realm = keycloakSession.getContext().getRealm();
|
||||
realm.getComponentsStream(realm.getId(), VerifiableCredentialsSigningService.class.getName())
|
||||
.forEach(cm -> addServiceFromComponent(signingServices, keycloakSession, cm));
|
||||
|
||||
RealmModel realmModel = keycloakSession.getContext().getRealm();
|
||||
String issuerDid = Optional.ofNullable(realmModel.getAttribute(ISSUER_DID_REALM_ATTRIBUTE_KEY))
|
||||
.orElseThrow(() -> new VCIssuerException("No issuer-did configured."));
|
||||
int preAuthorizedCodeLifespan = Optional.ofNullable(realmModel.getAttribute(CODE_LIFESPAN_REALM_ATTRIBUTE_KEY))
|
||||
.map(Integer::valueOf)
|
||||
.orElse(DEFAULT_CODE_LIFESPAN_S);
|
||||
|
||||
return new OID4VCIssuerEndpoint(
|
||||
keycloakSession,
|
||||
issuerDid,
|
||||
signingServices,
|
||||
new AppAuthManager.BearerTokenAuthenticator(keycloakSession),
|
||||
OBJECT_MAPPER,
|
||||
new OffsetTimeProvider(),
|
||||
preAuthorizedCodeLifespan);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void createDefaultClientScopes(RealmModel newRealm, boolean addScopesToExistingClients) {
|
||||
LOGGER.debugf("Create default scopes for realm %s", newRealm.getName());
|
||||
|
||||
ClientScopeModel naturalPersonScope = KeycloakModelUtils.getClientScopeByName(newRealm, "natural_person");
|
||||
if (naturalPersonScope == null) {
|
||||
LOGGER.debug("Add natural person scope");
|
||||
naturalPersonScope = newRealm.addClientScope(String.format("%s_%s", PROTOCOL_ID, "natural_person"));
|
||||
naturalPersonScope.setDescription("OIDC$VP Scope, that adds all properties required for a natural person.");
|
||||
naturalPersonScope.setProtocol(PROTOCOL_ID);
|
||||
naturalPersonScope.addProtocolMapper(builtins.get(SUBJECT_ID_MAPPER));
|
||||
naturalPersonScope.addProtocolMapper(builtins.get(CLIENT_ROLES_MAPPER));
|
||||
naturalPersonScope.addProtocolMapper(builtins.get(EMAIL_MAPPER));
|
||||
naturalPersonScope.addProtocolMapper(builtins.get(FIRST_NAME_MAPPER));
|
||||
naturalPersonScope.addProtocolMapper(builtins.get(LAST_NAME_MAPPER));
|
||||
newRealm.addDefaultClientScope(naturalPersonScope, true);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setupClientDefaults(ClientRepresentation rep, ClientModel newClient) {
|
||||
//no-op
|
||||
}
|
||||
|
||||
@Override
|
||||
public LoginProtocol create(KeycloakSession session) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return PROTOCOL_ID;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,386 @@
|
|||
/*
|
||||
* 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 com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import jakarta.ws.rs.BadRequestException;
|
||||
import jakarta.ws.rs.Consumes;
|
||||
import jakarta.ws.rs.GET;
|
||||
import jakarta.ws.rs.POST;
|
||||
import jakarta.ws.rs.Path;
|
||||
import jakarta.ws.rs.PathParam;
|
||||
import jakarta.ws.rs.Produces;
|
||||
import jakarta.ws.rs.QueryParam;
|
||||
import jakarta.ws.rs.WebApplicationException;
|
||||
import jakarta.ws.rs.core.Response;
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.common.util.SecretGenerator;
|
||||
import org.keycloak.models.AuthenticatedClientSessionModel;
|
||||
import org.keycloak.models.ClientModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.ProtocolMapperContainerModel;
|
||||
import org.keycloak.models.ProtocolMapperModel;
|
||||
import org.keycloak.models.UserSessionModel;
|
||||
import org.keycloak.protocol.ProtocolMapper;
|
||||
import org.keycloak.protocol.oid4vc.OID4VCClientRegistrationProvider;
|
||||
import org.keycloak.protocol.oid4vc.OID4VCLoginProtocolFactory;
|
||||
import org.keycloak.protocol.oid4vc.issuance.mappers.OID4VCMapper;
|
||||
import org.keycloak.protocol.oid4vc.issuance.signing.VerifiableCredentialsSigningService;
|
||||
import org.keycloak.protocol.oid4vc.model.CredentialOfferURI;
|
||||
import org.keycloak.protocol.oid4vc.model.CredentialRequest;
|
||||
import org.keycloak.protocol.oid4vc.model.CredentialResponse;
|
||||
import org.keycloak.protocol.oid4vc.model.CredentialsOffer;
|
||||
import org.keycloak.protocol.oid4vc.model.ErrorResponse;
|
||||
import org.keycloak.protocol.oid4vc.model.ErrorType;
|
||||
import org.keycloak.protocol.oid4vc.model.Format;
|
||||
import org.keycloak.protocol.oid4vc.model.OID4VCClient;
|
||||
import org.keycloak.protocol.oid4vc.model.PreAuthorizedCode;
|
||||
import org.keycloak.protocol.oid4vc.model.PreAuthorizedGrant;
|
||||
import org.keycloak.protocol.oid4vc.model.SupportedCredentialConfiguration;
|
||||
import org.keycloak.protocol.oid4vc.model.VerifiableCredential;
|
||||
import org.keycloak.protocol.oidc.grants.PreAuthorizedCodeGrantType;
|
||||
import org.keycloak.services.managers.AppAuthManager;
|
||||
import org.keycloak.services.managers.AuthenticationManager;
|
||||
import org.keycloak.utils.MediaType;
|
||||
|
||||
import java.net.URI;
|
||||
import java.time.Instant;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* Provides the (REST-)endpoints required for the OID4VCI protocol.
|
||||
* <p>
|
||||
* {@see https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html}
|
||||
*
|
||||
* @author <a href="https://github.com/wistefan">Stefan Wiedemann</a>
|
||||
*/
|
||||
public class OID4VCIssuerEndpoint {
|
||||
|
||||
private static final Logger LOGGER = Logger.getLogger(OID4VCIssuerEndpoint.class);
|
||||
|
||||
public static final String CREDENTIAL_PATH = "credential";
|
||||
public static final String CREDENTIAL_OFFER_PATH = "credential-offer/";
|
||||
private final KeycloakSession session;
|
||||
private final AppAuthManager.BearerTokenAuthenticator bearerTokenAuthenticator;
|
||||
private final ObjectMapper objectMapper;
|
||||
private final TimeProvider timeProvider;
|
||||
|
||||
private final String issuerDid;
|
||||
// lifespan of the preAuthorizedCodes in seconds
|
||||
private final int preAuthorizedCodeLifeSpan;
|
||||
|
||||
private final Map<Format, VerifiableCredentialsSigningService> signingServices;
|
||||
|
||||
public OID4VCIssuerEndpoint(KeycloakSession session,
|
||||
String issuerDid,
|
||||
Map<Format, VerifiableCredentialsSigningService> signingServices,
|
||||
AppAuthManager.BearerTokenAuthenticator authenticator,
|
||||
ObjectMapper objectMapper, TimeProvider timeProvider, int preAuthorizedCodeLifeSpan) {
|
||||
this.session = session;
|
||||
this.bearerTokenAuthenticator = authenticator;
|
||||
this.objectMapper = objectMapper;
|
||||
this.timeProvider = timeProvider;
|
||||
this.issuerDid = issuerDid;
|
||||
this.signingServices = signingServices;
|
||||
this.preAuthorizedCodeLifeSpan = preAuthorizedCodeLifeSpan;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides the URI to the OID4VCI compliant credentials offer
|
||||
*/
|
||||
@GET
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@Path("credential-offer-uri")
|
||||
public Response getCredentialOfferURI(@QueryParam("credential_configuration_id") String vcId) {
|
||||
|
||||
AuthenticatedClientSessionModel clientSession = getAuthenticatedClientSession();
|
||||
|
||||
Map<String, SupportedCredentialConfiguration> credentialsMap = OID4VCIssuerWellKnownProvider.getSupportedCredentials(session);
|
||||
|
||||
LOGGER.debugf("Get an offer for %s", vcId);
|
||||
if (!credentialsMap.containsKey(vcId)) {
|
||||
LOGGER.debugf("No credential with id %s exists.", vcId);
|
||||
LOGGER.debugf("Supported credentials are %s.", credentialsMap);
|
||||
throw new BadRequestException(getErrorResponse(ErrorType.INVALID_CREDENTIAL_REQUEST));
|
||||
}
|
||||
SupportedCredentialConfiguration supportedCredentialConfiguration = credentialsMap.get(vcId);
|
||||
Format format = supportedCredentialConfiguration.getFormat();
|
||||
|
||||
// check that the user is allowed to get such credential
|
||||
if (getClientsOfType(supportedCredentialConfiguration.getScope(), format).isEmpty()) {
|
||||
LOGGER.debugf("No OID4VP-Client supporting type %s registered.", supportedCredentialConfiguration.getScope());
|
||||
throw new BadRequestException(getErrorResponse(ErrorType.UNSUPPORTED_CREDENTIAL_TYPE));
|
||||
}
|
||||
|
||||
String nonce = generateNonce();
|
||||
try {
|
||||
clientSession.setNote(nonce, objectMapper.writeValueAsString(supportedCredentialConfiguration));
|
||||
} catch (JsonProcessingException e) {
|
||||
LOGGER.errorf("Could not convert Supported Credential POJO to JSON: %s", e.getMessage());
|
||||
throw new BadRequestException(getErrorResponse(ErrorType.INVALID_CREDENTIAL_REQUEST));
|
||||
}
|
||||
|
||||
CredentialOfferURI credentialOfferURI = new CredentialOfferURI()
|
||||
.setIssuer(OID4VCIssuerWellKnownProvider.getIssuer(session.getContext()) + "/protocol/" + OID4VCLoginProtocolFactory.PROTOCOL_ID + "/" + CREDENTIAL_OFFER_PATH)
|
||||
.setNonce(nonce);
|
||||
|
||||
return Response.ok()
|
||||
.entity(credentialOfferURI)
|
||||
.build();
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides an OID4VCI compliant credentials offer
|
||||
*/
|
||||
@GET
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@Path(CREDENTIAL_OFFER_PATH + "{nonce}")
|
||||
public Response getCredentialOffer(@PathParam("nonce") String nonce) {
|
||||
if (nonce == null) {
|
||||
throw new BadRequestException(getErrorResponse(ErrorType.INVALID_CREDENTIAL_REQUEST));
|
||||
}
|
||||
|
||||
AuthenticatedClientSessionModel clientSession = getAuthenticatedClientSession();
|
||||
|
||||
String note = clientSession.getNote(nonce);
|
||||
if (note == null) {
|
||||
throw new BadRequestException(getErrorResponse(ErrorType.INVALID_CREDENTIAL_REQUEST));
|
||||
}
|
||||
|
||||
SupportedCredentialConfiguration offeredCredential;
|
||||
try {
|
||||
offeredCredential = objectMapper.readValue(note,
|
||||
SupportedCredentialConfiguration.class);
|
||||
LOGGER.debugf("Creating an offer for %s - %s", offeredCredential.getScope(),
|
||||
offeredCredential.getFormat());
|
||||
clientSession.removeNote(nonce);
|
||||
} catch (JsonProcessingException e) {
|
||||
LOGGER.errorf("Could not convert SupportedCredential JSON to POJO: %s", e);
|
||||
throw new BadRequestException(getErrorResponse(ErrorType.INVALID_CREDENTIAL_REQUEST));
|
||||
}
|
||||
|
||||
String preAuthorizedCode = generateAuthorizationCodeForClientSession(clientSession);
|
||||
|
||||
CredentialsOffer theOffer = new CredentialsOffer()
|
||||
.setCredentialIssuer(OID4VCIssuerWellKnownProvider.getIssuer(session.getContext()))
|
||||
.setCredentialConfigurationIds(List.of(offeredCredential.getId()))
|
||||
.setGrants(
|
||||
new PreAuthorizedGrant()
|
||||
.setPreAuthorizedCode(
|
||||
new PreAuthorizedCode()
|
||||
.setPreAuthorizedCode(preAuthorizedCode)));
|
||||
|
||||
LOGGER.debugf("Responding with offer: %s", theOffer);
|
||||
return Response.ok()
|
||||
.entity(theOffer)
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a verifiable credential
|
||||
*/
|
||||
@POST
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@Path(CREDENTIAL_PATH)
|
||||
public Response requestCredential(
|
||||
CredentialRequest credentialRequestVO) {
|
||||
LOGGER.debugf("Received credentials request %s.", credentialRequestVO);
|
||||
|
||||
// do first to fail fast on auth
|
||||
UserSessionModel userSessionModel = getUserSessionModel();
|
||||
|
||||
Format requestedFormat = credentialRequestVO.getFormat();
|
||||
String requestedCredential = credentialRequestVO.getCredentialIdentifier();
|
||||
|
||||
SupportedCredentialConfiguration supportedCredentialConfiguration = Optional
|
||||
.ofNullable(OID4VCIssuerWellKnownProvider.getSupportedCredentials(this.session)
|
||||
.get(requestedCredential))
|
||||
.orElseThrow(
|
||||
() -> {
|
||||
LOGGER.debugf("Unsupported credential %s was requested.", requestedCredential);
|
||||
return new BadRequestException(getErrorResponse(ErrorType.UNSUPPORTED_CREDENTIAL_TYPE));
|
||||
});
|
||||
|
||||
if (!supportedCredentialConfiguration.getFormat().equals(requestedFormat)) {
|
||||
LOGGER.debugf("Format %s is not supported for credential %s.", requestedFormat, requestedCredential);
|
||||
throw new BadRequestException(getErrorResponse(ErrorType.UNSUPPORTED_CREDENTIAL_FORMAT));
|
||||
}
|
||||
|
||||
CredentialResponse responseVO = new CredentialResponse();
|
||||
|
||||
Object theCredential = getCredential(userSessionModel, supportedCredentialConfiguration.getScope(), credentialRequestVO.getFormat());
|
||||
switch (requestedFormat) {
|
||||
case LDP_VC, JWT_VC, SD_JWT_VC -> responseVO.setCredential(theCredential);
|
||||
default -> throw new BadRequestException(
|
||||
getErrorResponse(ErrorType.UNSUPPORTED_CREDENTIAL_TYPE));
|
||||
}
|
||||
return Response.ok().entity(responseVO)
|
||||
.build();
|
||||
}
|
||||
|
||||
private AuthenticatedClientSessionModel getAuthenticatedClientSession() {
|
||||
AuthenticationManager.AuthResult authResult = getAuthResult();
|
||||
UserSessionModel userSessionModel = authResult.getSession();
|
||||
|
||||
AuthenticatedClientSessionModel clientSession = userSessionModel.
|
||||
getAuthenticatedClientSessionByClient(
|
||||
authResult.getClient().getId());
|
||||
if (clientSession == null) {
|
||||
throw new BadRequestException(getErrorResponse(ErrorType.INVALID_TOKEN));
|
||||
}
|
||||
return clientSession;
|
||||
}
|
||||
|
||||
// return the current UserSessionModel
|
||||
private UserSessionModel getUserSessionModel() {
|
||||
return getAuthResult(
|
||||
new BadRequestException(getErrorResponse(ErrorType.INVALID_TOKEN))).getSession();
|
||||
}
|
||||
|
||||
private AuthenticationManager.AuthResult getAuthResult() {
|
||||
return getAuthResult(new BadRequestException(getErrorResponse(ErrorType.INVALID_TOKEN)));
|
||||
}
|
||||
|
||||
// get the auth result from the authentication manager
|
||||
private AuthenticationManager.AuthResult getAuthResult(WebApplicationException errorResponse) {
|
||||
AuthenticationManager.AuthResult authResult = bearerTokenAuthenticator.authenticate();
|
||||
if (authResult == null) {
|
||||
throw errorResponse;
|
||||
}
|
||||
return authResult;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a signed credential
|
||||
*
|
||||
* @param userSessionModel userSession to create the credential for
|
||||
* @param vcType type of the credential to be created
|
||||
* @param format format of the credential to be created
|
||||
* @return the signed credential
|
||||
*/
|
||||
private Object getCredential(UserSessionModel userSessionModel, String vcType, Format format) {
|
||||
|
||||
List<OID4VCClient> clients = getClientsOfType(vcType, format);
|
||||
|
||||
List<OID4VCMapper> protocolMappers = getProtocolMappers(clients)
|
||||
.stream()
|
||||
.map(pm -> {
|
||||
if (session.getProvider(ProtocolMapper.class, pm.getProtocolMapper()) instanceof OID4VCMapper mapperFactory) {
|
||||
ProtocolMapper protocolMapper = mapperFactory.create(session);
|
||||
if (protocolMapper instanceof OID4VCMapper oid4VCMapper) {
|
||||
oid4VCMapper.setMapperModel(pm);
|
||||
return oid4VCMapper;
|
||||
}
|
||||
}
|
||||
LOGGER.warnf("The protocol mapper %s is not an instance of OID4VCMapper.", pm.getId());
|
||||
return null;
|
||||
})
|
||||
.filter(Objects::nonNull)
|
||||
.toList();
|
||||
|
||||
VerifiableCredential credentialToSign = getVCToSign(protocolMappers, vcType, userSessionModel);
|
||||
|
||||
return Optional.ofNullable(signingServices.get(format))
|
||||
.map(verifiableCredentialsSigningService -> verifiableCredentialsSigningService.signCredential(credentialToSign))
|
||||
.orElseThrow(() -> new IllegalArgumentException(String.format("Requested format %s is not supported.", format)));
|
||||
}
|
||||
|
||||
private List<ProtocolMapperModel> getProtocolMappers(List<OID4VCClient> oid4VCClients) {
|
||||
|
||||
return oid4VCClients.stream()
|
||||
.map(OID4VCClient::getClientDid)
|
||||
.map(this::getClient)
|
||||
.flatMap(ProtocolMapperContainerModel::getProtocolMappersStream)
|
||||
.toList();
|
||||
}
|
||||
|
||||
private String generateNonce() {
|
||||
return SecretGenerator.getInstance().randomString();
|
||||
}
|
||||
|
||||
private String generateAuthorizationCodeForClientSession(AuthenticatedClientSessionModel clientSessionModel) {
|
||||
int expiration = timeProvider.currentTimeSeconds() + preAuthorizedCodeLifeSpan;
|
||||
return PreAuthorizedCodeGrantType.getPreAuthorizedCode(session, clientSessionModel, expiration);
|
||||
}
|
||||
|
||||
private Response getErrorResponse(ErrorType errorType) {
|
||||
var errorResponse = new ErrorResponse();
|
||||
errorResponse.setError(errorType);
|
||||
return Response.status(Response.Status.BAD_REQUEST).entity(errorResponse).build();
|
||||
}
|
||||
|
||||
// Return all {@link OID4VCClient}s that support the given type and format
|
||||
private List<OID4VCClient> getClientsOfType(String vcType, Format format) {
|
||||
LOGGER.debugf("Retrieve all clients of type %s, supporting format %s", vcType, format.toString());
|
||||
|
||||
if (Optional.ofNullable(vcType).filter(type -> !type.isEmpty()).isEmpty()) {
|
||||
throw new BadRequestException("No VerifiableCredential-Type was provided in the request.");
|
||||
}
|
||||
|
||||
return getOID4VCClientsFromSession()
|
||||
.stream()
|
||||
.filter(oid4VCClient -> oid4VCClient.getSupportedVCTypes()
|
||||
.stream()
|
||||
.anyMatch(supportedCredential -> supportedCredential.getScope().equals(vcType)))
|
||||
.toList();
|
||||
}
|
||||
|
||||
private ClientModel getClient(String clientId) {
|
||||
return session.clients().getClientByClientId(session.getContext().getRealm(), clientId);
|
||||
}
|
||||
|
||||
private List<OID4VCClient> getOID4VCClientsFromSession() {
|
||||
return session.clients().getClientsStream(session.getContext().getRealm())
|
||||
.filter(clientModel -> clientModel.getProtocol() != null)
|
||||
.filter(clientModel -> clientModel.getProtocol()
|
||||
.equals(OID4VCLoginProtocolFactory.PROTOCOL_ID))
|
||||
.map(clientModel -> OID4VCClientRegistrationProvider.fromClientAttributes(clientModel.getClientId(), clientModel.getAttributes()))
|
||||
.toList();
|
||||
}
|
||||
|
||||
// builds the unsigned credential by applying all protocol mappers.
|
||||
private VerifiableCredential getVCToSign(List<OID4VCMapper> protocolMappers, String vcType,
|
||||
UserSessionModel userSessionModel) {
|
||||
// set the required claims
|
||||
VerifiableCredential vc = new VerifiableCredential()
|
||||
.setIssuer(URI.create(issuerDid))
|
||||
.setIssuanceDate(Date.from(Instant.ofEpochMilli(timeProvider.currentTimeMillis())))
|
||||
.setType(List.of(vcType));
|
||||
|
||||
Map<String, Object> subjectClaims = new HashMap<>();
|
||||
protocolMappers
|
||||
.forEach(mapper -> mapper.setClaimsForSubject(subjectClaims, userSessionModel));
|
||||
|
||||
subjectClaims.forEach((key, value) -> vc.getCredentialSubject().setClaims(key, value));
|
||||
|
||||
protocolMappers
|
||||
.forEach(mapper -> mapper.setClaimsForCredential(vc, userSessionModel));
|
||||
|
||||
LOGGER.debugf("The credential to sign is: %s", vc);
|
||||
return vc;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,118 @@
|
|||
/*
|
||||
* 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.models.KeycloakContext;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.protocol.oid4vc.OID4VCClientRegistrationProvider;
|
||||
import org.keycloak.protocol.oid4vc.OID4VCLoginProtocolFactory;
|
||||
import org.keycloak.protocol.oid4vc.issuance.signing.VCSigningServiceProviderFactory;
|
||||
import org.keycloak.protocol.oid4vc.issuance.signing.VerifiableCredentialsSigningService;
|
||||
import org.keycloak.protocol.oid4vc.model.CredentialIssuer;
|
||||
import org.keycloak.protocol.oid4vc.model.Format;
|
||||
import org.keycloak.protocol.oid4vc.model.OID4VCClient;
|
||||
import org.keycloak.protocol.oid4vc.model.SupportedCredentialConfiguration;
|
||||
import org.keycloak.services.Urls;
|
||||
import org.keycloak.urls.UrlType;
|
||||
import org.keycloak.wellknown.WellKnownProvider;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* {@link WellKnownProvider} implementation to provide the .well-known/openid-credential-issuer endpoint, offering
|
||||
* the Credential Issuer Metadata as defined by the OID4VCI protocol
|
||||
* {@see https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html#section-11.2.2}
|
||||
*
|
||||
* @author <a href="https://github.com/wistefan">Stefan Wiedemann</a>
|
||||
*/
|
||||
public class OID4VCIssuerWellKnownProvider implements WellKnownProvider {
|
||||
|
||||
private final KeycloakSession keycloakSession;
|
||||
|
||||
public OID4VCIssuerWellKnownProvider(KeycloakSession keycloakSession) {
|
||||
this.keycloakSession = keycloakSession;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
// no-op
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getConfig() {
|
||||
return new CredentialIssuer()
|
||||
.setCredentialIssuer(getIssuer(keycloakSession.getContext()))
|
||||
.setCredentialEndpoint(getCredentialsEndpoint(keycloakSession.getContext()))
|
||||
.setCredentialsSupported(getSupportedCredentials(keycloakSession))
|
||||
.setAuthorizationServers(List.of(getIssuer(keycloakSession.getContext())));
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the supported credentials from the current session.
|
||||
* It will take into account the configured {@link VerifiableCredentialsSigningService}'s and there supported format
|
||||
* and the credentials supported by the clients available in the session.
|
||||
*/
|
||||
public static Map<String, SupportedCredentialConfiguration> getSupportedCredentials(KeycloakSession keycloakSession) {
|
||||
|
||||
RealmModel realm = keycloakSession.getContext().getRealm();
|
||||
List<Format> supportedFormats = realm.getComponentsStream(realm.getId(), VerifiableCredentialsSigningService.class.getName())
|
||||
.map(cm ->
|
||||
keycloakSession
|
||||
.getKeycloakSessionFactory()
|
||||
.getProviderFactory(VerifiableCredentialsSigningService.class, cm.getProviderId())
|
||||
)
|
||||
.filter(VCSigningServiceProviderFactory.class::isInstance)
|
||||
.map(VCSigningServiceProviderFactory.class::cast)
|
||||
.map(VCSigningServiceProviderFactory::supportedFormat)
|
||||
.toList();
|
||||
|
||||
return keycloakSession.getContext()
|
||||
.getRealm()
|
||||
.getClientsStream()
|
||||
.filter(cm -> cm.getProtocol() != null)
|
||||
.filter(cm -> cm.getProtocol().equals(OID4VCLoginProtocolFactory.PROTOCOL_ID))
|
||||
.map(cm -> OID4VCClientRegistrationProvider.fromClientAttributes(cm.getClientId(), cm.getAttributes()))
|
||||
.map(OID4VCClient::getSupportedVCTypes)
|
||||
.flatMap(List::stream)
|
||||
.filter(sc -> supportedFormats.contains(sc.getFormat()))
|
||||
.distinct()
|
||||
.collect(Collectors.toMap(SupportedCredentialConfiguration::getId, sc -> sc, (sc1, sc2) -> sc1));
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the url of the issuer.
|
||||
*/
|
||||
public static String getIssuer(KeycloakContext context) {
|
||||
UriInfo frontendUriInfo = context.getUri(UrlType.FRONTEND);
|
||||
return Urls.realmIssuer(frontendUriInfo.getBaseUri(),
|
||||
context.getRealm().getName());
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the credentials endpoint address
|
||||
*/
|
||||
public static String getCredentialsEndpoint(KeycloakContext context) {
|
||||
return getIssuer(context) + "/protocol/" + OID4VCLoginProtocolFactory.PROTOCOL_ID + "/" + OID4VCIssuerEndpoint.CREDENTIAL_PATH;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,62 @@
|
|||
/*
|
||||
* 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 the OID4VCI metadata
|
||||
* <p>
|
||||
* {@see https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html#section-11.2.2}
|
||||
*
|
||||
* @author <a href="https://github.com/wistefan">Stefan Wiedemann</a>
|
||||
*/
|
||||
public class OID4VCIssuerWellKnownProviderFactory implements WellKnownProviderFactory, OID4VCEnvironmentProviderFactory {
|
||||
|
||||
public static final String PROVIDER_ID = "openid-credential-issuer";
|
||||
|
||||
@Override
|
||||
public WellKnownProvider create(KeycloakSession session) {
|
||||
return new OID4VCIssuerWellKnownProvider(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,95 @@
|
|||
/*
|
||||
* 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.mappers;
|
||||
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.UserSessionModel;
|
||||
import org.keycloak.protocol.ProtocolMapper;
|
||||
import org.keycloak.protocol.oid4vc.model.VerifiableCredential;
|
||||
import org.keycloak.provider.ProviderConfigProperty;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Allows to add the context to the credential subject
|
||||
*
|
||||
* @author <a href="https://github.com/wistefan">Stefan Wiedemann</a>
|
||||
*/
|
||||
public class OID4VCContextMapper extends OID4VCMapper {
|
||||
|
||||
public static final String MAPPER_ID = "oid4vc-context-mapper";
|
||||
public static final String TYPE_KEY = "context";
|
||||
|
||||
private static final List<ProviderConfigProperty> CONFIG_PROPERTIES = new ArrayList<>();
|
||||
|
||||
static {
|
||||
ProviderConfigProperty contextPropertyNameConfig = new ProviderConfigProperty();
|
||||
contextPropertyNameConfig.setName(TYPE_KEY);
|
||||
contextPropertyNameConfig.setLabel("Verifiable Credentials Context");
|
||||
contextPropertyNameConfig.setHelpText("Context of the credential.");
|
||||
contextPropertyNameConfig.setType(ProviderConfigProperty.STRING_TYPE);
|
||||
contextPropertyNameConfig.setDefaultValue("https://www.w3.org/2018/credentials/v1");
|
||||
CONFIG_PROPERTIES.add(contextPropertyNameConfig);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
protected List<ProviderConfigProperty> getIndividualConfigProperties() {
|
||||
return CONFIG_PROPERTIES;
|
||||
}
|
||||
|
||||
public void setClaimsForCredential(VerifiableCredential verifiableCredential,
|
||||
UserSessionModel userSessionModel) {
|
||||
// remove duplicates
|
||||
Set<String> contexts = new HashSet<>();
|
||||
if (verifiableCredential.getContext() != null) {
|
||||
contexts = new HashSet<>(verifiableCredential.getContext());
|
||||
}
|
||||
contexts.add(mapperModel.getConfig().get(TYPE_KEY));
|
||||
verifiableCredential.setContext(new ArrayList<>(contexts));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setClaimsForSubject(Map<String, Object> claims, UserSessionModel userSessionModel) {
|
||||
// nothing to do for the mapper.
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDisplayType() {
|
||||
return "Credential Context Mapper";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getHelpText() {
|
||||
return "Assigns a context to the credential.";
|
||||
}
|
||||
|
||||
@Override
|
||||
public ProtocolMapper create(KeycloakSession session) {
|
||||
return new OID4VCContextMapper();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return MAPPER_ID;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,123 @@
|
|||
/*
|
||||
* 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.mappers;
|
||||
|
||||
import org.keycloak.Config;
|
||||
import org.keycloak.models.KeycloakSessionFactory;
|
||||
import org.keycloak.models.ProtocolMapperModel;
|
||||
import org.keycloak.models.UserSessionModel;
|
||||
import org.keycloak.protocol.ProtocolMapper;
|
||||
import org.keycloak.protocol.oid4vc.OID4VCEnvironmentProviderFactory;
|
||||
import org.keycloak.protocol.oid4vc.OID4VCLoginProtocolFactory;
|
||||
import org.keycloak.protocol.oid4vc.model.VerifiableCredential;
|
||||
import org.keycloak.provider.ProviderConfigProperty;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
/**
|
||||
* Base class for OID4VC Mappers, to provide common configuration and functionality for all of them
|
||||
*
|
||||
* @author <a href="https://github.com/wistefan">Stefan Wiedemann</a>
|
||||
*/
|
||||
public abstract class OID4VCMapper implements ProtocolMapper, OID4VCEnvironmentProviderFactory {
|
||||
|
||||
protected static final String SUPPORTED_CREDENTIALS_KEY = "supportedCredentialTypes";
|
||||
|
||||
protected ProtocolMapperModel mapperModel;
|
||||
|
||||
private static final List<ProviderConfigProperty> OID4VC_CONFIG_PROPERTIES = new ArrayList<>();
|
||||
|
||||
static {
|
||||
ProviderConfigProperty supportedCredentialsConfig = new ProviderConfigProperty();
|
||||
supportedCredentialsConfig.setType(ProviderConfigProperty.STRING_TYPE);
|
||||
supportedCredentialsConfig.setLabel("Supported Credential Types");
|
||||
supportedCredentialsConfig.setDefaultValue("VerifiableCredential");
|
||||
supportedCredentialsConfig.setHelpText(
|
||||
"Types of Credentials to apply the mapper. Needs to be a comma-separated list.");
|
||||
supportedCredentialsConfig.setName(SUPPORTED_CREDENTIALS_KEY);
|
||||
OID4VC_CONFIG_PROPERTIES.clear();
|
||||
OID4VC_CONFIG_PROPERTIES.add(supportedCredentialsConfig);
|
||||
}
|
||||
|
||||
protected abstract List<ProviderConfigProperty> getIndividualConfigProperties();
|
||||
|
||||
@Override
|
||||
public List<ProviderConfigProperty> getConfigProperties() {
|
||||
return Stream.concat(OID4VC_CONFIG_PROPERTIES.stream(), getIndividualConfigProperties().stream()).toList();
|
||||
}
|
||||
|
||||
public OID4VCMapper setMapperModel(ProtocolMapperModel mapperModel) {
|
||||
this.mapperModel = mapperModel;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getProtocol() {
|
||||
return OID4VCLoginProtocolFactory.PROTOCOL_ID;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDisplayCategory() {
|
||||
return "OID4VC Mapper";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init(Config.Scope scope) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void postInit(KeycloakSessionFactory keycloakSessionFactory) {
|
||||
// try to get the credentials
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the mapper supports the given credential type. Allows to configure them not only per client, but also per VC Type.
|
||||
*
|
||||
* @param credentialType type of the VerifiableCredential that should be checked
|
||||
* @return true if it is supported
|
||||
*/
|
||||
public boolean isTypeSupported(String credentialType) {
|
||||
var optionalTypes = Optional.ofNullable(mapperModel.getConfig().get(SUPPORTED_CREDENTIALS_KEY));
|
||||
if (optionalTypes.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
return Arrays.asList(optionalTypes.get().split(",")).contains(credentialType);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the claims to credential, like f.e. the context
|
||||
*/
|
||||
public abstract void setClaimsForCredential(VerifiableCredential verifiableCredential,
|
||||
UserSessionModel userSessionModel);
|
||||
|
||||
/**
|
||||
* Set the claims to the credential subject.
|
||||
*/
|
||||
public abstract void setClaimsForSubject(Map<String, Object> claims,
|
||||
UserSessionModel userSessionModel);
|
||||
|
||||
}
|
|
@ -0,0 +1,96 @@
|
|||
/*
|
||||
* 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.mappers;
|
||||
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.UserSessionModel;
|
||||
import org.keycloak.protocol.ProtocolMapper;
|
||||
import org.keycloak.protocol.oid4vc.model.VerifiableCredential;
|
||||
import org.keycloak.provider.ProviderConfigProperty;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Allows to add statically configured claims to the credential subject
|
||||
*
|
||||
* @author <a href="https://github.com/wistefan">Stefan Wiedemann</a>
|
||||
*/
|
||||
public class OID4VCStaticClaimMapper extends OID4VCMapper {
|
||||
|
||||
public static final String MAPPER_ID = "oid4vc-static-claim-mapper";
|
||||
|
||||
public static final String SUBJECT_PROPERTY_CONFIG_KEY = "subjectProperty";
|
||||
public static final String STATIC_CLAIM_KEY = "staticValue";
|
||||
|
||||
private static final List<ProviderConfigProperty> CONFIG_PROPERTIES = new ArrayList<>();
|
||||
|
||||
static {
|
||||
ProviderConfigProperty subjectPropertyNameConfig = new ProviderConfigProperty();
|
||||
subjectPropertyNameConfig.setName(SUBJECT_PROPERTY_CONFIG_KEY);
|
||||
subjectPropertyNameConfig.setLabel("Static Claim Property Name");
|
||||
subjectPropertyNameConfig.setHelpText("Name of the property to contain the static value.");
|
||||
subjectPropertyNameConfig.setType(ProviderConfigProperty.STRING_TYPE);
|
||||
CONFIG_PROPERTIES.add(subjectPropertyNameConfig);
|
||||
|
||||
ProviderConfigProperty claimValueConfig = new ProviderConfigProperty();
|
||||
claimValueConfig.setName(STATIC_CLAIM_KEY);
|
||||
claimValueConfig.setLabel("Static Claim Value");
|
||||
claimValueConfig.setHelpText("Value to be set for the property.");
|
||||
claimValueConfig.setType(ProviderConfigProperty.STRING_TYPE);
|
||||
CONFIG_PROPERTIES.add(claimValueConfig);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<ProviderConfigProperty> getIndividualConfigProperties() {
|
||||
return CONFIG_PROPERTIES;
|
||||
}
|
||||
|
||||
public void setClaimsForCredential(VerifiableCredential verifiableCredential,
|
||||
UserSessionModel userSessionModel) {
|
||||
// nothing to do for the mapper.
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setClaimsForSubject(Map<String, Object> claims, UserSessionModel userSessionModel) {
|
||||
String propertyName = mapperModel.getConfig().get(SUBJECT_PROPERTY_CONFIG_KEY);
|
||||
String staticValue = mapperModel.getConfig().get(STATIC_CLAIM_KEY);
|
||||
claims.put(propertyName, staticValue);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDisplayType() {
|
||||
return "Static Claim Mapper";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getHelpText() {
|
||||
return "Allows to set static values for the credential subject.";
|
||||
}
|
||||
|
||||
@Override
|
||||
public ProtocolMapper create(KeycloakSession session) {
|
||||
return new OID4VCStaticClaimMapper();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return MAPPER_ID;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,102 @@
|
|||
/*
|
||||
* 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.mappers;
|
||||
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.ProtocolMapperModel;
|
||||
import org.keycloak.models.UserSessionModel;
|
||||
import org.keycloak.protocol.ProtocolMapper;
|
||||
import org.keycloak.protocol.oid4vc.OID4VCLoginProtocolFactory;
|
||||
import org.keycloak.protocol.oid4vc.model.VerifiableCredential;
|
||||
import org.keycloak.provider.ProviderConfigProperty;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Sets an ID for the credential, either randomly generated or statically configured
|
||||
*
|
||||
* @author <a href="https://github.com/wistefan">Stefan Wiedemann</a>
|
||||
*/
|
||||
public class OID4VCSubjectIdMapper extends OID4VCMapper {
|
||||
|
||||
public static final String MAPPER_ID = "oid4vc-subject-id-mapper";
|
||||
public static final String ID_KEY = "subjectIdProperty";
|
||||
|
||||
private static final List<ProviderConfigProperty> CONFIG_PROPERTIES = new ArrayList<>();
|
||||
|
||||
static {
|
||||
ProviderConfigProperty idPropertyNameConfig = new ProviderConfigProperty();
|
||||
idPropertyNameConfig.setName(ID_KEY);
|
||||
idPropertyNameConfig.setLabel("ID Property Name");
|
||||
idPropertyNameConfig.setHelpText("Name of the property to contain the id.");
|
||||
idPropertyNameConfig.setDefaultValue("id");
|
||||
idPropertyNameConfig.setType(ProviderConfigProperty.STRING_TYPE);
|
||||
CONFIG_PROPERTIES.add(idPropertyNameConfig);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<ProviderConfigProperty> getIndividualConfigProperties() {
|
||||
return CONFIG_PROPERTIES;
|
||||
}
|
||||
|
||||
public static ProtocolMapperModel create(String name, String subjectId) {
|
||||
var mapperModel = new ProtocolMapperModel();
|
||||
mapperModel.setName(name);
|
||||
Map<String, String> configMap = new HashMap<>();
|
||||
configMap.put(ID_KEY, subjectId);
|
||||
configMap.put(SUPPORTED_CREDENTIALS_KEY, "VerifiableCredential");
|
||||
mapperModel.setConfig(configMap);
|
||||
mapperModel.setProtocol(OID4VCLoginProtocolFactory.PROTOCOL_ID);
|
||||
mapperModel.setProtocolMapper(MAPPER_ID);
|
||||
return mapperModel;
|
||||
}
|
||||
|
||||
public void setClaimsForCredential(VerifiableCredential verifiableCredential,
|
||||
UserSessionModel userSessionModel) {
|
||||
// nothing to do for the mapper.
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setClaimsForSubject(Map<String, Object> claims, UserSessionModel userSessionModel) {
|
||||
claims.put("id", mapperModel.getConfig().getOrDefault(ID_KEY, String.format("urn:uuid:%s", UUID.randomUUID())));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDisplayType() {
|
||||
return "CredentialSubject ID Mapper";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getHelpText() {
|
||||
return "Assigns a subject ID to the credentials subject. If no specific id is configured, a randomly generated one is used.";
|
||||
}
|
||||
|
||||
@Override
|
||||
public ProtocolMapper create(KeycloakSession session) {
|
||||
return new OID4VCSubjectIdMapper();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return MAPPER_ID;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,170 @@
|
|||
/*
|
||||
* 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.mappers;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.models.ClientModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.ProtocolMapperModel;
|
||||
import org.keycloak.models.RoleModel;
|
||||
import org.keycloak.models.UserSessionModel;
|
||||
import org.keycloak.protocol.ProtocolMapper;
|
||||
import org.keycloak.protocol.oid4vc.OID4VCLoginProtocolFactory;
|
||||
import org.keycloak.protocol.oid4vc.model.Role;
|
||||
import org.keycloak.protocol.oid4vc.model.VerifiableCredential;
|
||||
import org.keycloak.provider.ProviderConfigProperty;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* Adds the users roles to the credential subject
|
||||
*
|
||||
* @author <a href="https://github.com/wistefan">Stefan Wiedemann</a>
|
||||
*/
|
||||
public class OID4VCTargetRoleMapper extends OID4VCMapper {
|
||||
|
||||
private static final Logger LOGGER = Logger.getLogger(OID4VCTargetRoleMapper.class);
|
||||
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
|
||||
|
||||
public static final String MAPPER_ID = "oid4vc-target-role-mapper";
|
||||
public static final String SUBJECT_PROPERTY_CONFIG_KEY = "subjectProperty";
|
||||
public static final String CLIENT_CONFIG_KEY = "clientId";
|
||||
|
||||
private static final List<ProviderConfigProperty> CONFIG_PROPERTIES = new ArrayList<>();
|
||||
|
||||
static {
|
||||
ProviderConfigProperty subjectPropertyNameConfig = new ProviderConfigProperty();
|
||||
subjectPropertyNameConfig.setName(SUBJECT_PROPERTY_CONFIG_KEY);
|
||||
subjectPropertyNameConfig.setLabel("Roles Property Name");
|
||||
subjectPropertyNameConfig.setHelpText("Property to add the roles to in the credential subject.");
|
||||
subjectPropertyNameConfig.setDefaultValue("roles");
|
||||
subjectPropertyNameConfig.setType(ProviderConfigProperty.STRING_TYPE);
|
||||
CONFIG_PROPERTIES.add(subjectPropertyNameConfig);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<ProviderConfigProperty> getIndividualConfigProperties() {
|
||||
return CONFIG_PROPERTIES;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDisplayType() {
|
||||
return "Target-Role Mapper";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getHelpText() {
|
||||
return "Map the assigned role to the credential subject, providing the client id as the target.";
|
||||
}
|
||||
|
||||
public static ProtocolMapperModel create(String clientId, String name) {
|
||||
var mapperModel = new ProtocolMapperModel();
|
||||
mapperModel.setName(name);
|
||||
Map<String, String> configMap = new HashMap<>();
|
||||
configMap.put(SUBJECT_PROPERTY_CONFIG_KEY, "roles");
|
||||
configMap.put(CLIENT_CONFIG_KEY, clientId);
|
||||
mapperModel.setConfig(configMap);
|
||||
mapperModel.setProtocol(OID4VCLoginProtocolFactory.PROTOCOL_ID);
|
||||
mapperModel.setProtocolMapper(MAPPER_ID);
|
||||
return mapperModel;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ProtocolMapper create(KeycloakSession session) {
|
||||
return new OID4VCTargetRoleMapper();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return MAPPER_ID;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setClaimsForCredential(VerifiableCredential verifiableCredential,
|
||||
UserSessionModel userSessionModel) {
|
||||
// nothing to do for the mapper.
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setClaimsForSubject(Map<String, Object> claims,
|
||||
UserSessionModel userSessionModel) {
|
||||
String client = mapperModel.getConfig().get(CLIENT_CONFIG_KEY);
|
||||
String propertyName = mapperModel.getConfig().get(SUBJECT_PROPERTY_CONFIG_KEY);
|
||||
ClientModel clientModel = userSessionModel.getRealm().getClientByClientId(client);
|
||||
if (clientModel == null || !clientModel.getProtocol().equals(OID4VCLoginProtocolFactory.PROTOCOL_ID)) {
|
||||
return;
|
||||
}
|
||||
|
||||
ClientRoleModel clientRoleModel = new ClientRoleModel(clientModel.getClientId(),
|
||||
userSessionModel.getUser().getClientRoleMappingsStream(clientModel).toList());
|
||||
Role rolesClaim = toRolesClaim(clientRoleModel);
|
||||
if (rolesClaim.getNames().isEmpty()) {
|
||||
return;
|
||||
}
|
||||
var modelMap = OBJECT_MAPPER.convertValue(toRolesClaim(clientRoleModel), Map.class);
|
||||
|
||||
if (claims.containsKey(propertyName)) {
|
||||
if (claims.get(propertyName) instanceof Set rolesProperty) {
|
||||
rolesProperty.add(modelMap);
|
||||
claims.put(propertyName, rolesProperty);
|
||||
} else {
|
||||
LOGGER.warnf("Incompatible types for property %s. The mapper will not set the roles for client %s",
|
||||
propertyName, client);
|
||||
}
|
||||
} else {
|
||||
// needs to be mutable
|
||||
Set roles = new HashSet();
|
||||
roles.add(modelMap);
|
||||
claims.put(propertyName, roles);
|
||||
}
|
||||
}
|
||||
|
||||
private Role toRolesClaim(ClientRoleModel crm) {
|
||||
Set<String> roleNames = crm
|
||||
.getRoleModels()
|
||||
.stream()
|
||||
.map(RoleModel::getName)
|
||||
.collect(Collectors.toSet());
|
||||
return new Role(roleNames, crm.getClientId());
|
||||
}
|
||||
|
||||
private static class ClientRoleModel {
|
||||
private final String clientId;
|
||||
private final List<RoleModel> roleModels;
|
||||
|
||||
public ClientRoleModel(String clientId, List<RoleModel> roleModels) {
|
||||
this.clientId = clientId;
|
||||
this.roleModels = roleModels;
|
||||
}
|
||||
|
||||
public String getClientId() {
|
||||
return clientId;
|
||||
}
|
||||
|
||||
public List<RoleModel> getRoleModels() {
|
||||
return roleModels;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,95 @@
|
|||
/*
|
||||
* 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.mappers;
|
||||
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.UserSessionModel;
|
||||
import org.keycloak.protocol.ProtocolMapper;
|
||||
import org.keycloak.protocol.oid4vc.model.VerifiableCredential;
|
||||
import org.keycloak.provider.ProviderConfigProperty;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Allows to add types to the credential subject
|
||||
*
|
||||
* @author <a href="https://github.com/wistefan">Stefan Wiedemann</a>
|
||||
*/
|
||||
public class OID4VCTypeMapper extends OID4VCMapper {
|
||||
|
||||
public static final String MAPPER_ID = "oid4vc-vc-type-mapper";
|
||||
public static final String TYPE_KEY = "vcTypeProperty";
|
||||
public static final String DEFAULT_VC_TYPE = "VerifiableCredential";
|
||||
|
||||
private static final List<ProviderConfigProperty> CONFIG_PROPERTIES = new ArrayList<>();
|
||||
|
||||
static {
|
||||
ProviderConfigProperty vcTypePropertyNameConfig = new ProviderConfigProperty();
|
||||
vcTypePropertyNameConfig.setName(TYPE_KEY);
|
||||
vcTypePropertyNameConfig.setLabel("Verifiable Credential Type");
|
||||
vcTypePropertyNameConfig.setHelpText("Type of the credential.");
|
||||
vcTypePropertyNameConfig.setType(ProviderConfigProperty.STRING_TYPE);
|
||||
CONFIG_PROPERTIES.add(vcTypePropertyNameConfig);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<ProviderConfigProperty> getIndividualConfigProperties() {
|
||||
return CONFIG_PROPERTIES;
|
||||
}
|
||||
|
||||
public void setClaimsForCredential(VerifiableCredential verifiableCredential,
|
||||
UserSessionModel userSessionModel) {
|
||||
// remove duplicates
|
||||
Set<String> types = new HashSet<>();
|
||||
if (verifiableCredential.getType() != null) {
|
||||
types = new HashSet<>(verifiableCredential.getType());
|
||||
}
|
||||
types.add(Optional.ofNullable(mapperModel.getConfig().get(TYPE_KEY)).orElse(DEFAULT_VC_TYPE));
|
||||
verifiableCredential.setType(new ArrayList<>(types));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setClaimsForSubject(Map<String, Object> claims, UserSessionModel userSessionModel) {
|
||||
// nothing to do for the mapper.
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDisplayType() {
|
||||
return "Credential Type Mapper";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getHelpText() {
|
||||
return "Assigns a type to the credential.";
|
||||
}
|
||||
|
||||
@Override
|
||||
public ProtocolMapper create(KeycloakSession session) {
|
||||
return new OID4VCTypeMapper();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return MAPPER_ID;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,136 @@
|
|||
/*
|
||||
* 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.mappers;
|
||||
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.ProtocolMapperModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.models.UserSessionModel;
|
||||
import org.keycloak.models.utils.KeycloakModelUtils;
|
||||
import org.keycloak.protocol.ProtocolMapper;
|
||||
import org.keycloak.protocol.oid4vc.OID4VCLoginProtocolFactory;
|
||||
import org.keycloak.protocol.oid4vc.model.VerifiableCredential;
|
||||
import org.keycloak.provider.ProviderConfigProperty;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* Allows to add user attributes to the credential subject
|
||||
*
|
||||
* @author <a href="https://github.com/wistefan">Stefan Wiedemann</a>
|
||||
*/
|
||||
public class OID4VCUserAttributeMapper extends OID4VCMapper {
|
||||
|
||||
public static final String MAPPER_ID = "oid4vc-user-attribute-mapper";
|
||||
public static final String SUBJECT_PROPERTY_CONFIG_KEY = "subjectProperty";
|
||||
public static final String USER_ATTRIBUTE_KEY = "userAttribute";
|
||||
public static final String AGGREGATE_ATTRIBUTES_KEY = "aggregateAttributes";
|
||||
|
||||
private static final List<ProviderConfigProperty> CONFIG_PROPERTIES = new ArrayList<>();
|
||||
|
||||
static {
|
||||
ProviderConfigProperty subjectPropertyNameConfig = new ProviderConfigProperty();
|
||||
subjectPropertyNameConfig.setName(SUBJECT_PROPERTY_CONFIG_KEY);
|
||||
subjectPropertyNameConfig.setLabel("Attribute Property Name");
|
||||
subjectPropertyNameConfig.setHelpText("Property to add the user attribute to in the credential subject.");
|
||||
subjectPropertyNameConfig.setType(ProviderConfigProperty.STRING_TYPE);
|
||||
CONFIG_PROPERTIES.add(subjectPropertyNameConfig);
|
||||
|
||||
ProviderConfigProperty userAttributeConfig = new ProviderConfigProperty();
|
||||
userAttributeConfig.setName(USER_ATTRIBUTE_KEY);
|
||||
userAttributeConfig.setLabel("User attribute");
|
||||
userAttributeConfig.setHelpText("The user attribute to be added to the credential subject.");
|
||||
userAttributeConfig.setType(ProviderConfigProperty.LIST_TYPE);
|
||||
userAttributeConfig.setOptions(
|
||||
List.of(UserModel.USERNAME, UserModel.LOCALE, UserModel.FIRST_NAME, UserModel.LAST_NAME,
|
||||
UserModel.DISABLED_REASON, UserModel.EMAIL, UserModel.EMAIL_VERIFIED));
|
||||
CONFIG_PROPERTIES.add(userAttributeConfig);
|
||||
|
||||
ProviderConfigProperty aggregateAttributesConfig = new ProviderConfigProperty();
|
||||
aggregateAttributesConfig.setName(AGGREGATE_ATTRIBUTES_KEY);
|
||||
aggregateAttributesConfig.setLabel("Aggregate attributes");
|
||||
aggregateAttributesConfig.setHelpText("Should the mapper aggregate user attributes.");
|
||||
aggregateAttributesConfig.setType(ProviderConfigProperty.BOOLEAN_TYPE);
|
||||
CONFIG_PROPERTIES.add(aggregateAttributesConfig);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<ProviderConfigProperty> getIndividualConfigProperties() {
|
||||
return CONFIG_PROPERTIES;
|
||||
}
|
||||
|
||||
public void setClaimsForCredential(VerifiableCredential verifiableCredential,
|
||||
UserSessionModel userSessionModel) {
|
||||
// nothing to do for the mapper.
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setClaimsForSubject(Map<String, Object> claims, UserSessionModel userSessionModel) {
|
||||
String propertyName = mapperModel.getConfig().get(SUBJECT_PROPERTY_CONFIG_KEY);
|
||||
String userAttribute = mapperModel.getConfig().get(USER_ATTRIBUTE_KEY);
|
||||
boolean aggregateAttributes = Optional.ofNullable(mapperModel.getConfig().get(AGGREGATE_ATTRIBUTES_KEY))
|
||||
.map(Boolean::parseBoolean).orElse(false);
|
||||
Collection<String> attributes =
|
||||
KeycloakModelUtils.resolveAttribute(userSessionModel.getUser(), userAttribute,
|
||||
aggregateAttributes);
|
||||
attributes.removeAll(Collections.singleton(null));
|
||||
if (!attributes.isEmpty()) {
|
||||
claims.put(propertyName, String.join(",", attributes));
|
||||
}
|
||||
}
|
||||
|
||||
public static ProtocolMapperModel create(String mapperName, String userAttribute, String propertyName,
|
||||
boolean aggregateAttributes) {
|
||||
var mapperModel = new ProtocolMapperModel();
|
||||
mapperModel.setName(mapperName);
|
||||
Map<String, String> configMap = new HashMap<>();
|
||||
configMap.put(SUBJECT_PROPERTY_CONFIG_KEY, propertyName);
|
||||
configMap.put(USER_ATTRIBUTE_KEY, userAttribute);
|
||||
configMap.put(AGGREGATE_ATTRIBUTES_KEY, Boolean.toString(aggregateAttributes));
|
||||
mapperModel.setConfig(configMap);
|
||||
mapperModel.setProtocol(OID4VCLoginProtocolFactory.PROTOCOL_ID);
|
||||
mapperModel.setProtocolMapper(MAPPER_ID);
|
||||
return mapperModel;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDisplayType() {
|
||||
return "User Attribute Mapper";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getHelpText() {
|
||||
return "Maps user attributes to credential subject properties.";
|
||||
}
|
||||
|
||||
@Override
|
||||
public ProtocolMapper create(KeycloakSession session) {
|
||||
return new OID4VCUserAttributeMapper();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return MAPPER_ID;
|
||||
}
|
||||
}
|
|
@ -43,7 +43,6 @@ public class JwtSigningService extends SigningService<String> {
|
|||
private static final Logger LOGGER = Logger.getLogger(JwtSigningService.class);
|
||||
|
||||
private static final String ID_TEMPLATE = "urn:uuid:%s";
|
||||
private static final String TOKEN_TYPE = "JWT";
|
||||
private static final String VC_CLAIM_KEY = "vc";
|
||||
private static final String ID_CLAIM_KEY = "id";
|
||||
|
||||
|
|
|
@ -34,7 +34,6 @@ import org.keycloak.sdjwt.SdJwtUtils;
|
|||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.StringJoiner;
|
||||
import java.util.stream.IntStream;
|
||||
|
||||
/**
|
||||
|
|
|
@ -18,7 +18,6 @@
|
|||
package org.keycloak.protocol.oid4vc.issuance.signing;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.keycloak.common.util.Time;
|
||||
import org.keycloak.component.ComponentModel;
|
||||
import org.keycloak.component.ComponentValidationException;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
|
@ -50,7 +49,7 @@ public class SdJwtSigningServiceProviderFactory implements VCSigningServiceProvi
|
|||
String tokenType = model.get(SigningProperties.TOKEN_TYPE.getKey());
|
||||
String hashAlgorithm = model.get(SigningProperties.HASH_ALGORITHM.getKey());
|
||||
Optional<String> kid = Optional.ofNullable(model.get(SigningProperties.KID_HEADER.getKey()));
|
||||
int decoys = Integer.valueOf(model.get(SigningProperties.DECOYS.getKey()));
|
||||
int decoys = Integer.parseInt(model.get(SigningProperties.DECOYS.getKey()));
|
||||
|
||||
List<String> visibleClaims = Optional.ofNullable(model.get(SigningProperties.VISIBLE_CLAIMS.getKey()))
|
||||
.map(visibileClaims -> visibileClaims.split(","))
|
||||
|
|
|
@ -25,25 +25,23 @@ import org.keycloak.component.ComponentValidationException;
|
|||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.KeycloakSessionFactory;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.protocol.oid4vc.OID4VCEnvironmentProviderFactory;
|
||||
import org.keycloak.protocol.oid4vc.model.Format;
|
||||
import org.keycloak.provider.ConfigurationValidationHelper;
|
||||
import org.keycloak.provider.EnvironmentDependentProviderFactory;
|
||||
import org.keycloak.provider.ProviderConfigurationBuilder;
|
||||
|
||||
import java.time.Clock;
|
||||
|
||||
/**
|
||||
* Provider Factory to create {@link VerifiableCredentialsSigningService}s
|
||||
*
|
||||
* @author <a href="https://github.com/wistefan">Stefan Wiedemann</a>
|
||||
*/
|
||||
public interface VCSigningServiceProviderFactory extends ComponentFactory<VerifiableCredentialsSigningService, VerifiableCredentialsSigningService>, EnvironmentDependentProviderFactory {
|
||||
public interface VCSigningServiceProviderFactory extends ComponentFactory<VerifiableCredentialsSigningService, VerifiableCredentialsSigningService>, OID4VCEnvironmentProviderFactory {
|
||||
|
||||
/**
|
||||
* Key for the realm attribute providing the issuerDidy.
|
||||
*/
|
||||
String ISSUER_DID_REALM_ATTRIBUTE_KEY = "issuerDid";
|
||||
|
||||
|
||||
public static ProviderConfigurationBuilder configurationBuilder() {
|
||||
return ProviderConfigurationBuilder.create()
|
||||
.property(SigningProperties.KEY_ID.asConfigProperty());
|
||||
|
|
|
@ -0,0 +1,119 @@
|
|||
/*
|
||||
* 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 com.google.common.collect.ImmutableMap;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Represents a credentials issuer according to the OID4VCI Credentials Issuer Metadata
|
||||
* {@see https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html#name-credential-issuer-metadata}
|
||||
*
|
||||
* @author <a href="https://github.com/wistefan">Stefan Wiedemann</a>
|
||||
*/
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
public class CredentialIssuer {
|
||||
|
||||
@JsonProperty("credential_issuer")
|
||||
private String credentialIssuer;
|
||||
|
||||
@JsonProperty("credential_endpoint")
|
||||
private String credentialEndpoint;
|
||||
|
||||
@JsonProperty("authorization_servers")
|
||||
private List<String> authorizationServers;
|
||||
|
||||
@JsonProperty("batch_credential_endpoint")
|
||||
private String batchCredentialEndpoint;
|
||||
|
||||
@JsonProperty("notification_endpoint")
|
||||
private String notificationEndpoint;
|
||||
|
||||
@JsonProperty("credential_configurations_supported")
|
||||
private Map<String, SupportedCredentialConfiguration> credentialsSupported;
|
||||
|
||||
private DisplayObject display;
|
||||
|
||||
public String getCredentialIssuer() {
|
||||
return credentialIssuer;
|
||||
}
|
||||
|
||||
public CredentialIssuer setCredentialIssuer(String credentialIssuer) {
|
||||
this.credentialIssuer = credentialIssuer;
|
||||
return this;
|
||||
}
|
||||
|
||||
public String getCredentialEndpoint() {
|
||||
return credentialEndpoint;
|
||||
}
|
||||
|
||||
public CredentialIssuer setCredentialEndpoint(String credentialEndpoint) {
|
||||
this.credentialEndpoint = credentialEndpoint;
|
||||
return this;
|
||||
}
|
||||
|
||||
public String getBatchCredentialEndpoint() {
|
||||
return batchCredentialEndpoint;
|
||||
}
|
||||
|
||||
public CredentialIssuer setBatchCredentialEndpoint(String batchCredentialEndpoint) {
|
||||
this.batchCredentialEndpoint = batchCredentialEndpoint;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Map<String, SupportedCredentialConfiguration> getCredentialsSupported() {
|
||||
return credentialsSupported;
|
||||
}
|
||||
|
||||
public CredentialIssuer setCredentialsSupported(Map<String, SupportedCredentialConfiguration> credentialsSupported) {
|
||||
this.credentialsSupported = ImmutableMap.copyOf(credentialsSupported);
|
||||
return this;
|
||||
}
|
||||
|
||||
public DisplayObject getDisplay() {
|
||||
return display;
|
||||
}
|
||||
|
||||
public CredentialIssuer setDisplay(DisplayObject display) {
|
||||
this.display = display;
|
||||
return this;
|
||||
}
|
||||
|
||||
public List<String> getAuthorizationServers() {
|
||||
return authorizationServers;
|
||||
}
|
||||
|
||||
public CredentialIssuer setAuthorizationServers(List<String> authorizationServers) {
|
||||
this.authorizationServers = authorizationServers;
|
||||
return this;
|
||||
}
|
||||
|
||||
public String getNotificationEndpoint() {
|
||||
return notificationEndpoint;
|
||||
}
|
||||
|
||||
public CredentialIssuer setNotificationEndpoint(String notificationEndpoint) {
|
||||
this.notificationEndpoint = notificationEndpoint;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
/*
|
||||
* 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;
|
||||
|
||||
/**
|
||||
* Holds all information required to build a uri to a credentials offer.
|
||||
*
|
||||
* @author <a href="https://github.com/wistefan">Stefan Wiedemann</a>
|
||||
*/
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
public class CredentialOfferURI {
|
||||
private String issuer;
|
||||
private String nonce;
|
||||
|
||||
public String getIssuer() {
|
||||
return issuer;
|
||||
}
|
||||
|
||||
public CredentialOfferURI setIssuer(String issuer) {
|
||||
this.issuer = issuer;
|
||||
return this;
|
||||
}
|
||||
|
||||
public String getNonce() {
|
||||
return nonce;
|
||||
}
|
||||
|
||||
public CredentialOfferURI setNonce(String nonce) {
|
||||
this.nonce = nonce;
|
||||
return this;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,65 @@
|
|||
/*
|
||||
* 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;
|
||||
|
||||
/**
|
||||
* Represents a CredentialRequest according to OID4VCI
|
||||
* {@see https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html#name-credential-request}
|
||||
*
|
||||
* @author <a href="https://github.com/wistefan">Stefan Wiedemann</a>
|
||||
*/
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
public class CredentialRequest {
|
||||
|
||||
private Format format;
|
||||
|
||||
@JsonProperty("credential_identifier")
|
||||
private String credentialIdentifier;
|
||||
|
||||
private Proof proof;
|
||||
|
||||
public Format getFormat() {
|
||||
return format;
|
||||
}
|
||||
|
||||
public CredentialRequest setFormat(Format format) {
|
||||
this.format = format;
|
||||
return this;
|
||||
}
|
||||
|
||||
public String getCredentialIdentifier() {
|
||||
return credentialIdentifier;
|
||||
}
|
||||
|
||||
public CredentialRequest setCredentialIdentifier(String credentialIdentifier) {
|
||||
this.credentialIdentifier = credentialIdentifier;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Proof getProof() {
|
||||
return proof;
|
||||
}
|
||||
|
||||
public CredentialRequest setProof(Proof proof) {
|
||||
this.proof = proof;
|
||||
return this;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,79 @@
|
|||
/*
|
||||
* 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;
|
||||
|
||||
/**
|
||||
* Represents a CredentialResponse according to the OID4VCI Spec
|
||||
* {@see https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html#name-credential-response}
|
||||
*
|
||||
* @author <a href="https://github.com/wistefan">Stefan Wiedemann</a>
|
||||
*/
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
public class CredentialResponse {
|
||||
|
||||
// concrete type depends on the format
|
||||
private Object credential;
|
||||
|
||||
@JsonProperty("c_nonce")
|
||||
private String cNonce;
|
||||
|
||||
@JsonProperty("c_nonce_expires_in")
|
||||
private String cNonceExpiresIn;
|
||||
|
||||
@JsonProperty("notification_id")
|
||||
private String notificationId;
|
||||
|
||||
public Object getCredential() {
|
||||
return credential;
|
||||
}
|
||||
|
||||
public CredentialResponse setCredential(Object credential) {
|
||||
this.credential = credential;
|
||||
return this;
|
||||
}
|
||||
|
||||
public String getcNonce() {
|
||||
return cNonce;
|
||||
}
|
||||
|
||||
public CredentialResponse setcNonce(String cNonce) {
|
||||
this.cNonce = cNonce;
|
||||
return this;
|
||||
}
|
||||
|
||||
public String getcNonceExpiresIn() {
|
||||
return cNonceExpiresIn;
|
||||
}
|
||||
|
||||
public CredentialResponse setcNonceExpiresIn(String cNonceExpiresIn) {
|
||||
this.cNonceExpiresIn = cNonceExpiresIn;
|
||||
return this;
|
||||
}
|
||||
|
||||
public String getNotificationId() {
|
||||
return notificationId;
|
||||
}
|
||||
|
||||
public CredentialResponse setNotificationId(String notificationId) {
|
||||
this.notificationId = notificationId;
|
||||
return this;
|
||||
}
|
||||
}
|
|
@ -33,7 +33,6 @@ import java.util.Map;
|
|||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
public class CredentialSubject {
|
||||
|
||||
|
||||
@JsonIgnore
|
||||
private Map<String, Object> claims = new HashMap<>();
|
||||
|
||||
|
@ -46,4 +45,9 @@ public class CredentialSubject {
|
|||
public void setClaims(String name, Object claim) {
|
||||
claims.put(name, claim);
|
||||
}
|
||||
|
||||
public CredentialSubject setClaims(Map<String, Object> claims) {
|
||||
this.claims = claims;
|
||||
return this;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,71 @@
|
|||
/*
|
||||
* 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 com.google.common.collect.ImmutableList;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Represents a CredentialsOffer according to the OID4VCI Spec
|
||||
* {@see https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html#name-credential-offer}
|
||||
*
|
||||
* @author <a href="https://github.com/wistefan">Stefan Wiedemann</a>
|
||||
*/
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
public class CredentialsOffer {
|
||||
|
||||
@JsonProperty("credential_issuer")
|
||||
private String credentialIssuer;
|
||||
|
||||
//ids of credentials as offered in the issuer metadata
|
||||
@JsonProperty("credential_configuration_ids")
|
||||
private List<String> credentialConfigurationIds;
|
||||
|
||||
// current implementation only supports pre-authorized codes.
|
||||
private PreAuthorizedGrant grants;
|
||||
|
||||
public String getCredentialIssuer() {
|
||||
return credentialIssuer;
|
||||
}
|
||||
|
||||
public CredentialsOffer setCredentialIssuer(String credentialIssuer) {
|
||||
this.credentialIssuer = credentialIssuer;
|
||||
return this;
|
||||
}
|
||||
|
||||
public List<String> getCredentialConfigurationIds() {
|
||||
return credentialConfigurationIds;
|
||||
}
|
||||
|
||||
public CredentialsOffer setCredentialConfigurationIds(List<String> credentialConfigurationIds) {
|
||||
this.credentialConfigurationIds = ImmutableList.copyOf(credentialConfigurationIds);
|
||||
return this;
|
||||
}
|
||||
|
||||
public PreAuthorizedGrant getGrants() {
|
||||
return grants;
|
||||
}
|
||||
|
||||
public CredentialsOffer setGrants(PreAuthorizedGrant grants) {
|
||||
this.grants = grants;
|
||||
return this;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,176 @@
|
|||
/*
|
||||
* 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.JsonAutoDetect;
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* Represents a DisplayObject, as used in the OID4VCI Credentials Issuer Metadata
|
||||
* {@see https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html#name-credential-issuer-metadata}
|
||||
*
|
||||
* @author <a href="https://github.com/wistefan">Stefan Wiedemann</a>
|
||||
*/
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
@JsonAutoDetect(
|
||||
getterVisibility = JsonAutoDetect.Visibility.NONE,
|
||||
isGetterVisibility = JsonAutoDetect.Visibility.NONE,
|
||||
setterVisibility = JsonAutoDetect.Visibility.NONE
|
||||
)
|
||||
public class DisplayObject {
|
||||
|
||||
@JsonIgnore
|
||||
private static final String NAME_KEY = "name";
|
||||
@JsonIgnore
|
||||
private static final String LOCALE_KEY = "locale";
|
||||
@JsonIgnore
|
||||
private static final String LOGO_KEY = "logo";
|
||||
@JsonIgnore
|
||||
private static final String DESCRIPTION_KEY = "description";
|
||||
@JsonIgnore
|
||||
private static final String BG_COLOR_KEY = "background_color";
|
||||
@JsonIgnore
|
||||
private static final String TEXT_COLOR_KEY = "text_color";
|
||||
|
||||
@JsonProperty(DisplayObject.NAME_KEY)
|
||||
private String name;
|
||||
|
||||
@JsonProperty(DisplayObject.LOCALE_KEY)
|
||||
private String locale;
|
||||
|
||||
@JsonProperty(DisplayObject.LOGO_KEY)
|
||||
private String logo;
|
||||
|
||||
@JsonProperty(DisplayObject.DESCRIPTION_KEY)
|
||||
private String description;
|
||||
|
||||
@JsonProperty(DisplayObject.BG_COLOR_KEY)
|
||||
private String backgroundColor;
|
||||
|
||||
@JsonProperty(DisplayObject.TEXT_COLOR_KEY)
|
||||
private String textColor;
|
||||
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public DisplayObject setName(String name) {
|
||||
this.name = name;
|
||||
return this;
|
||||
}
|
||||
|
||||
public String getLocale() {
|
||||
return locale;
|
||||
}
|
||||
|
||||
public DisplayObject setLocale(String locale) {
|
||||
this.locale = locale;
|
||||
return this;
|
||||
}
|
||||
|
||||
public String getLogo() {
|
||||
return logo;
|
||||
}
|
||||
|
||||
public DisplayObject setLogo(String logo) {
|
||||
this.logo = logo;
|
||||
return this;
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
return description;
|
||||
}
|
||||
|
||||
public DisplayObject setDescription(String description) {
|
||||
this.description = description;
|
||||
return this;
|
||||
}
|
||||
|
||||
public String getBackgroundColor() {
|
||||
return backgroundColor;
|
||||
}
|
||||
|
||||
public DisplayObject setBackgroundColor(String backgroundColor) {
|
||||
this.backgroundColor = backgroundColor;
|
||||
return this;
|
||||
}
|
||||
|
||||
public String getTextColor() {
|
||||
return textColor;
|
||||
}
|
||||
|
||||
public DisplayObject setTextColor(String textColor) {
|
||||
this.textColor = textColor;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Map<String, String> toDotNotation() {
|
||||
Map<String, String> dotNotation = new HashMap<>();
|
||||
dotNotation.put(NAME_KEY, name);
|
||||
dotNotation.put(LOCALE_KEY, locale);
|
||||
dotNotation.put(LOGO_KEY, logo);
|
||||
dotNotation.put(DESCRIPTION_KEY, description);
|
||||
dotNotation.put(BG_COLOR_KEY, backgroundColor);
|
||||
dotNotation.put(TEXT_COLOR_KEY, textColor);
|
||||
return dotNotation;
|
||||
}
|
||||
|
||||
public static DisplayObject fromDotNotation(Map<String, String> dotNotated) {
|
||||
DisplayObject displayObject = new DisplayObject();
|
||||
Optional.ofNullable(dotNotated.get(NAME_KEY)).ifPresent(displayObject::setName);
|
||||
Optional.ofNullable(dotNotated.get(LOCALE_KEY)).ifPresent(displayObject::setLocale);
|
||||
Optional.ofNullable(dotNotated.get(LOGO_KEY)).ifPresent(displayObject::setLogo);
|
||||
Optional.ofNullable(dotNotated.get(DESCRIPTION_KEY)).ifPresent(displayObject::setDescription);
|
||||
Optional.ofNullable(dotNotated.get(BG_COLOR_KEY)).ifPresent(displayObject::setBackgroundColor);
|
||||
Optional.ofNullable(dotNotated.get(TEXT_COLOR_KEY)).ifPresent(displayObject::setTextColor);
|
||||
return displayObject;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (!(o instanceof DisplayObject that)) return false;
|
||||
|
||||
if (getName() != null ? !getName().equals(that.getName()) : that.getName() != null) return false;
|
||||
if (getLocale() != null ? !getLocale().equals(that.getLocale()) : that.getLocale() != null) return false;
|
||||
if (getLogo() != null ? !getLogo().equals(that.getLogo()) : that.getLogo() != null) return false;
|
||||
if (getDescription() != null ? !getDescription().equals(that.getDescription()) : that.getDescription() != null)
|
||||
return false;
|
||||
if (getBackgroundColor() != null ? !getBackgroundColor().equals(that.getBackgroundColor()) : that.getBackgroundColor() != null)
|
||||
return false;
|
||||
return getTextColor() != null ? getTextColor().equals(that.getTextColor()) : that.getTextColor() == null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int result = getName() != null ? getName().hashCode() : 0;
|
||||
result = 31 * result + (getLocale() != null ? getLocale().hashCode() : 0);
|
||||
result = 31 * result + (getLogo() != null ? getLogo().hashCode() : 0);
|
||||
result = 31 * result + (getDescription() != null ? getDescription().hashCode() : 0);
|
||||
result = 31 * result + (getBackgroundColor() != null ? getBackgroundColor().hashCode() : 0);
|
||||
result = 31 * result + (getTextColor() != null ? getTextColor().hashCode() : 0);
|
||||
return result;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,77 @@
|
|||
/*
|
||||
* 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;
|
||||
|
||||
/**
|
||||
* Represents an error response, containing the error type as defined by OID4VCI
|
||||
*
|
||||
* @author <a href="https://github.com/wistefan">Stefan Wiedemann</a>
|
||||
*/
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
public class ErrorResponse {
|
||||
|
||||
private ErrorType error;
|
||||
|
||||
@JsonProperty("error_description")
|
||||
private String errorDescription;
|
||||
|
||||
@JsonProperty("c_nonce")
|
||||
private String cNonce;
|
||||
|
||||
@JsonProperty("c_nonce_expires_in")
|
||||
private long cNonceExpiresIn;
|
||||
|
||||
public ErrorType getError() {
|
||||
return error;
|
||||
}
|
||||
|
||||
public ErrorResponse setError(ErrorType error) {
|
||||
this.error = error;
|
||||
return this;
|
||||
}
|
||||
|
||||
public String getErrorDescription() {
|
||||
return errorDescription;
|
||||
}
|
||||
|
||||
public ErrorResponse setErrorDescription(String errorDescription) {
|
||||
this.errorDescription = errorDescription;
|
||||
return this;
|
||||
}
|
||||
|
||||
public String getcNonce() {
|
||||
return cNonce;
|
||||
}
|
||||
|
||||
public ErrorResponse setcNonce(String cNonce) {
|
||||
this.cNonce = cNonce;
|
||||
return this;
|
||||
}
|
||||
|
||||
public long getcNonceExpiresIn() {
|
||||
return cNonceExpiresIn;
|
||||
}
|
||||
|
||||
public ErrorResponse setcNonceExpiresIn(long cNonceExpiresIn) {
|
||||
this.cNonceExpiresIn = cNonceExpiresIn;
|
||||
return this;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
/*
|
||||
* 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;
|
||||
|
||||
|
||||
/**
|
||||
* Enum to handle potential errors in issuing credentials with the error types defined in OID4VCI
|
||||
* {@see https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html}
|
||||
*
|
||||
* @author <a href="https://github.com/wistefan">Stefan Wiedemann</a>
|
||||
*/
|
||||
public enum ErrorType {
|
||||
|
||||
INVALID_CREDENTIAL_REQUEST("invalid_credential_request"),
|
||||
INVALID_TOKEN("invalid_token"),
|
||||
UNSUPPORTED_CREDENTIAL_TYPE("unsupported_credential_type"),
|
||||
UNSUPPORTED_CREDENTIAL_FORMAT("unsupported_credential_format"),
|
||||
INVALID_PROOF("invalid_proof"),
|
||||
INVALID_ENCRYPTION_PARAMETER("invalid_encryption_parameters");
|
||||
|
||||
private final String value;
|
||||
|
||||
ErrorType(String value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
public String getValue() {
|
||||
return value;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,133 @@
|
|||
/*
|
||||
* 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.google.common.collect.ImmutableList;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Pojo, containing all information required to create a VCClient.
|
||||
*
|
||||
* @author <a href="https://github.com/wistefan">Stefan Wiedemann</a>
|
||||
*/
|
||||
public class OID4VCClient {
|
||||
|
||||
/**
|
||||
* Id of the client.
|
||||
*/
|
||||
private String id;
|
||||
|
||||
/**
|
||||
* Did of the target/client, will be used as client-id
|
||||
*/
|
||||
private String clientDid;
|
||||
/**
|
||||
* Comma-separated list of supported credentials types
|
||||
*/
|
||||
private List<SupportedCredentialConfiguration> supportedVCTypes;
|
||||
/**
|
||||
* Description of the client, will f.e. be displayed in the admin-console
|
||||
*/
|
||||
private String description;
|
||||
/**
|
||||
* Human-readable name of the client
|
||||
*/
|
||||
private String name;
|
||||
|
||||
public OID4VCClient() {
|
||||
}
|
||||
|
||||
public OID4VCClient(String id, String clientDid, List<SupportedCredentialConfiguration> supportedVCTypes, String description, String name) {
|
||||
this.id = id;
|
||||
this.clientDid = clientDid;
|
||||
this.supportedVCTypes = supportedVCTypes;
|
||||
this.description = description;
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public OID4VCClient setId(String id) {
|
||||
this.id = id;
|
||||
return this;
|
||||
}
|
||||
|
||||
public String getClientDid() {
|
||||
return clientDid;
|
||||
}
|
||||
|
||||
public OID4VCClient setClientDid(String clientDid) {
|
||||
this.clientDid = clientDid;
|
||||
return this;
|
||||
}
|
||||
|
||||
public List<SupportedCredentialConfiguration> getSupportedVCTypes() {
|
||||
return supportedVCTypes;
|
||||
}
|
||||
|
||||
public OID4VCClient setSupportedVCTypes(List<SupportedCredentialConfiguration> supportedVCTypes) {
|
||||
this.supportedVCTypes = ImmutableList.copyOf(supportedVCTypes);
|
||||
return this;
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
return description;
|
||||
}
|
||||
|
||||
public OID4VCClient setDescription(String description) {
|
||||
this.description = description;
|
||||
return this;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public OID4VCClient setName(String name) {
|
||||
this.name = name;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (!(o instanceof OID4VCClient that)) return false;
|
||||
|
||||
if (getId() != null ? !getId().equals(that.getId()) : that.getId() != null) return false;
|
||||
if (getClientDid() != null ? !getClientDid().equals(that.getClientDid()) : that.getClientDid() != null)
|
||||
return false;
|
||||
if (getSupportedVCTypes() != null ? !getSupportedVCTypes().equals(that.getSupportedVCTypes()) : that.getSupportedVCTypes() != null)
|
||||
return false;
|
||||
if (getDescription() != null ? !getDescription().equals(that.getDescription()) : that.getDescription() != null)
|
||||
return false;
|
||||
return getName() != null ? getName().equals(that.getName()) : that.getName() == null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int result = getId() != null ? getId().hashCode() : 0;
|
||||
result = 31 * result + (getClientDid() != null ? getClientDid().hashCode() : 0);
|
||||
result = 31 * result + (getSupportedVCTypes() != null ? getSupportedVCTypes().hashCode() : 0);
|
||||
result = 31 * result + (getDescription() != null ? getDescription().hashCode() : 0);
|
||||
result = 31 * result + (getName() != null ? getName().hashCode() : 0);
|
||||
return result;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,79 @@
|
|||
/*
|
||||
* 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;
|
||||
|
||||
/**
|
||||
* Represents a pre-authorized grant, as used by the Credential Offer in OID4VCI
|
||||
* {@see https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html#name-credential-offer}
|
||||
*
|
||||
* @author <a href="https://github.com/wistefan">Stefan Wiedemann</a>
|
||||
*/
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
public class PreAuthorizedCode {
|
||||
|
||||
@JsonProperty("pre-authorized_code")
|
||||
private String preAuthorizedCode;
|
||||
|
||||
@JsonProperty("tx_code")
|
||||
private TxCode txCode;
|
||||
|
||||
@JsonProperty("interval")
|
||||
private long interval;
|
||||
|
||||
@JsonProperty("authorization_server")
|
||||
private String authorizationServer;
|
||||
|
||||
public String getPreAuthorizedCode() {
|
||||
return preAuthorizedCode;
|
||||
}
|
||||
|
||||
public PreAuthorizedCode setPreAuthorizedCode(String preAuthorizedCode) {
|
||||
this.preAuthorizedCode = preAuthorizedCode;
|
||||
return this;
|
||||
}
|
||||
|
||||
public TxCode getTxCode() {
|
||||
return txCode;
|
||||
}
|
||||
|
||||
public PreAuthorizedCode setTxCode(TxCode txCode) {
|
||||
this.txCode = txCode;
|
||||
return this;
|
||||
}
|
||||
|
||||
public long getInterval() {
|
||||
return interval;
|
||||
}
|
||||
|
||||
public PreAuthorizedCode setInterval(long interval) {
|
||||
this.interval = interval;
|
||||
return this;
|
||||
}
|
||||
|
||||
public String getAuthorizationServer() {
|
||||
return authorizationServer;
|
||||
}
|
||||
|
||||
public PreAuthorizedCode setAuthorizationServer(String authorizationServer) {
|
||||
this.authorizationServer = authorizationServer;
|
||||
return this;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
/*
|
||||
* 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.protocol.oidc.grants.PreAuthorizedCodeGrantType;
|
||||
import org.keycloak.protocol.oidc.grants.PreAuthorizedCodeGrantTypeFactory;
|
||||
|
||||
/**
|
||||
* Container for the pre-authorized code to be used in a Credential Offer
|
||||
* <p>
|
||||
* {@see https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html#name-credential-offer}
|
||||
*
|
||||
* @author <a href="https://github.com/wistefan">Stefan Wiedemann</a>
|
||||
*/
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
public class PreAuthorizedGrant {
|
||||
|
||||
@JsonProperty(PreAuthorizedCodeGrantTypeFactory.GRANT_TYPE)
|
||||
private PreAuthorizedCode preAuthorizedCode;
|
||||
|
||||
public PreAuthorizedCode getPreAuthorizedCode() {
|
||||
return preAuthorizedCode;
|
||||
}
|
||||
|
||||
public PreAuthorizedGrant setPreAuthorizedCode(PreAuthorizedCode preAuthorizedCode) {
|
||||
this.preAuthorizedCode = preAuthorizedCode;
|
||||
return this;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,54 @@
|
|||
/*
|
||||
* 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;
|
||||
|
||||
/**
|
||||
* Proof to be used in the Credential Request(to allow holder binding) according to OID4VCI
|
||||
* {@see https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html#name-credential-request}
|
||||
*
|
||||
* @author <a href="https://github.com/wistefan">Stefan Wiedemann</a>
|
||||
*/
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
public class Proof {
|
||||
|
||||
@JsonProperty("proof_type")
|
||||
private ProofType proofType;
|
||||
|
||||
private Object proofObject;
|
||||
|
||||
public ProofType getProofType() {
|
||||
return proofType;
|
||||
}
|
||||
|
||||
public Proof setProofType(ProofType proofType) {
|
||||
this.proofType = proofType;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Object getProofObject() {
|
||||
return proofObject;
|
||||
}
|
||||
|
||||
public Proof setProofObject(Object proofObject) {
|
||||
this.proofObject = proofObject;
|
||||
return this;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
/*
|
||||
* 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;
|
||||
|
||||
|
||||
/**
|
||||
* Enum to provide potential proof types for holder-binding
|
||||
*
|
||||
* @author <a href="https://github.com/wistefan">Stefan Wiedemann</a>
|
||||
*/
|
||||
public enum ProofType {
|
||||
|
||||
JWT("jwt"),
|
||||
LD_PROOF("ldp_vp"),
|
||||
CWT("cwt");
|
||||
|
||||
private final String value;
|
||||
|
||||
ProofType(String value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
public String getValue() {
|
||||
return value;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,72 @@
|
|||
/*
|
||||
* 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.google.common.collect.ImmutableSet;
|
||||
import org.keycloak.protocol.oid4vc.issuance.mappers.OID4VCTargetRoleMapper;
|
||||
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Pojo representation of a role to be added by the {@link OID4VCTargetRoleMapper}
|
||||
*
|
||||
* @author <a href="https://github.com/wistefan">Stefan Wiedemann</a>
|
||||
*/
|
||||
public class Role {
|
||||
|
||||
private Set<String> names;
|
||||
private String target;
|
||||
|
||||
public Role() {
|
||||
}
|
||||
|
||||
public Role(Set<String> names, String target) {
|
||||
this.names = ImmutableSet.copyOf(names);
|
||||
this.target = target;
|
||||
}
|
||||
|
||||
public Set<String> getNames() {
|
||||
return names;
|
||||
}
|
||||
|
||||
public void setNames(Set<String> names) {
|
||||
this.names = names;
|
||||
}
|
||||
|
||||
public String getTarget() {
|
||||
return target;
|
||||
}
|
||||
|
||||
public void setTarget(String target) {
|
||||
this.target = target;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
Role role = (Role) o;
|
||||
return Objects.equals(names, role.names) && Objects.equals(target, role.target);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(names, target);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,216 @@
|
|||
/*
|
||||
* 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.JsonIgnore;
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* A supported credential, as used in the Credentials Issuer Metadata in OID4VCI
|
||||
* {@see https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html#name-credential-issuer-metadata}
|
||||
*/
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
public class SupportedCredentialConfiguration {
|
||||
|
||||
private static final String DOT_SEPARATOR = ".";
|
||||
|
||||
@JsonIgnore
|
||||
private static final String FORMAT_KEY = "format";
|
||||
@JsonIgnore
|
||||
private static final String SCOPE_KEY = "scope";
|
||||
@JsonIgnore
|
||||
private static final String CRYPTOGRAPHIC_BINDING_METHODS_SUPPORTED_KEY = " credential_signing_alg_values_supported";
|
||||
@JsonIgnore
|
||||
private static final String CRYPTOGRAPHIC_SUITES_SUPPORTED_KEY = "cryptographic_suites_supported";
|
||||
@JsonIgnore
|
||||
private static final String CREDENTIAL_SIGNING_ALG_VALUES_SUPPORTED_KEY = "credential_signing_alg_values_supported";
|
||||
@JsonIgnore
|
||||
private static final String DISPLAY_KEY = "display";
|
||||
private String id;
|
||||
|
||||
@JsonProperty(FORMAT_KEY)
|
||||
private Format format;
|
||||
|
||||
@JsonProperty(SCOPE_KEY)
|
||||
private String scope;
|
||||
|
||||
@JsonProperty(CRYPTOGRAPHIC_BINDING_METHODS_SUPPORTED_KEY)
|
||||
private List<String> cryptographicBindingMethodsSupported;
|
||||
|
||||
@JsonProperty(CRYPTOGRAPHIC_SUITES_SUPPORTED_KEY)
|
||||
private List<String> cryptographicSuitesSupported;
|
||||
|
||||
@JsonProperty(CREDENTIAL_SIGNING_ALG_VALUES_SUPPORTED_KEY)
|
||||
private List<String> credentialSigningAlgValuesSupported;
|
||||
|
||||
@JsonProperty(DISPLAY_KEY)
|
||||
private DisplayObject display;
|
||||
|
||||
public Format getFormat() {
|
||||
return format;
|
||||
}
|
||||
|
||||
public SupportedCredentialConfiguration setFormat(Format format) {
|
||||
this.format = format;
|
||||
return this;
|
||||
}
|
||||
|
||||
public String getScope() {
|
||||
return scope;
|
||||
}
|
||||
|
||||
public SupportedCredentialConfiguration setScope(String scope) {
|
||||
this.scope = scope;
|
||||
return this;
|
||||
}
|
||||
|
||||
public List<String> getCryptographicBindingMethodsSupported() {
|
||||
return cryptographicBindingMethodsSupported;
|
||||
}
|
||||
|
||||
public SupportedCredentialConfiguration setCryptographicBindingMethodsSupported(List<String> cryptographicBindingMethodsSupported) {
|
||||
this.cryptographicBindingMethodsSupported = ImmutableList.copyOf(cryptographicBindingMethodsSupported);
|
||||
return this;
|
||||
}
|
||||
|
||||
public List<String> getCryptographicSuitesSupported() {
|
||||
return cryptographicSuitesSupported;
|
||||
}
|
||||
|
||||
public SupportedCredentialConfiguration setCryptographicSuitesSupported(List<String> cryptographicSuitesSupported) {
|
||||
this.cryptographicSuitesSupported = ImmutableList.copyOf(cryptographicSuitesSupported);
|
||||
return this;
|
||||
}
|
||||
|
||||
public DisplayObject getDisplay() {
|
||||
return display;
|
||||
}
|
||||
|
||||
public SupportedCredentialConfiguration setDisplay(DisplayObject display) {
|
||||
this.display = display;
|
||||
return this;
|
||||
}
|
||||
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public SupportedCredentialConfiguration setId(String id) {
|
||||
if (id.contains(".")) {
|
||||
throw new IllegalArgumentException("dots are not supported as part of the supported credentials id.");
|
||||
}
|
||||
this.id = id;
|
||||
return this;
|
||||
}
|
||||
|
||||
public List<String> getCredentialSigningAlgValuesSupported() {
|
||||
return credentialSigningAlgValuesSupported;
|
||||
}
|
||||
|
||||
public SupportedCredentialConfiguration setCredentialSigningAlgValuesSupported(List<String> credentialSigningAlgValuesSupported) {
|
||||
this.credentialSigningAlgValuesSupported = ImmutableList.copyOf(credentialSigningAlgValuesSupported);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Map<String, String> toDotNotation() {
|
||||
Map<String, String> dotNotation = new HashMap<>();
|
||||
Optional.ofNullable(format).ifPresent(format -> dotNotation.put(id + DOT_SEPARATOR + FORMAT_KEY, format.toString()));
|
||||
Optional.ofNullable(scope).ifPresent(scope -> dotNotation.put(id + DOT_SEPARATOR + SCOPE_KEY, scope));
|
||||
Optional.ofNullable(cryptographicBindingMethodsSupported).ifPresent(types ->
|
||||
dotNotation.put(id + DOT_SEPARATOR + CRYPTOGRAPHIC_BINDING_METHODS_SUPPORTED_KEY, String.join(",", cryptographicBindingMethodsSupported)));
|
||||
Optional.ofNullable(cryptographicSuitesSupported).ifPresent(types ->
|
||||
dotNotation.put(id + DOT_SEPARATOR + CRYPTOGRAPHIC_SUITES_SUPPORTED_KEY, String.join(",", cryptographicSuitesSupported)));
|
||||
Optional.ofNullable(cryptographicSuitesSupported).ifPresent(types ->
|
||||
dotNotation.put(id + DOT_SEPARATOR + CREDENTIAL_SIGNING_ALG_VALUES_SUPPORTED_KEY, String.join(",", credentialSigningAlgValuesSupported)));
|
||||
|
||||
Map<String, String> dotNotatedDisplay = Optional.ofNullable(display)
|
||||
.map(DisplayObject::toDotNotation)
|
||||
.orElse(Map.of());
|
||||
dotNotatedDisplay.entrySet().stream()
|
||||
.filter(entry -> entry.getValue() != null)
|
||||
.forEach(entry -> dotNotation.put(id + DOT_SEPARATOR + DISPLAY_KEY + "." + entry.getKey(), entry.getValue()));
|
||||
return dotNotation;
|
||||
}
|
||||
|
||||
public static SupportedCredentialConfiguration fromDotNotation(String credentialId, Map<String, String> dotNotated) {
|
||||
|
||||
SupportedCredentialConfiguration supportedCredentialConfiguration = new SupportedCredentialConfiguration().setId(credentialId);
|
||||
Optional.ofNullable(dotNotated.get(credentialId + DOT_SEPARATOR + FORMAT_KEY)).map(Format::fromString).ifPresent(supportedCredentialConfiguration::setFormat);
|
||||
Optional.ofNullable(dotNotated.get(credentialId + DOT_SEPARATOR + SCOPE_KEY)).ifPresent(supportedCredentialConfiguration::setScope);
|
||||
Optional.ofNullable(dotNotated.get(credentialId + DOT_SEPARATOR + CRYPTOGRAPHIC_BINDING_METHODS_SUPPORTED_KEY))
|
||||
.map(cbms -> cbms.split(","))
|
||||
.map(Arrays::asList)
|
||||
.ifPresent(supportedCredentialConfiguration::setCryptographicBindingMethodsSupported);
|
||||
Optional.ofNullable(dotNotated.get(credentialId + DOT_SEPARATOR + CRYPTOGRAPHIC_SUITES_SUPPORTED_KEY))
|
||||
.map(css -> css.split(","))
|
||||
.map(Arrays::asList)
|
||||
.ifPresent(supportedCredentialConfiguration::setCryptographicSuitesSupported);
|
||||
Optional.ofNullable(dotNotated.get(credentialId + DOT_SEPARATOR + CREDENTIAL_SIGNING_ALG_VALUES_SUPPORTED_KEY))
|
||||
.map(css -> css.split(","))
|
||||
.map(Arrays::asList)
|
||||
.ifPresent(supportedCredentialConfiguration::setCredentialSigningAlgValuesSupported);
|
||||
Map<String, String> displayMap = new HashMap<>();
|
||||
dotNotated.entrySet().forEach(entry -> {
|
||||
String key = entry.getKey();
|
||||
if (key.startsWith(credentialId + DOT_SEPARATOR + DISPLAY_KEY)) {
|
||||
displayMap.put(key.substring((credentialId + DOT_SEPARATOR + DISPLAY_KEY).length() + 1), entry.getValue());
|
||||
}
|
||||
});
|
||||
if (!displayMap.isEmpty()) {
|
||||
supportedCredentialConfiguration.setDisplay(DisplayObject.fromDotNotation(displayMap));
|
||||
}
|
||||
return supportedCredentialConfiguration;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (!(o instanceof SupportedCredentialConfiguration that)) return false;
|
||||
|
||||
if (getId() != null ? !getId().equals(that.getId()) : that.getId() != null) return false;
|
||||
if (getFormat() != that.getFormat()) return false;
|
||||
if (getScope() != null ? !getScope().equals(that.getScope()) : that.getScope() != null) return false;
|
||||
if (getCryptographicBindingMethodsSupported() != null ? !getCryptographicBindingMethodsSupported().equals(that.getCryptographicBindingMethodsSupported()) : that.getCryptographicBindingMethodsSupported() != null)
|
||||
return false;
|
||||
if (getCryptographicSuitesSupported() != null ? !getCryptographicSuitesSupported().equals(that.getCryptographicSuitesSupported()) : that.getCryptographicSuitesSupported() != null)
|
||||
return false;
|
||||
if (getCredentialSigningAlgValuesSupported() != null ? !getCredentialSigningAlgValuesSupported().equals(that.getCredentialSigningAlgValuesSupported()) : that.getCredentialSigningAlgValuesSupported() != null)
|
||||
return false;
|
||||
return getDisplay() != null ? getDisplay().equals(that.getDisplay()) : that.getDisplay() == null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int result = getId() != null ? getId().hashCode() : 0;
|
||||
result = 31 * result + (getFormat() != null ? getFormat().hashCode() : 0);
|
||||
result = 31 * result + (getScope() != null ? getScope().hashCode() : 0);
|
||||
result = 31 * result + (getCryptographicBindingMethodsSupported() != null ? getCryptographicBindingMethodsSupported().hashCode() : 0);
|
||||
result = 31 * result + (getCryptographicSuitesSupported() != null ? getCryptographicSuitesSupported().hashCode() : 0);
|
||||
result = 31 * result + (getCredentialSigningAlgValuesSupported() != null ? getCredentialSigningAlgValuesSupported().hashCode() : 0);
|
||||
result = 31 * result + (getDisplay() != null ? getDisplay().hashCode() : 0);
|
||||
return result;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,67 @@
|
|||
/*
|
||||
* 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;
|
||||
|
||||
/**
|
||||
* Represents a transaction code as used in the pre-authorized grant in the Credential Offer in OID4VCI
|
||||
* {@see https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html#name-credential-offer}
|
||||
*
|
||||
* @author <a href="https://github.com/wistefan">Stefan Wiedemann</a>
|
||||
*/
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
public class TxCode {
|
||||
|
||||
@JsonProperty("input_mode")
|
||||
private String inputMode;
|
||||
|
||||
@JsonProperty("length")
|
||||
private int length;
|
||||
|
||||
@JsonProperty("description")
|
||||
private String description;
|
||||
|
||||
public String getInputMode() {
|
||||
return inputMode;
|
||||
}
|
||||
|
||||
public TxCode setInputMode(String inputMode) {
|
||||
this.inputMode = inputMode;
|
||||
return this;
|
||||
}
|
||||
|
||||
public int getLength() {
|
||||
return length;
|
||||
}
|
||||
|
||||
public TxCode setLength(int length) {
|
||||
this.length = length;
|
||||
return this;
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
return description;
|
||||
}
|
||||
|
||||
public TxCode setDescription(String description) {
|
||||
this.description = description;
|
||||
return this;
|
||||
}
|
||||
}
|
|
@ -55,67 +55,76 @@ public class VerifiableCredential {
|
|||
}
|
||||
|
||||
@JsonAnySetter
|
||||
public void setAdditionalProperties(String name, Object property) {
|
||||
public VerifiableCredential setAdditionalProperties(String name, Object property) {
|
||||
additionalProperties.put(name, property);
|
||||
return this;
|
||||
}
|
||||
|
||||
public List<String> getContext() {
|
||||
return context;
|
||||
}
|
||||
|
||||
public void setContext(List<String> context) {
|
||||
public VerifiableCredential setContext(List<String> context) {
|
||||
this.context = context;
|
||||
return this;
|
||||
}
|
||||
|
||||
public List<String> getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public void setType(List<String> type) {
|
||||
public VerifiableCredential setType(List<String> type) {
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
public void addType(String type) {
|
||||
this.type.add(type);
|
||||
return this;
|
||||
}
|
||||
|
||||
public URI getIssuer() {
|
||||
return issuer;
|
||||
}
|
||||
|
||||
public void setIssuer(URI issuer) {
|
||||
public VerifiableCredential setIssuer(URI issuer) {
|
||||
this.issuer = issuer;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Date getIssuanceDate() {
|
||||
return issuanceDate;
|
||||
}
|
||||
|
||||
public void setIssuanceDate(Date issuanceDate) {
|
||||
public VerifiableCredential setIssuanceDate(Date issuanceDate) {
|
||||
this.issuanceDate = issuanceDate;
|
||||
}
|
||||
|
||||
public Date getExpirationDate() {
|
||||
return expirationDate;
|
||||
}
|
||||
|
||||
public void setExpirationDate(Date expirationDate) {
|
||||
this.expirationDate = expirationDate;
|
||||
}
|
||||
|
||||
public CredentialSubject getCredentialSubject() {
|
||||
return credentialSubject;
|
||||
}
|
||||
|
||||
public void setCredentialSubject(CredentialSubject credentialSubject) {
|
||||
this.credentialSubject = credentialSubject;
|
||||
return this;
|
||||
}
|
||||
|
||||
public URI getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(URI id) {
|
||||
public VerifiableCredential setId(URI id) {
|
||||
this.id = id;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Date getExpirationDate() {
|
||||
return expirationDate;
|
||||
}
|
||||
|
||||
public VerifiableCredential setExpirationDate(Date expirationDate) {
|
||||
this.expirationDate = expirationDate;
|
||||
return this;
|
||||
}
|
||||
|
||||
public CredentialSubject getCredentialSubject() {
|
||||
return credentialSubject;
|
||||
}
|
||||
|
||||
public VerifiableCredential setCredentialSubject(CredentialSubject credentialSubject) {
|
||||
this.credentialSubject = credentialSubject;
|
||||
return this;
|
||||
}
|
||||
|
||||
public VerifiableCredential setAdditionalProperties(Map<String, Object> additionalProperties) {
|
||||
this.additionalProperties = additionalProperties;
|
||||
return this;
|
||||
}
|
||||
}
|
|
@ -33,6 +33,8 @@ import org.keycloak.models.KeycloakSession;
|
|||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.protocol.oidc.endpoints.AuthorizationEndpoint;
|
||||
import org.keycloak.protocol.oidc.endpoints.TokenEndpoint;
|
||||
import org.keycloak.protocol.oidc.grants.PreAuthorizedCodeGrantType;
|
||||
import org.keycloak.protocol.oidc.grants.PreAuthorizedCodeGrantTypeFactory;
|
||||
import org.keycloak.protocol.oidc.grants.ciba.CibaGrantType;
|
||||
import org.keycloak.protocol.oidc.grants.device.endpoints.DeviceEndpoint;
|
||||
import org.keycloak.protocol.oidc.par.endpoints.ParEndpoint;
|
||||
|
@ -82,9 +84,9 @@ public class OIDCWellKnownProvider implements WellKnownProvider {
|
|||
public static final List<String> DEFAULT_CLIENT_AUTH_SIGNING_ALG_VALUES_SUPPORTED = list(Algorithm.RS256.toString());
|
||||
|
||||
// The exact list depends on protocolMappers
|
||||
public static final List<String> DEFAULT_CLAIMS_SUPPORTED= list("aud", "sub", "iss", IDToken.AUTH_TIME, IDToken.NAME, IDToken.GIVEN_NAME, IDToken.FAMILY_NAME, IDToken.PREFERRED_USERNAME, IDToken.EMAIL, IDToken.ACR);
|
||||
public static final List<String> DEFAULT_CLAIMS_SUPPORTED = list("aud", "sub", "iss", IDToken.AUTH_TIME, IDToken.NAME, IDToken.GIVEN_NAME, IDToken.FAMILY_NAME, IDToken.PREFERRED_USERNAME, IDToken.EMAIL, IDToken.ACR);
|
||||
|
||||
public static final List<String> DEFAULT_CLAIM_TYPES_SUPPORTED= list("normal");
|
||||
public static final List<String> DEFAULT_CLAIM_TYPES_SUPPORTED = list("normal");
|
||||
|
||||
// KEYCLOAK-7451 OAuth Authorization Server Metadata for Proof Key for Code Exchange
|
||||
public static final List<String> DEFAULT_CODE_CHALLENGE_METHODS_SUPPORTED = list(OAuth2Constants.PKCE_METHOD_PLAIN, OAuth2Constants.PKCE_METHOD_S256);
|
||||
|
@ -108,6 +110,9 @@ public class OIDCWellKnownProvider implements WellKnownProvider {
|
|||
if (Profile.isFeatureEnabled(Profile.Feature.DEVICE_FLOW)) {
|
||||
DEFAULT_GRANT_TYPES_SUPPORTED.add(OAuth2Constants.DEVICE_CODE_GRANT_TYPE);
|
||||
}
|
||||
if (Profile.isFeatureEnabled(Profile.Feature.OID4VC_VCI)) {
|
||||
DEFAULT_GRANT_TYPES_SUPPORTED.add(PreAuthorizedCodeGrantTypeFactory.GRANT_TYPE);
|
||||
}
|
||||
|
||||
this.session = session;
|
||||
this.openidConfigOverride = openidConfigOverride;
|
||||
|
@ -137,7 +142,7 @@ public class OIDCWellKnownProvider implements WellKnownProvider {
|
|||
.build(realm.getName(), OIDCLoginProtocol.LOGIN_PROTOCOL).toString());
|
||||
}
|
||||
URI jwksUri = backendUriBuilder.clone().path(OIDCLoginProtocolService.class, "certs").build(realm.getName(),
|
||||
OIDCLoginProtocol.LOGIN_PROTOCOL);
|
||||
OIDCLoginProtocol.LOGIN_PROTOCOL);
|
||||
|
||||
// NOTE: Don't hardcode HTTPS checks here. JWKS URI is exposed just in the development/testing environment. For the production environment, the OIDCWellKnownProvider
|
||||
// is not exposed over "http" at all.
|
||||
|
@ -183,7 +188,7 @@ public class OIDCWellKnownProvider implements WellKnownProvider {
|
|||
.collect(Collectors.toList());
|
||||
if (!scopeNames.contains(OAuth2Constants.SCOPE_OPENID)) {
|
||||
scopeNames.add(0, OAuth2Constants.SCOPE_OPENID);
|
||||
}
|
||||
}
|
||||
config.setScopesSupported(scopeNames);
|
||||
}
|
||||
|
||||
|
@ -203,7 +208,7 @@ public class OIDCWellKnownProvider implements WellKnownProvider {
|
|||
}
|
||||
|
||||
URI revocationEndpoint = frontendUriBuilder.clone().path(OIDCLoginProtocolService.class, "revoke")
|
||||
.build(realm.getName(), OIDCLoginProtocol.LOGIN_PROTOCOL);
|
||||
.build(realm.getName(), OIDCLoginProtocol.LOGIN_PROTOCOL);
|
||||
|
||||
// NOTE: Don't hardcode HTTPS checks here. JWKS URI is exposed just in the development/testing environment. For the production environment, the OIDCWellKnownProvider
|
||||
// is not exposed over "http" at all.
|
||||
|
|
|
@ -17,14 +17,24 @@
|
|||
|
||||
package org.keycloak.protocol.oidc.endpoints;
|
||||
|
||||
import jakarta.ws.rs.Consumes;
|
||||
import jakarta.ws.rs.OPTIONS;
|
||||
import jakarta.ws.rs.POST;
|
||||
import jakarta.ws.rs.Path;
|
||||
import jakarta.ws.rs.core.HttpHeaders;
|
||||
import jakarta.ws.rs.core.MediaType;
|
||||
import jakarta.ws.rs.core.MultivaluedHashMap;
|
||||
import jakarta.ws.rs.core.MultivaluedMap;
|
||||
import jakarta.ws.rs.core.Response;
|
||||
import jakarta.ws.rs.core.Response.Status;
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.http.HttpRequest;
|
||||
import org.keycloak.http.HttpResponse;
|
||||
import org.keycloak.OAuth2Constants;
|
||||
import org.keycloak.OAuthErrorException;
|
||||
import org.keycloak.common.ClientConnection;
|
||||
import org.keycloak.events.Details;
|
||||
import org.keycloak.events.EventBuilder;
|
||||
import org.keycloak.http.HttpRequest;
|
||||
import org.keycloak.http.HttpResponse;
|
||||
import org.keycloak.models.AuthenticatedClientSessionModel;
|
||||
import org.keycloak.models.ClientModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
|
@ -33,6 +43,7 @@ import org.keycloak.protocol.oidc.OIDCAdvancedConfigWrapper;
|
|||
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
|
||||
import org.keycloak.protocol.oidc.TokenManager;
|
||||
import org.keycloak.protocol.oidc.grants.OAuth2GrantType;
|
||||
import org.keycloak.protocol.oidc.grants.PreAuthorizedCodeGrantTypeFactory;
|
||||
import org.keycloak.protocol.oidc.utils.AuthorizeClientUtil;
|
||||
import org.keycloak.protocol.saml.JaxrsSAML2BindingBuilder;
|
||||
import org.keycloak.protocol.saml.SamlClient;
|
||||
|
@ -48,20 +59,8 @@ import org.keycloak.services.cors.Cors;
|
|||
import org.w3c.dom.Document;
|
||||
import org.w3c.dom.Element;
|
||||
|
||||
import jakarta.ws.rs.Consumes;
|
||||
import jakarta.ws.rs.OPTIONS;
|
||||
import jakarta.ws.rs.POST;
|
||||
import jakarta.ws.rs.Path;
|
||||
import jakarta.ws.rs.core.HttpHeaders;
|
||||
import jakarta.ws.rs.core.MediaType;
|
||||
import jakarta.ws.rs.core.MultivaluedHashMap;
|
||||
import jakarta.ws.rs.core.MultivaluedMap;
|
||||
import jakarta.ws.rs.core.Response;
|
||||
import jakarta.ws.rs.core.Response.Status;
|
||||
import javax.xml.namespace.QName;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
|
@ -130,7 +129,9 @@ public class TokenEndpoint {
|
|||
checkRealm();
|
||||
checkGrantType();
|
||||
|
||||
if (!grantType.equals(OAuth2Constants.UMA_GRANT_TYPE)) {
|
||||
if (!grantType.equals(OAuth2Constants.UMA_GRANT_TYPE)
|
||||
// pre-authorized grants are not necessarily used by known clients.
|
||||
&& !grantType.equals(PreAuthorizedCodeGrantTypeFactory.GRANT_TYPE)) {
|
||||
checkClient();
|
||||
checkParameterDuplicated();
|
||||
}
|
||||
|
@ -202,7 +203,7 @@ public class TokenEndpoint {
|
|||
for (String key : formParams.keySet()) {
|
||||
if (formParams.get(key).size() != 1) {
|
||||
throw new CorsErrorResponseException(cors, OAuthErrorException.INVALID_REQUEST, "duplicated parameter",
|
||||
Response.Status.BAD_REQUEST);
|
||||
Response.Status.BAD_REQUEST);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,130 @@
|
|||
/*
|
||||
* 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.grants;
|
||||
|
||||
import jakarta.ws.rs.core.Response;
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.Config;
|
||||
import org.keycloak.OAuth2Constants;
|
||||
import org.keycloak.OAuthErrorException;
|
||||
import org.keycloak.common.Profile;
|
||||
import org.keycloak.common.util.SecretGenerator;
|
||||
import org.keycloak.events.Errors;
|
||||
import org.keycloak.events.EventType;
|
||||
import org.keycloak.models.AuthenticatedClientSessionModel;
|
||||
import org.keycloak.models.ClientSessionContext;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.protocol.oidc.utils.OAuth2Code;
|
||||
import org.keycloak.protocol.oidc.utils.OAuth2CodeParser;
|
||||
import org.keycloak.provider.EnvironmentDependentProviderFactory;
|
||||
import org.keycloak.representations.AccessToken;
|
||||
import org.keycloak.representations.AccessTokenResponse;
|
||||
import org.keycloak.services.CorsErrorResponseException;
|
||||
import org.keycloak.services.util.DefaultClientSessionContext;
|
||||
import org.keycloak.utils.MediaType;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
public class PreAuthorizedCodeGrantType extends OAuth2GrantTypeBase implements EnvironmentDependentProviderFactory {
|
||||
|
||||
private static final Logger LOGGER = Logger.getLogger(PreAuthorizedCodeGrantType.class);
|
||||
|
||||
@Override
|
||||
public Response process(Context context) {
|
||||
LOGGER.debug("Process grant request for preauthorized.");
|
||||
setContext(context);
|
||||
|
||||
String code = formParams.getFirst(OAuth2Constants.CODE);
|
||||
|
||||
if (code == null) {
|
||||
event.error(Errors.INVALID_CODE);
|
||||
throw new CorsErrorResponseException(cors, OAuthErrorException.INVALID_REQUEST,
|
||||
"Missing parameter: " + OAuth2Constants.CODE, Response.Status.BAD_REQUEST);
|
||||
}
|
||||
OAuth2CodeParser.ParseResult result = OAuth2CodeParser.parseCode(session, code, realm, event);
|
||||
if (result.isIllegalCode()) {
|
||||
event.error(Errors.INVALID_CODE);
|
||||
throw new CorsErrorResponseException(cors, OAuthErrorException.INVALID_GRANT, "Code not valid",
|
||||
Response.Status.BAD_REQUEST);
|
||||
}
|
||||
if (result.isExpiredCode()) {
|
||||
event.error(Errors.EXPIRED_CODE);
|
||||
throw new CorsErrorResponseException(cors, OAuthErrorException.INVALID_GRANT, "Code is expired",
|
||||
Response.Status.BAD_REQUEST);
|
||||
}
|
||||
AuthenticatedClientSessionModel clientSession = result.getClientSession();
|
||||
ClientSessionContext sessionContext = DefaultClientSessionContext.fromClientSessionAndScopeParameter(clientSession,
|
||||
OAuth2Constants.SCOPE_OPENID, session);
|
||||
|
||||
|
||||
// set the client as retrieved from the pre-authorized session
|
||||
session.getContext().setClient(result.getClientSession().getClient());
|
||||
|
||||
AccessToken accessToken = tokenManager.createClientAccessToken(session,
|
||||
clientSession.getRealm(),
|
||||
clientSession.getClient(),
|
||||
clientSession.getUserSession().getUser(),
|
||||
clientSession.getUserSession(),
|
||||
sessionContext);
|
||||
|
||||
AccessTokenResponse tokenResponse = tokenManager.responseBuilder(
|
||||
clientSession.getRealm(),
|
||||
clientSession.getClient(),
|
||||
event,
|
||||
session,
|
||||
clientSession.getUserSession(),
|
||||
sessionContext)
|
||||
.accessToken(accessToken).build();
|
||||
|
||||
event.success();
|
||||
|
||||
return cors.allowAllOrigins().builder(Response.ok(tokenResponse).type(MediaType.APPLICATION_JSON_TYPE)).build();
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public boolean isSupported(Config.Scope config) {
|
||||
return Profile.isFeatureEnabled(Profile.Feature.OID4VC_VCI);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isSupported() {
|
||||
return Profile.isFeatureEnabled(Profile.Feature.OID4VC_VCI);
|
||||
}
|
||||
|
||||
@Override
|
||||
public EventType getEventType() {
|
||||
return EventType.CODE_TO_TOKEN;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a pre-authorized Code for the given client session.
|
||||
*
|
||||
* @param session - keycloak session to be used
|
||||
* @param authenticatedClientSession - client session to be persisted
|
||||
* @param expirationTime - expiration time of the code, the code should be short-lived
|
||||
* @return the pre-authorized code
|
||||
*/
|
||||
public static String getPreAuthorizedCode(KeycloakSession session, AuthenticatedClientSessionModel authenticatedClientSession, int expirationTime) {
|
||||
String codeId = UUID.randomUUID().toString();
|
||||
String nonce = SecretGenerator.getInstance().randomString();
|
||||
OAuth2Code oAuth2Code = new OAuth2Code(codeId, expirationTime, nonce, null, null, null, null,
|
||||
authenticatedClientSession.getUserSession().getId());
|
||||
return OAuth2CodeParser.persistCode(session, authenticatedClientSession, oAuth2Code);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,55 @@
|
|||
/*
|
||||
* 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.grants;
|
||||
|
||||
import org.keycloak.Config;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.KeycloakSessionFactory;
|
||||
|
||||
/**
|
||||
* Factory for Pre-Authorized Code Grant
|
||||
*
|
||||
* @author <a href="https://github.com/wistefan">Stefan Wiedemann</a>
|
||||
*/
|
||||
public class PreAuthorizedCodeGrantTypeFactory implements OAuth2GrantTypeFactory {
|
||||
|
||||
public static final String GRANT_TYPE = "urn:ietf:params:oauth:grant-type:pre-authorized_code";
|
||||
|
||||
@Override
|
||||
public OAuth2GrantType create(KeycloakSession session) {
|
||||
return new PreAuthorizedCodeGrantType();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init(Config.Scope config) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void postInit(KeycloakSessionFactory factory) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return GRANT_TYPE;
|
||||
}
|
||||
|
||||
}
|
|
@ -89,8 +89,8 @@ public abstract class AbstractOIDCProtocolMapper implements ProtocolMapper {
|
|||
public AccessToken transformAccessToken(AccessToken token, ProtocolMapperModel mappingModel, KeycloakSession session,
|
||||
UserSessionModel userSession, ClientSessionContext clientSessionCtx) {
|
||||
boolean shouldUseLightweightToken = getShouldUseLightweightToken(session);
|
||||
boolean includeInAccessToken = shouldUseLightweightToken ? OIDCAttributeMapperHelper.includeInLightweightAccessToken(mappingModel) : OIDCAttributeMapperHelper.includeInAccessToken(mappingModel);
|
||||
if (!includeInAccessToken){
|
||||
boolean includeInAccessToken = shouldUseLightweightToken ? OIDCAttributeMapperHelper.includeInLightweightAccessToken(mappingModel) : OIDCAttributeMapperHelper.includeInAccessToken(mappingModel);
|
||||
if (!includeInAccessToken) {
|
||||
return token;
|
||||
}
|
||||
|
||||
|
@ -101,7 +101,7 @@ public abstract class AbstractOIDCProtocolMapper implements ProtocolMapper {
|
|||
public IDToken transformIDToken(IDToken token, ProtocolMapperModel mappingModel, KeycloakSession session,
|
||||
UserSessionModel userSession, ClientSessionContext clientSessionCtx) {
|
||||
|
||||
if (!OIDCAttributeMapperHelper.includeInIDToken(mappingModel)){
|
||||
if (!OIDCAttributeMapperHelper.includeInIDToken(mappingModel)) {
|
||||
return token;
|
||||
}
|
||||
|
||||
|
@ -122,9 +122,9 @@ public abstract class AbstractOIDCProtocolMapper implements ProtocolMapper {
|
|||
}
|
||||
|
||||
public AccessToken transformIntrospectionToken(AccessToken token, ProtocolMapperModel mappingModel, KeycloakSession session,
|
||||
UserSessionModel userSession, ClientSessionContext clientSessionCtx) {
|
||||
UserSessionModel userSession, ClientSessionContext clientSessionCtx) {
|
||||
|
||||
if (!OIDCAttributeMapperHelper.includeInIntrospection(mappingModel)){
|
||||
if (!OIDCAttributeMapperHelper.includeInIntrospection(mappingModel)) {
|
||||
return token;
|
||||
}
|
||||
|
||||
|
@ -134,10 +134,10 @@ public abstract class AbstractOIDCProtocolMapper implements ProtocolMapper {
|
|||
|
||||
/**
|
||||
* Intended to be overridden in {@link ProtocolMapper} implementations to add claims to an token.
|
||||
*
|
||||
* @param token
|
||||
* @param mappingModel
|
||||
* @param userSession
|
||||
*
|
||||
* @deprecated override {@link #setClaim(IDToken, ProtocolMapperModel, UserSessionModel, KeycloakSession, ClientSessionContext)} instead.
|
||||
*/
|
||||
@Deprecated
|
||||
|
@ -146,6 +146,7 @@ public abstract class AbstractOIDCProtocolMapper implements ProtocolMapper {
|
|||
|
||||
/**
|
||||
* Intended to be overridden in {@link ProtocolMapper} implementations to add claims to an token.
|
||||
*
|
||||
* @param token
|
||||
* @param mappingModel
|
||||
* @param userSession
|
||||
|
@ -160,6 +161,7 @@ public abstract class AbstractOIDCProtocolMapper implements ProtocolMapper {
|
|||
|
||||
/**
|
||||
* Intended to be overridden in {@link ProtocolMapper} implementations to add claims to an token.
|
||||
*
|
||||
* @param accessTokenResponse
|
||||
* @param mappingModel
|
||||
* @param userSession
|
||||
|
|
|
@ -17,4 +17,5 @@
|
|||
|
||||
org.keycloak.protocol.oidc.OIDCLoginProtocolFactory
|
||||
org.keycloak.protocol.saml.SamlProtocolFactory
|
||||
org.keycloak.protocol.docker.DockerAuthV2ProtocolFactory
|
||||
org.keycloak.protocol.docker.DockerAuthV2ProtocolFactory
|
||||
org.keycloak.protocol.oid4vc.OID4VCLoginProtocolFactory
|
|
@ -48,5 +48,11 @@ org.keycloak.protocol.oidc.mappers.ClaimsParameterTokenMapper
|
|||
org.keycloak.protocol.saml.mappers.UserAttributeNameIdMapper
|
||||
org.keycloak.protocol.oidc.mappers.ClaimsParameterWithValueIdTokenMapper
|
||||
org.keycloak.protocol.oidc.mappers.NonceBackwardsCompatibleMapper
|
||||
org.keycloak.protocol.oid4vc.issuance.mappers.OID4VCSubjectIdMapper
|
||||
org.keycloak.protocol.oid4vc.issuance.mappers.OID4VCStaticClaimMapper
|
||||
org.keycloak.protocol.oid4vc.issuance.mappers.OID4VCTargetRoleMapper
|
||||
org.keycloak.protocol.oid4vc.issuance.mappers.OID4VCUserAttributeMapper
|
||||
org.keycloak.protocol.oid4vc.issuance.mappers.OID4VCTypeMapper
|
||||
org.keycloak.protocol.oid4vc.issuance.mappers.OID4VCContextMapper
|
||||
org.keycloak.protocol.oidc.mappers.SessionStateMapper
|
||||
org.keycloak.protocol.oidc.mappers.SubMapper
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
#
|
||||
# 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.
|
||||
#
|
||||
|
||||
org.keycloak.protocol.oid4vc.issuance.signing.JwtSigningServiceProviderFactory
|
||||
org.keycloak.protocol.oid4vc.issuance.signing.SdJwtSigningServiceProviderFactory
|
|
@ -6,3 +6,4 @@ org.keycloak.protocol.oidc.grants.ResourceOwnerPasswordCredentialsGrantTypeFacto
|
|||
org.keycloak.protocol.oidc.grants.TokenExchangeGrantTypeFactory
|
||||
org.keycloak.protocol.oidc.grants.ciba.CibaGrantTypeFactory
|
||||
org.keycloak.protocol.oidc.grants.device.DeviceGrantTypeFactory
|
||||
org.keycloak.protocol.oidc.grants.PreAuthorizedCodeGrantTypeFactory
|
||||
|
|
|
@ -18,4 +18,5 @@
|
|||
org.keycloak.services.clientregistration.DefaultClientRegistrationProviderFactory
|
||||
org.keycloak.services.clientregistration.oidc.OIDCClientRegistrationProviderFactory
|
||||
org.keycloak.services.clientregistration.AdapterInstallationClientRegistrationProviderFactory
|
||||
org.keycloak.protocol.saml.clientregistration.EntityDescriptorClientRegistrationProviderFactory
|
||||
org.keycloak.protocol.saml.clientregistration.EntityDescriptorClientRegistrationProviderFactory
|
||||
org.keycloak.protocol.oid4vc.OID4VCClientRegistrationProviderFactory
|
|
@ -16,4 +16,5 @@
|
|||
#
|
||||
|
||||
org.keycloak.protocol.oidc.OIDCWellKnownProviderFactory
|
||||
org.keycloak.authorization.config.UmaWellKnownProviderFactory
|
||||
org.keycloak.authorization.config.UmaWellKnownProviderFactory
|
||||
org.keycloak.protocol.oid4vc.issuance.OID4VCIssuerWellKnownProviderFactory
|
|
@ -0,0 +1,132 @@
|
|||
/*
|
||||
* 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 org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.Parameterized;
|
||||
import org.keycloak.protocol.oid4vc.model.DisplayObject;
|
||||
import org.keycloak.protocol.oid4vc.model.Format;
|
||||
import org.keycloak.protocol.oid4vc.model.OID4VCClient;
|
||||
import org.keycloak.protocol.oid4vc.model.SupportedCredentialConfiguration;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
@RunWith(Parameterized.class)
|
||||
public class OID4VCClientRegistrationProviderTest {
|
||||
|
||||
@Parameterized.Parameters(name = "{0}")
|
||||
public static Collection<Object[]> parameters() {
|
||||
return Arrays.asList(new Object[][]{
|
||||
{
|
||||
"Single Supported Credential with format and single-type.",
|
||||
Map.of(
|
||||
"vc.credential-id.format", Format.JWT_VC.toString(),
|
||||
"vc.credential-id.scope", "VerifiableCredential"),
|
||||
new OID4VCClient(null, "did:web:test.org",
|
||||
List.of(new SupportedCredentialConfiguration()
|
||||
.setId("credential-id")
|
||||
.setFormat(Format.JWT_VC)
|
||||
.setScope("VerifiableCredential")),
|
||||
null, null)
|
||||
},
|
||||
{
|
||||
"Single Supported Credential with format and multi-type.",
|
||||
Map.of(
|
||||
"vc.credential-id.format", Format.JWT_VC.toString(),
|
||||
"vc.credential-id.scope", "AnotherCredential"),
|
||||
new OID4VCClient(null, "did:web:test.org",
|
||||
List.of(new SupportedCredentialConfiguration()
|
||||
.setId("credential-id")
|
||||
.setFormat(Format.JWT_VC)
|
||||
.setScope("AnotherCredential")),
|
||||
null, null)
|
||||
},
|
||||
{
|
||||
"Single Supported Credential with format, multi-type and a display object.",
|
||||
Map.of(
|
||||
"vc.credential-id.format", Format.JWT_VC.toString(),
|
||||
"vc.credential-id.scope", "AnotherCredential",
|
||||
"vc.credential-id.display.name", "Another",
|
||||
"vc.credential-id.display.locale", "en"),
|
||||
new OID4VCClient(null, "did:web:test.org",
|
||||
List.of(new SupportedCredentialConfiguration()
|
||||
.setId("credential-id")
|
||||
.setFormat(Format.JWT_VC)
|
||||
.setDisplay(new DisplayObject().setLocale("en").setName("Another"))
|
||||
.setScope("AnotherCredential")),
|
||||
null, null)
|
||||
},
|
||||
{
|
||||
"Multiple Supported Credentials.",
|
||||
Map.of(
|
||||
"vc.first-id.format", Format.JWT_VC.toString(),
|
||||
"vc.first-id.scope", "AnotherCredential",
|
||||
"vc.first-id.display.name", "First",
|
||||
"vc.first-id.display.locale", "en",
|
||||
"vc.second-id.format", Format.SD_JWT_VC.toString(),
|
||||
"vc.second-id.scope", "MyType",
|
||||
"vc.second-id.display.name", "Second Credential",
|
||||
"vc.second-id.display.locale", "de"),
|
||||
new OID4VCClient(null, "did:web:test.org",
|
||||
List.of(new SupportedCredentialConfiguration()
|
||||
.setId("first-id")
|
||||
.setFormat(Format.JWT_VC)
|
||||
.setDisplay(new DisplayObject().setLocale("en").setName("First"))
|
||||
.setScope("AnotherCredential"),
|
||||
new SupportedCredentialConfiguration()
|
||||
.setId("second-id")
|
||||
.setFormat(Format.SD_JWT_VC)
|
||||
.setDisplay(new DisplayObject().setLocale("de").setName("Second Credential"))
|
||||
.setScope("MyType")),
|
||||
null, null)
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private Map<String, String> clientAttributes;
|
||||
private OID4VCClient oid4VCClient;
|
||||
|
||||
public OID4VCClientRegistrationProviderTest(String name, Map<String, String> clientAttributes, OID4VCClient oid4VCClient) {
|
||||
this.clientAttributes = clientAttributes;
|
||||
this.oid4VCClient = oid4VCClient;
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testToClientRepresentation() {
|
||||
Map<String, String> translatedAttributes = OID4VCClientRegistrationProvider.toClientRepresentation(oid4VCClient).getAttributes();
|
||||
|
||||
assertEquals("The client should have been translated into the correct clientRepresentation.", clientAttributes.entrySet().size(), translatedAttributes.size());
|
||||
clientAttributes.forEach((key, value) ->
|
||||
assertEquals("The client should have been translated into the correct clientRepresentation.", clientAttributes.get(key), translatedAttributes.get(key)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFromClientAttributes() {
|
||||
assertEquals("The client should have been correctly build from the client representation",
|
||||
oid4VCClient,
|
||||
OID4VCClientRegistrationProvider.fromClientAttributes("did:web:test.org", clientAttributes));
|
||||
}
|
||||
|
||||
}
|
|
@ -17,13 +17,19 @@
|
|||
|
||||
package org.keycloak.testsuite.rest;
|
||||
|
||||
import static java.util.Objects.requireNonNull;
|
||||
|
||||
import jakarta.ws.rs.BadRequestException;
|
||||
import jakarta.ws.rs.Consumes;
|
||||
import jakarta.ws.rs.GET;
|
||||
import jakarta.ws.rs.NotFoundException;
|
||||
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.QueryParam;
|
||||
import jakarta.ws.rs.core.CacheControl;
|
||||
import jakarta.ws.rs.core.Response;
|
||||
import org.jboss.resteasy.reactive.NoCache;
|
||||
import org.keycloak.cookie.CookieProvider;
|
||||
import org.keycloak.cookie.CookieType;
|
||||
import org.keycloak.http.HttpRequest;
|
||||
import org.keycloak.Config;
|
||||
import org.keycloak.common.Profile;
|
||||
import org.keycloak.common.Profile.Feature;
|
||||
|
@ -32,6 +38,8 @@ import org.keycloak.common.profile.PropertiesProfileConfigResolver;
|
|||
import org.keycloak.common.util.HtmlUtils;
|
||||
import org.keycloak.common.util.Time;
|
||||
import org.keycloak.component.ComponentModel;
|
||||
import org.keycloak.cookie.CookieProvider;
|
||||
import org.keycloak.cookie.CookieType;
|
||||
import org.keycloak.events.Event;
|
||||
import org.keycloak.events.EventQuery;
|
||||
import org.keycloak.events.EventStoreProvider;
|
||||
|
@ -40,6 +48,8 @@ import org.keycloak.events.admin.AdminEvent;
|
|||
import org.keycloak.events.admin.AdminEventQuery;
|
||||
import org.keycloak.events.admin.AuthDetails;
|
||||
import org.keycloak.events.admin.OperationType;
|
||||
import org.keycloak.http.HttpRequest;
|
||||
import org.keycloak.models.AuthenticatedClientSessionModel;
|
||||
import org.keycloak.models.AuthenticationFlowModel;
|
||||
import org.keycloak.models.ClientModel;
|
||||
import org.keycloak.models.ClientScopeModel;
|
||||
|
@ -57,6 +67,7 @@ import org.keycloak.models.sessions.infinispan.changes.sessions.CrossDCLastSessi
|
|||
import org.keycloak.models.utils.ModelToRepresentation;
|
||||
import org.keycloak.models.utils.ResetTimeOffsetEvent;
|
||||
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
|
||||
import org.keycloak.protocol.oidc.grants.PreAuthorizedCodeGrantType;
|
||||
import org.keycloak.protocol.oidc.mappers.AudienceProtocolMapper;
|
||||
import org.keycloak.provider.Provider;
|
||||
import org.keycloak.provider.ProviderFactory;
|
||||
|
@ -96,17 +107,6 @@ import org.keycloak.truststore.TruststoreProvider;
|
|||
import org.keycloak.util.JsonSerialization;
|
||||
import org.keycloak.utils.MediaType;
|
||||
|
||||
import jakarta.ws.rs.BadRequestException;
|
||||
import jakarta.ws.rs.Consumes;
|
||||
import jakarta.ws.rs.GET;
|
||||
import jakarta.ws.rs.NotFoundException;
|
||||
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.QueryParam;
|
||||
import jakarta.ws.rs.core.Response;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
|
@ -124,10 +124,12 @@ import java.util.List;
|
|||
import java.util.Map;
|
||||
import java.util.Properties;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
import java.util.UUID;
|
||||
|
||||
import static java.util.Objects.requireNonNull;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
||||
|
@ -452,14 +454,14 @@ public class TestingResourceProvider implements RealmResourceProvider {
|
|||
|
||||
/**
|
||||
* Get admin events
|
||||
*
|
||||
* <p>
|
||||
* Returns all admin events, or filters events based on URL query parameters listed here
|
||||
*
|
||||
* @param realmId
|
||||
* @param operationTypes
|
||||
* @param authRealm
|
||||
* @param authClient
|
||||
* @param authUser user id
|
||||
* @param authUser user id
|
||||
* @param authIpAddress
|
||||
* @param resourcePath
|
||||
* @param dateFrom
|
||||
|
@ -473,10 +475,10 @@ public class TestingResourceProvider implements RealmResourceProvider {
|
|||
@NoCache
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public Stream<AdminEventRepresentation> getAdminEvents(@QueryParam("realmId") String realmId, @QueryParam("operationTypes") List<String> operationTypes, @QueryParam("authRealm") String authRealm, @QueryParam("authClient") String authClient,
|
||||
@QueryParam("authUser") String authUser, @QueryParam("authIpAddress") String authIpAddress,
|
||||
@QueryParam("resourcePath") String resourcePath, @QueryParam("dateFrom") String dateFrom,
|
||||
@QueryParam("dateTo") String dateTo, @QueryParam("first") Integer firstResult,
|
||||
@QueryParam("max") Integer maxResults) {
|
||||
@QueryParam("authUser") String authUser, @QueryParam("authIpAddress") String authIpAddress,
|
||||
@QueryParam("resourcePath") String resourcePath, @QueryParam("dateFrom") String dateFrom,
|
||||
@QueryParam("dateTo") String dateTo, @QueryParam("first") Integer firstResult,
|
||||
@QueryParam("max") Integer maxResults) {
|
||||
|
||||
EventStoreProvider eventStore = session.getProvider(EventStoreProvider.class);
|
||||
AdminEventQuery query = eventStore.createAdminQuery();
|
||||
|
@ -682,7 +684,7 @@ public class TestingResourceProvider implements RealmResourceProvider {
|
|||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public UserRepresentation getUserByServiceAccountClient(@QueryParam("realmName") String realmName, @QueryParam("clientId") String clientId) {
|
||||
RealmModel realm = getRealmByName(realmName);
|
||||
ClientModel client = realm.getClientByClientId(clientId);
|
||||
ClientModel client = realm.getClientByClientId(clientId);
|
||||
UserModel user = session.users().getServiceAccount(client);
|
||||
if (user == null) return null;
|
||||
return ModelToRepresentation.toRepresentation(session, realm, user);
|
||||
|
@ -702,10 +704,10 @@ public class TestingResourceProvider implements RealmResourceProvider {
|
|||
.collect(Collectors.toMap(ComponentModel::getName,
|
||||
componentModel -> {
|
||||
ProviderFactory<TestProvider> f = session.getKeycloakSessionFactory()
|
||||
.getProviderFactory(TestProvider.class, componentModel.getProviderId());
|
||||
TestProviderFactory factory = (TestProviderFactory) f;
|
||||
TestProvider p = (TestProvider) factory.create(session, componentModel);
|
||||
return p.getDetails();
|
||||
.getProviderFactory(TestProvider.class, componentModel.getProviderId());
|
||||
TestProviderFactory factory = (TestProviderFactory) f;
|
||||
TestProvider p = (TestProvider) factory.create(session, componentModel);
|
||||
return p.getDetails();
|
||||
}));
|
||||
}
|
||||
|
||||
|
@ -716,11 +718,11 @@ public class TestingResourceProvider implements RealmResourceProvider {
|
|||
RealmModel realm = session.getContext().getRealm();
|
||||
return realm.getComponentsStream(realm.getId(), TestAmphibianProvider.class.getName())
|
||||
.collect(Collectors.toMap(
|
||||
ComponentModel::getName,
|
||||
componentModel -> {
|
||||
TestAmphibianProvider t = session.getComponentProvider(TestAmphibianProvider.class, componentModel.getId());
|
||||
return t == null ? null : t.getDetails();
|
||||
}));
|
||||
ComponentModel::getName,
|
||||
componentModel -> {
|
||||
TestAmphibianProvider t = session.getComponentProvider(TestAmphibianProvider.class, componentModel.getId());
|
||||
return t == null ? null : t.getDetails();
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
|
@ -798,13 +800,13 @@ public class TestingResourceProvider implements RealmResourceProvider {
|
|||
}
|
||||
|
||||
ClientScopeModel clientScopeModel = realm.addClientScope(clientId);
|
||||
clientScopeModel.setProtocol(serviceClient.getProtocol()==null ? OIDCLoginProtocol.LOGIN_PROTOCOL : serviceClient.getProtocol());
|
||||
clientScopeModel.setProtocol(serviceClient.getProtocol() == null ? OIDCLoginProtocol.LOGIN_PROTOCOL : serviceClient.getProtocol());
|
||||
clientScopeModel.setDisplayOnConsentScreen(true);
|
||||
clientScopeModel.setConsentScreenText(clientId);
|
||||
clientScopeModel.setIncludeInTokenScope(true);
|
||||
|
||||
// Add audience protocol mapper
|
||||
ProtocolMapperModel audienceMapper = AudienceProtocolMapper.createClaimMapper("Audience for " + clientId, clientId, null,true, false, true );
|
||||
ProtocolMapperModel audienceMapper = AudienceProtocolMapper.createClaimMapper("Audience for " + clientId, clientId, null, true, false, true);
|
||||
clientScopeModel.addProtocolMapper(audienceMapper);
|
||||
|
||||
return clientScopeModel.getId();
|
||||
|
@ -1015,7 +1017,7 @@ public class TestingResourceProvider implements RealmResourceProvider {
|
|||
@Path("/reinitialize-provider-factory-with-system-properties-scope")
|
||||
@Consumes(MediaType.TEXT_HTML_UTF_8)
|
||||
public void reinitializeProviderFactoryWithSystemPropertiesScope(@QueryParam("provider-type") String providerType, @QueryParam("provider-id") String providerId,
|
||||
@QueryParam("system-properties-prefix") String systemPropertiesPrefix) throws Exception {
|
||||
@QueryParam("system-properties-prefix") String systemPropertiesPrefix) throws Exception {
|
||||
Class<? extends Provider> providerClass = (Class<? extends Provider>) Class.forName(providerType);
|
||||
ProviderFactory factory = session.getKeycloakSessionFactory().getProviderFactory(providerClass, providerId);
|
||||
factory.init(new Config.SystemPropertiesScope(systemPropertiesPrefix));
|
||||
|
@ -1024,10 +1026,10 @@ public class TestingResourceProvider implements RealmResourceProvider {
|
|||
/**
|
||||
* This will send POST request to specified URL with specified form parameters. It's not easily possible to "trick" web driver to send POST
|
||||
* request with custom parameters, which are not directly available in the form.
|
||||
*
|
||||
* <p>
|
||||
* See URLUtils.sendPOSTWithWebDriver for more details
|
||||
*
|
||||
* @param postRequestUrl Absolute URL. It can include query parameters etc. The POST request will be send to this URL
|
||||
* @param postRequestUrl Absolute URL. It can include query parameters etc. The POST request will be send to this URL
|
||||
* @param encodedFormParameters Encoded parameters in the form of "param1=value1¶m2=value2"
|
||||
* @return
|
||||
*/
|
||||
|
@ -1035,7 +1037,7 @@ public class TestingResourceProvider implements RealmResourceProvider {
|
|||
@Path("/simulate-post-request")
|
||||
@Produces(MediaType.TEXT_HTML_UTF_8)
|
||||
public Response simulatePostRequest(@QueryParam("postRequestUrl") String postRequestUrl,
|
||||
@QueryParam("encodedFormParameters") String encodedFormParameters) {
|
||||
@QueryParam("encodedFormParameters") String encodedFormParameters) {
|
||||
Map<String, String> params = new HashMap<>();
|
||||
|
||||
// Parse parameters to use in the POST request
|
||||
|
@ -1178,4 +1180,19 @@ public class TestingResourceProvider implements RealmResourceProvider {
|
|||
public Response getBlankPage() {
|
||||
return Response.ok("<html><body></body></html>").build();
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/pre-authorized-code")
|
||||
@NoCache
|
||||
public String getPreAuthorizedCode(@QueryParam("realm") final String realmName, @QueryParam("userSessionId") final String userSessionId, @QueryParam("clientId") final String clientId, @QueryParam("expiration") final int expiration) {
|
||||
RealmModel realm = getRealmByName(realmName);
|
||||
AuthenticatedClientSessionModel ascm = session.sessions()
|
||||
.getUserSession(realm, userSessionId)
|
||||
.getAuthenticatedClientSessions()
|
||||
.values()
|
||||
.stream().filter(acsm -> acsm.getClient().getClientId().equals(clientId))
|
||||
.findFirst()
|
||||
.orElseThrow(() -> new RuntimeException("No authenticatedClientSession found."));
|
||||
return PreAuthorizedCodeGrantType.getPreAuthorizedCode(session, ascm, expiration);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
|
||||
package org.keycloak.testsuite.runonserver;
|
||||
|
||||
import org.keycloak.common.VerificationException;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
|
||||
import java.io.IOException;
|
||||
|
@ -27,6 +28,6 @@ import java.io.Serializable;
|
|||
*/
|
||||
public interface RunOnServer extends Serializable {
|
||||
|
||||
void run(KeycloakSession session) throws IOException;
|
||||
void run(KeycloakSession session) throws IOException, VerificationException;
|
||||
|
||||
}
|
||||
|
|
|
@ -95,7 +95,7 @@ public interface TestingResource {
|
|||
|
||||
/**
|
||||
* Query events
|
||||
*
|
||||
* <p>
|
||||
* Returns all events, or filters them based on URL query parameters listed here
|
||||
*
|
||||
* @param realmId The realm
|
||||
|
@ -114,9 +114,9 @@ public interface TestingResource {
|
|||
@NoCache
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public List<EventRepresentation> queryEvents(@QueryParam("realmId") String realmId, @QueryParam("type") List<String> types, @QueryParam("client") String client,
|
||||
@QueryParam("user") String user, @QueryParam("dateFrom") String dateFrom, @QueryParam("dateTo") String dateTo,
|
||||
@QueryParam("ipAddress") String ipAddress, @QueryParam("first") Integer firstResult,
|
||||
@QueryParam("max") Integer maxResults);
|
||||
@QueryParam("user") String user, @QueryParam("dateFrom") String dateFrom, @QueryParam("dateTo") String dateTo,
|
||||
@QueryParam("ipAddress") String ipAddress, @QueryParam("first") Integer firstResult,
|
||||
@QueryParam("max") Integer maxResults);
|
||||
|
||||
@PUT
|
||||
@Path("/on-event")
|
||||
|
@ -142,7 +142,7 @@ public interface TestingResource {
|
|||
* @param operationTypes
|
||||
* @param authRealm
|
||||
* @param authClient
|
||||
* @param authUser user id
|
||||
* @param authUser user id
|
||||
* @param authIpAddress
|
||||
* @param resourcePath
|
||||
* @param dateFrom
|
||||
|
@ -156,10 +156,10 @@ public interface TestingResource {
|
|||
@NoCache
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public List<AdminEventRepresentation> getAdminEvents(@QueryParam("realmId") String realmId, @QueryParam("operationTypes") List<String> operationTypes, @QueryParam("authRealm") String authRealm, @QueryParam("authClient") String authClient,
|
||||
@QueryParam("authUser") String authUser, @QueryParam("authIpAddress") String authIpAddress,
|
||||
@QueryParam("resourcePath") String resourcePath, @QueryParam("dateFrom") String dateFrom,
|
||||
@QueryParam("dateTo") String dateTo, @QueryParam("first") Integer firstResult,
|
||||
@QueryParam("max") Integer maxResults);
|
||||
@QueryParam("authUser") String authUser, @QueryParam("authIpAddress") String authIpAddress,
|
||||
@QueryParam("resourcePath") String resourcePath, @QueryParam("dateFrom") String dateFrom,
|
||||
@QueryParam("dateTo") String dateTo, @QueryParam("first") Integer firstResult,
|
||||
@QueryParam("max") Integer maxResults);
|
||||
|
||||
@POST
|
||||
@Path("/on-admin-event")
|
||||
|
@ -369,12 +369,12 @@ public interface TestingResource {
|
|||
|
||||
/**
|
||||
* Re-initialize specified provider factory with system properties scope. This will allow to change providerConfig in runtime with {@link #setSystemPropertyOnServer}
|
||||
*
|
||||
* <p>
|
||||
* This works just for the provider factories, which can be re-initialized without any side-effects (EG. some functionality already dependent
|
||||
* on the previously initialized properties, which cannot be easily changed in runtime)
|
||||
*
|
||||
* @param providerType fully qualified class name of provider (subclass of org.keycloak.provider.Provider)
|
||||
* @param providerId provider Id
|
||||
* @param providerType fully qualified class name of provider (subclass of org.keycloak.provider.Provider)
|
||||
* @param providerId provider Id
|
||||
* @param systemPropertiesPrefix prefix to be used for system properties
|
||||
*/
|
||||
@GET
|
||||
|
@ -387,14 +387,14 @@ public interface TestingResource {
|
|||
|
||||
/**
|
||||
* This method is here just to have all endpoints from TestingResourceProvider available here.
|
||||
*
|
||||
* <p>
|
||||
* But usually it is requested to call this endpoint through WebDriver. See URLUtils.sendPOSTWithWebDriver for more details
|
||||
*/
|
||||
@GET
|
||||
@Path("/simulate-post-request")
|
||||
@Produces(MediaType.TEXT_HTML_UTF_8)
|
||||
Response simulatePostRequest(@QueryParam("postRequestUrl") String postRequestUrl,
|
||||
@QueryParam("encodedFormParameters") String encodedFormParameters);
|
||||
@QueryParam("encodedFormParameters") String encodedFormParameters);
|
||||
|
||||
/**
|
||||
* Display message to Error Page - for testing purposes
|
||||
|
@ -408,7 +408,7 @@ public interface TestingResource {
|
|||
|
||||
/**
|
||||
* @param providerClass Full name of class such as for example "org.keycloak.authentication.Authenticator"
|
||||
* @param providerId providerId referenced in particular provider factory. Can be null (in this case we're returning default provider for particular providerClass)
|
||||
* @param providerId providerId referenced in particular provider factory. Can be null (in this case we're returning default provider for particular providerClass)
|
||||
* @return fullname of provider implementation class
|
||||
*/
|
||||
@GET
|
||||
|
@ -426,6 +426,7 @@ public interface TestingResource {
|
|||
|
||||
/**
|
||||
* Temporarily changes the truststore SPI with another hostname verification policy. Call reenableTruststoreSpi to revert.
|
||||
*
|
||||
* @param hostnamePolicy The hostname verification policy to set
|
||||
*/
|
||||
@GET
|
||||
|
@ -444,7 +445,7 @@ public interface TestingResource {
|
|||
/**
|
||||
* Get count of tabs (child authentication sessions) for given "root authentication session"
|
||||
*
|
||||
* @param realm realm name (not ID)
|
||||
* @param realm realm name (not ID)
|
||||
* @param authSessionId ID of authentication session
|
||||
* @return count of tabs. Return 0 if authentication session of given ID does not exists (or if it exists, but without any authenticationSessions attached, which should not happen with normal usage)
|
||||
*/
|
||||
|
@ -456,4 +457,17 @@ public interface TestingResource {
|
|||
@GET
|
||||
@Path("/no-cache-annotated-endpoint")
|
||||
Response getNoCacheAnnotatedEndpointResponse(@QueryParam("programmatic_max_age_value") Long programmaticMaxAgeValue);
|
||||
|
||||
/**
|
||||
* Return a pre-authorized code for the current session.
|
||||
*
|
||||
* @param realmName name of the realm to be used
|
||||
* @param userSessionId id of the user session to get a code for
|
||||
* @param clientId id of the client to be used
|
||||
* @param expiration expiration time of the code
|
||||
* @return the code
|
||||
*/
|
||||
@GET
|
||||
@Path("/pre-authorized-code")
|
||||
String getPreAuthorizedCode(@QueryParam("realm") final String realmName, @QueryParam("userSessionId") final String userSessionId, @QueryParam("clientId") final String clientId, @QueryParam("expiration") final int expiration);
|
||||
}
|
||||
|
|
|
@ -18,6 +18,9 @@
|
|||
package org.keycloak.testsuite.util;
|
||||
|
||||
import com.google.common.base.Charsets;
|
||||
import jakarta.ws.rs.client.Entity;
|
||||
import jakarta.ws.rs.core.Form;
|
||||
import jakarta.ws.rs.core.UriBuilder;
|
||||
import org.apache.commons.io.IOUtils;
|
||||
import org.apache.commons.io.output.ByteArrayOutputStream;
|
||||
import org.apache.http.Header;
|
||||
|
@ -52,12 +55,13 @@ import org.keycloak.crypto.SignatureSignerContext;
|
|||
import org.keycloak.jose.jwk.JSONWebKeySet;
|
||||
import org.keycloak.jose.jwk.JWK;
|
||||
import org.keycloak.jose.jwk.JWKParser;
|
||||
import org.keycloak.jose.jws.JWSInput;
|
||||
import org.keycloak.jose.jwk.OKPPublicJWK;
|
||||
import org.keycloak.jose.jws.JWSInput;
|
||||
import org.keycloak.models.Constants;
|
||||
import org.keycloak.models.utils.KeycloakModelUtils;
|
||||
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
|
||||
import org.keycloak.protocol.oidc.OIDCLoginProtocolService;
|
||||
import org.keycloak.protocol.oidc.grants.PreAuthorizedCodeGrantTypeFactory;
|
||||
import org.keycloak.protocol.oidc.grants.ciba.CibaGrantType;
|
||||
import org.keycloak.protocol.oidc.grants.ciba.channel.AuthenticationChannelResponse;
|
||||
import org.keycloak.protocol.oidc.par.endpoints.ParEndpoint;
|
||||
|
@ -82,9 +86,6 @@ import org.openqa.selenium.By;
|
|||
import org.openqa.selenium.WebDriver;
|
||||
import org.openqa.selenium.WebElement;
|
||||
|
||||
import jakarta.ws.rs.client.Entity;
|
||||
import jakarta.ws.rs.core.Form;
|
||||
import jakarta.ws.rs.core.UriBuilder;
|
||||
import java.io.IOException;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.net.URI;
|
||||
|
@ -246,14 +247,14 @@ public class OAuthClient {
|
|||
|
||||
public LogoutUrlBuilder uiLocales(String uiLocales) {
|
||||
if (uiLocales != null) {
|
||||
b.queryParam(OIDCLoginProtocol.UI_LOCALES_PARAM, uiLocales);
|
||||
b.queryParam(OIDCLoginProtocol.UI_LOCALES_PARAM, uiLocales);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public LogoutUrlBuilder initiatingIdp(String initiatingIdp) {
|
||||
if (initiatingIdp != null) {
|
||||
b.queryParam(AuthenticationManager.INITIATING_IDP_PARAM, initiatingIdp);
|
||||
b.queryParam(AuthenticationManager.INITIATING_IDP_PARAM, initiatingIdp);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
@ -440,7 +441,7 @@ public class OAuthClient {
|
|||
}
|
||||
|
||||
public static CloseableHttpClient newCloseableHttpClientSSL(String keyStorePath,
|
||||
String keyStorePassword, String trustStorePath, String trustStorePassword) {
|
||||
String keyStorePassword, String trustStorePath, String trustStorePassword) {
|
||||
KeyStore keystore = null;
|
||||
// load the keystore containing the client certificate - keystore type is probably jks or pkcs12
|
||||
try {
|
||||
|
@ -478,7 +479,7 @@ public class OAuthClient {
|
|||
public AccessTokenResponse doAccessTokenRequest(String code, String password) {
|
||||
try (CloseableHttpClient client = httpClient.get()) {
|
||||
return doAccessTokenRequest(code, password, client);
|
||||
} catch (IOException ioe) {
|
||||
} catch (IOException ioe) {
|
||||
throw new RuntimeException(ioe);
|
||||
}
|
||||
}
|
||||
|
@ -537,7 +538,7 @@ public class OAuthClient {
|
|||
public String introspectTokenWithClientCredential(String clientId, String clientSecret, String tokenType, String tokenToIntrospect) {
|
||||
try (CloseableHttpClient client = HttpClientBuilder.create().build()) {
|
||||
return introspectTokenWithClientCredential(clientId, clientSecret, tokenType, tokenToIntrospect, client);
|
||||
} catch (IOException ioe) {
|
||||
} catch (IOException ioe) {
|
||||
throw new RuntimeException(ioe);
|
||||
}
|
||||
}
|
||||
|
@ -592,7 +593,7 @@ public class OAuthClient {
|
|||
return doGrantAccessTokenRequest(realm, username, password, null, clientId, clientSecret);
|
||||
}
|
||||
|
||||
public AccessTokenResponse doGrantAccessTokenRequest(String clientSecret, String username, String password, String otp) throws Exception {
|
||||
public AccessTokenResponse doGrantAccessTokenRequest(String clientSecret, String username, String password, String otp) throws Exception {
|
||||
return doGrantAccessTokenRequest(realm, username, password, otp, clientId, clientSecret);
|
||||
}
|
||||
|
||||
|
@ -740,7 +741,7 @@ public class OAuthClient {
|
|||
|
||||
}
|
||||
|
||||
UrlEncodedFormEntity formEntity;
|
||||
UrlEncodedFormEntity formEntity;
|
||||
try {
|
||||
formEntity = new UrlEncodedFormEntity(parameters, "UTF-8");
|
||||
} catch (UnsupportedEncodingException e) {
|
||||
|
@ -790,6 +791,27 @@ public class OAuthClient {
|
|||
}
|
||||
}
|
||||
|
||||
public AccessTokenResponse doPreauthorizedTokenRequest(String preAuthorizedCode) throws Exception {
|
||||
try (CloseableHttpClient client = httpClient.get()) {
|
||||
HttpPost post = new HttpPost(getAccessTokenUrl());
|
||||
|
||||
List<NameValuePair> parameters = new LinkedList<>();
|
||||
parameters.add(new BasicNameValuePair(OAuth2Constants.GRANT_TYPE, PreAuthorizedCodeGrantTypeFactory.GRANT_TYPE));
|
||||
parameters.add(new BasicNameValuePair("code", preAuthorizedCode));
|
||||
UrlEncodedFormEntity formEntity;
|
||||
try {
|
||||
formEntity = new UrlEncodedFormEntity(parameters, "UTF-8");
|
||||
} catch (UnsupportedEncodingException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
post.setEntity(formEntity);
|
||||
|
||||
return new AccessTokenResponse(client.execute(post));
|
||||
} catch (IOException ioe) {
|
||||
throw new RuntimeException(ioe);
|
||||
}
|
||||
}
|
||||
|
||||
public AuthenticationRequestAcknowledgement doBackchannelAuthenticationRequest(String clientId, String clientSecret, String userid, String bindingMessage, String acrValues) throws Exception {
|
||||
return doBackchannelAuthenticationRequest(clientId, clientSecret, userid, bindingMessage, acrValues, null, null);
|
||||
}
|
||||
|
@ -805,7 +827,8 @@ public class OAuthClient {
|
|||
if (userid != null) parameters.add(new BasicNameValuePair(LOGIN_HINT_PARAM, userid));
|
||||
if (bindingMessage != null) parameters.add(new BasicNameValuePair(BINDING_MESSAGE, bindingMessage));
|
||||
if (acrValues != null) parameters.add(new BasicNameValuePair(OAuth2Constants.ACR_VALUES, acrValues));
|
||||
if (clientNotificationToken != null) parameters.add(new BasicNameValuePair(CibaGrantType.CLIENT_NOTIFICATION_TOKEN, clientNotificationToken));
|
||||
if (clientNotificationToken != null)
|
||||
parameters.add(new BasicNameValuePair(CibaGrantType.CLIENT_NOTIFICATION_TOKEN, clientNotificationToken));
|
||||
if (scope != null) {
|
||||
parameters.add(new BasicNameValuePair(OAuth2Constants.SCOPE, OAuth2Constants.SCOPE_OPENID + " " + scope));
|
||||
} else {
|
||||
|
@ -909,7 +932,7 @@ public class OAuthClient {
|
|||
post.addHeader("Origin", origin);
|
||||
}
|
||||
|
||||
UrlEncodedFormEntity formEntity;
|
||||
UrlEncodedFormEntity formEntity;
|
||||
try {
|
||||
formEntity = new UrlEncodedFormEntity(parameters, "UTF-8");
|
||||
} catch (UnsupportedEncodingException e) {
|
||||
|
@ -955,7 +978,7 @@ public class OAuthClient {
|
|||
}
|
||||
|
||||
public CloseableHttpResponse doTokenRevoke(String token, String tokenTypeHint, String clientSecret,
|
||||
CloseableHttpClient client) throws IOException {
|
||||
CloseableHttpClient client) throws IOException {
|
||||
HttpPost post = new HttpPost(getTokenRevocationUrl());
|
||||
|
||||
List<NameValuePair> parameters = new LinkedList<>();
|
||||
|
@ -1167,11 +1190,13 @@ public class OAuthClient {
|
|||
}
|
||||
|
||||
public ParResponse doPushedAuthorizationRequest(String clientId, String clientSecret) throws IOException {
|
||||
return doPushedAuthorizationRequest(clientId, clientSecret, (CloseableHttpResponse c)->{}, null);
|
||||
return doPushedAuthorizationRequest(clientId, clientSecret, (CloseableHttpResponse c) -> {
|
||||
}, null);
|
||||
}
|
||||
|
||||
public ParResponse doPushedAuthorizationRequest(String clientId, String clientSecret, String signedJwt) throws IOException {
|
||||
return doPushedAuthorizationRequest(clientId, clientSecret, (CloseableHttpResponse c)->{}, signedJwt);
|
||||
return doPushedAuthorizationRequest(clientId, clientSecret, (CloseableHttpResponse c) -> {
|
||||
}, signedJwt);
|
||||
}
|
||||
|
||||
public ParResponse doPushedAuthorizationRequest(String clientId, String clientSecret, Consumer<CloseableHttpResponse> c) throws IOException {
|
||||
|
@ -1217,10 +1242,10 @@ public class OAuthClient {
|
|||
//if (state != null) {
|
||||
// parameters.add(new BasicNameValuePair(OAuth2Constants.STATE, state));
|
||||
//}
|
||||
if (uiLocales != null){
|
||||
if (uiLocales != null) {
|
||||
parameters.add(new BasicNameValuePair(OAuth2Constants.UI_LOCALES_PARAM, uiLocales));
|
||||
}
|
||||
if (nonce != null){
|
||||
if (nonce != null) {
|
||||
parameters.add(new BasicNameValuePair(OIDCLoginProtocol.NONCE_PARAM, nonce));
|
||||
}
|
||||
String scopeParam = openid ? TokenUtil.attachOIDCScope(scope) : scope;
|
||||
|
@ -1469,7 +1494,7 @@ public class OAuthClient {
|
|||
if (idTokenHint != null) {
|
||||
b.queryParam(OAuth2Constants.ID_TOKEN_HINT, idTokenHint);
|
||||
}
|
||||
if(initiatingIDP != null) {
|
||||
if (initiatingIDP != null) {
|
||||
b.queryParam(AuthenticationManager.INITIATING_IDP_PARAM, initiatingIDP);
|
||||
}
|
||||
driver.navigate().to(b.build(realm).toString());
|
||||
|
@ -1521,10 +1546,10 @@ public class OAuthClient {
|
|||
if (state != null) {
|
||||
b.queryParam(OAuth2Constants.STATE, state);
|
||||
}
|
||||
if (uiLocales != null){
|
||||
if (uiLocales != null) {
|
||||
b.queryParam(OAuth2Constants.UI_LOCALES_PARAM, uiLocales);
|
||||
}
|
||||
if (nonce != null){
|
||||
if (nonce != null) {
|
||||
b.queryParam(OIDCLoginProtocol.NONCE_PARAM, nonce);
|
||||
}
|
||||
|
||||
|
@ -1738,7 +1763,7 @@ public class OAuthClient {
|
|||
return this;
|
||||
}
|
||||
|
||||
public OAuthClient uiLocales(String uiLocales){
|
||||
public OAuthClient uiLocales(String uiLocales) {
|
||||
this.uiLocales = uiLocales;
|
||||
return this;
|
||||
}
|
||||
|
@ -1810,14 +1835,17 @@ public class OAuthClient {
|
|||
this.codeVerifier = codeVerifier;
|
||||
return this;
|
||||
}
|
||||
|
||||
public OAuthClient codeChallenge(String codeChallenge) {
|
||||
this.codeChallenge = codeChallenge;
|
||||
return this;
|
||||
}
|
||||
|
||||
public OAuthClient codeChallengeMethod(String codeChallengeMethod) {
|
||||
this.codeChallengeMethod = codeChallengeMethod;
|
||||
return this;
|
||||
}
|
||||
|
||||
public OAuthClient origin(String origin) {
|
||||
this.origin = origin;
|
||||
return this;
|
||||
|
@ -1880,7 +1908,7 @@ public class OAuthClient {
|
|||
} else {
|
||||
fragment = "fragment".equals(client.responseMode) || "fragment.jwt".equals(client.responseMode);
|
||||
}
|
||||
init (client, fragment);
|
||||
init(client, fragment);
|
||||
}
|
||||
|
||||
public AuthorizationEndpointResponse(OAuthClient client, boolean fragment) {
|
||||
|
@ -1947,6 +1975,7 @@ public class OAuthClient {
|
|||
public String getResponse() {
|
||||
return response;
|
||||
}
|
||||
|
||||
public String getIssuer() {
|
||||
return issuer;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,138 @@
|
|||
/*
|
||||
* 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.oauth;
|
||||
|
||||
import jakarta.ws.rs.core.UriBuilder;
|
||||
import org.apache.http.HttpStatus;
|
||||
import org.apache.http.NameValuePair;
|
||||
import org.apache.http.client.entity.UrlEncodedFormEntity;
|
||||
import org.apache.http.client.methods.HttpPost;
|
||||
import org.apache.http.impl.client.CloseableHttpClient;
|
||||
import org.apache.http.impl.client.HttpClientBuilder;
|
||||
import org.apache.http.message.BasicNameValuePair;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.keycloak.OAuth2Constants;
|
||||
import org.keycloak.common.Profile;
|
||||
import org.keycloak.common.util.Time;
|
||||
import org.keycloak.protocol.oidc.OIDCLoginProtocolService;
|
||||
import org.keycloak.protocol.oidc.grants.PreAuthorizedCodeGrantTypeFactory;
|
||||
import org.keycloak.representations.idm.RealmRepresentation;
|
||||
import org.keycloak.representations.idm.UserRepresentation;
|
||||
import org.keycloak.testsuite.AbstractTestRealmKeycloakTest;
|
||||
import org.keycloak.testsuite.arquillian.annotation.EnableFeature;
|
||||
import org.keycloak.testsuite.util.OAuthClient;
|
||||
import org.keycloak.testsuite.util.UserBuilder;
|
||||
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
@EnableFeature(value = Profile.Feature.OID4VC_VCI, skipRestart = true)
|
||||
public class PreAuthorizedGrantTest extends AbstractTestRealmKeycloakTest {
|
||||
|
||||
private CloseableHttpClient httpClient;
|
||||
|
||||
@Before
|
||||
public void setup() {
|
||||
httpClient = HttpClientBuilder.create().build();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPreAuthorizedGrant() throws Exception {
|
||||
String userSessionId = getUserSession();
|
||||
String preAuthorizedCode = getTestingClient().testing().getPreAuthorizedCode(TEST_REALM_NAME, userSessionId, "test-app", Time.currentTime() + 30);
|
||||
OAuthClient.AccessTokenResponse accessTokenResponse = postCode(preAuthorizedCode);
|
||||
|
||||
assertEquals("An access token should have successfully been returned.", HttpStatus.SC_OK, accessTokenResponse.getStatusCode());
|
||||
assertEquals("The correct session should have been used for the pre-authorized code.", userSessionId, accessTokenResponse.getSessionState());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPreAuthorizedGrantExpired() throws Exception {
|
||||
String userSessionId = getUserSession();
|
||||
String preAuthorizedCode = getTestingClient().testing().getPreAuthorizedCode(TEST_REALM_NAME, userSessionId, "test-app", Time.currentTime() - 30);
|
||||
OAuthClient.AccessTokenResponse accessTokenResponse = postCode(preAuthorizedCode);
|
||||
assertEquals("An expired code should not get an access token.", HttpStatus.SC_BAD_REQUEST, accessTokenResponse.getStatusCode());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPreAuthorizedGrantInvalidCode() throws Exception {
|
||||
// assure that a session exists.
|
||||
getUserSession();
|
||||
OAuthClient.AccessTokenResponse accessTokenResponse = postCode("invalid-code");
|
||||
assertEquals("An invalid code should not get an access token.", HttpStatus.SC_BAD_REQUEST, accessTokenResponse.getStatusCode());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPreAuthorizedGrantNoCode() throws Exception {
|
||||
// assure that a session exists.
|
||||
getUserSession();
|
||||
HttpPost post = new HttpPost(getTokenEndpoint());
|
||||
List<NameValuePair> parameters = new LinkedList<>();
|
||||
parameters.add(new BasicNameValuePair(OAuth2Constants.GRANT_TYPE, PreAuthorizedCodeGrantTypeFactory.GRANT_TYPE));
|
||||
UrlEncodedFormEntity formEntity = new UrlEncodedFormEntity(parameters, "UTF-8");
|
||||
post.setEntity(formEntity);
|
||||
|
||||
OAuthClient.AccessTokenResponse accessTokenResponse = new OAuthClient.AccessTokenResponse(httpClient.execute(post));
|
||||
assertEquals("If no code is provided, no access token should be returned.", HttpStatus.SC_BAD_REQUEST, accessTokenResponse.getStatusCode());
|
||||
}
|
||||
|
||||
private OAuthClient.AccessTokenResponse postCode(String preAuthorizedCode) throws Exception {
|
||||
HttpPost post = new HttpPost(getTokenEndpoint());
|
||||
List<NameValuePair> parameters = new LinkedList<>();
|
||||
parameters.add(new BasicNameValuePair(OAuth2Constants.GRANT_TYPE, PreAuthorizedCodeGrantTypeFactory.GRANT_TYPE));
|
||||
parameters.add(new BasicNameValuePair("code", preAuthorizedCode));
|
||||
UrlEncodedFormEntity formEntity = new UrlEncodedFormEntity(parameters, "UTF-8");
|
||||
post.setEntity(formEntity);
|
||||
|
||||
return new OAuthClient.AccessTokenResponse(httpClient.execute(post));
|
||||
}
|
||||
|
||||
private String getTokenEndpoint() {
|
||||
return OIDCLoginProtocolService
|
||||
.tokenUrl(UriBuilder.fromUri(suiteContext.getAuthServerInfo().getContextRoot().toString() + "/auth"))
|
||||
.build(TEST_REALM_NAME)
|
||||
.toString();
|
||||
}
|
||||
|
||||
private String getUserSession() {
|
||||
// create a session
|
||||
OAuthClient.AuthorizationEndpointResponse authorizationEndpointResponse = oauth.doLogin("john", "password");
|
||||
return authorizationEndpointResponse.getSessionState();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void configureTestRealm(RealmRepresentation testRealm) {
|
||||
UserRepresentation user = UserBuilder.create()
|
||||
.id("user-id")
|
||||
.username("john")
|
||||
.enabled(true)
|
||||
.email("john@email.cz")
|
||||
.emailVerified(true)
|
||||
.password("password").build();
|
||||
if (testRealm.getUsers() != null) {
|
||||
testRealm.getUsers().add(user);
|
||||
} else {
|
||||
testRealm.setUsers(List.of(user));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -24,7 +24,6 @@ import org.junit.Test;
|
|||
import org.keycloak.TokenVerifier;
|
||||
import org.keycloak.common.VerificationException;
|
||||
import org.keycloak.common.crypto.CryptoIntegration;
|
||||
import org.keycloak.common.util.KeyUtils;
|
||||
import org.keycloak.common.util.MultivaluedHashMap;
|
||||
import org.keycloak.crypto.Algorithm;
|
||||
import org.keycloak.crypto.AsymmetricSignatureVerifierContext;
|
||||
|
@ -53,7 +52,7 @@ import static org.junit.Assert.assertNotNull;
|
|||
import static org.junit.Assert.fail;
|
||||
|
||||
|
||||
public class JwtSigningServiceTest extends SigningServiceTest {
|
||||
public class JwtSigningServiceTest extends OID4VCTest {
|
||||
|
||||
private static final Logger LOGGER = Logger.getLogger(JwtSigningServiceTest.class);
|
||||
|
||||
|
@ -74,9 +73,9 @@ public class JwtSigningServiceTest extends SigningServiceTest {
|
|||
new JwtSigningService(
|
||||
session,
|
||||
getKeyFromSession(session).getKid(),
|
||||
"did:web:test.org",
|
||||
"JWT",
|
||||
"unsupported-algorithm",
|
||||
"JWT",
|
||||
"did:web:test.org",
|
||||
new StaticTimeProvider(1000)));
|
||||
} catch (RunOnServerException ros) {
|
||||
throw ros.getCause();
|
||||
|
|
|
@ -42,7 +42,7 @@ import java.util.UUID;
|
|||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
|
||||
public class LDSigningServiceTest extends SigningServiceTest {
|
||||
public class LDSigningServiceTest extends OID4VCTest {
|
||||
|
||||
@Before
|
||||
public void setup() {
|
||||
|
|
|
@ -0,0 +1,534 @@
|
|||
/*
|
||||
* 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 com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import jakarta.ws.rs.BadRequestException;
|
||||
import jakarta.ws.rs.core.Response;
|
||||
import org.apache.commons.io.IOUtils;
|
||||
import org.apache.http.HttpStatus;
|
||||
import org.apache.http.NameValuePair;
|
||||
import org.apache.http.client.entity.UrlEncodedFormEntity;
|
||||
import org.apache.http.client.methods.CloseableHttpResponse;
|
||||
import org.apache.http.client.methods.HttpGet;
|
||||
import org.apache.http.client.methods.HttpPost;
|
||||
import org.apache.http.entity.ContentType;
|
||||
import org.apache.http.entity.StringEntity;
|
||||
import org.apache.http.impl.client.CloseableHttpClient;
|
||||
import org.apache.http.impl.client.HttpClientBuilder;
|
||||
import org.apache.http.message.BasicNameValuePair;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.keycloak.OAuth2Constants;
|
||||
import org.keycloak.TokenVerifier;
|
||||
import org.keycloak.common.VerificationException;
|
||||
import org.keycloak.common.crypto.CryptoIntegration;
|
||||
import org.keycloak.common.util.MultivaluedHashMap;
|
||||
import org.keycloak.common.util.SecretGenerator;
|
||||
import org.keycloak.crypto.Algorithm;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.UserSessionModel;
|
||||
import org.keycloak.protocol.oid4vc.issuance.OID4VCIssuerEndpoint;
|
||||
import org.keycloak.protocol.oid4vc.issuance.TimeProvider;
|
||||
import org.keycloak.protocol.oid4vc.issuance.signing.JwtSigningService;
|
||||
import org.keycloak.protocol.oid4vc.model.CredentialIssuer;
|
||||
import org.keycloak.protocol.oid4vc.model.CredentialOfferURI;
|
||||
import org.keycloak.protocol.oid4vc.model.CredentialRequest;
|
||||
import org.keycloak.protocol.oid4vc.model.CredentialResponse;
|
||||
import org.keycloak.protocol.oid4vc.model.CredentialsOffer;
|
||||
import org.keycloak.protocol.oid4vc.model.Format;
|
||||
import org.keycloak.protocol.oid4vc.model.PreAuthorizedGrant;
|
||||
import org.keycloak.protocol.oid4vc.model.SupportedCredentialConfiguration;
|
||||
import org.keycloak.protocol.oid4vc.model.VerifiableCredential;
|
||||
import org.keycloak.protocol.oidc.grants.PreAuthorizedCodeGrantTypeFactory;
|
||||
import org.keycloak.protocol.oidc.representations.OIDCConfigurationRepresentation;
|
||||
import org.keycloak.representations.JsonWebToken;
|
||||
import org.keycloak.representations.idm.ClientRepresentation;
|
||||
import org.keycloak.representations.idm.RealmRepresentation;
|
||||
import org.keycloak.services.managers.AppAuthManager;
|
||||
import org.keycloak.services.managers.AuthenticationManager;
|
||||
import org.keycloak.testsuite.runonserver.RunOnServerException;
|
||||
import org.keycloak.testsuite.util.OAuthClient;
|
||||
import org.keycloak.util.JsonSerialization;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.junit.Assert.fail;
|
||||
|
||||
public class OID4VCIssuerEndpointTest extends OID4VCTest {
|
||||
|
||||
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
|
||||
private static final TimeProvider TIME_PROVIDER = new OID4VCTest.StaticTimeProvider(1000);
|
||||
private CloseableHttpClient httpClient;
|
||||
|
||||
|
||||
@Before
|
||||
public void setup() {
|
||||
CryptoIntegration.init(this.getClass().getClassLoader());
|
||||
httpClient = HttpClientBuilder.create().build();
|
||||
}
|
||||
|
||||
|
||||
// ----- getCredentialOfferUri
|
||||
|
||||
@Test(expected = BadRequestException.class)
|
||||
public void testGetCredentialOfferUriUnsupportedCredential() throws Throwable {
|
||||
String token = getBearerToken(oauth);
|
||||
withCausePropagation(() -> testingClient.server(TEST_REALM_NAME)
|
||||
.run((session -> {
|
||||
AppAuthManager.BearerTokenAuthenticator authenticator = new AppAuthManager.BearerTokenAuthenticator(session);
|
||||
authenticator.setTokenString(token);
|
||||
|
||||
OID4VCIssuerEndpoint oid4VCIssuerEndpoint = prepareIssuerEndpoint(session, authenticator);
|
||||
oid4VCIssuerEndpoint.getCredentialOfferURI("inexistent-id");
|
||||
})));
|
||||
|
||||
}
|
||||
|
||||
@Test(expected = BadRequestException.class)
|
||||
public void testGetCredentialOfferUriUnauthorized() throws Throwable {
|
||||
withCausePropagation(() -> testingClient.server(TEST_REALM_NAME)
|
||||
.run((session -> {
|
||||
AppAuthManager.BearerTokenAuthenticator authenticator = new AppAuthManager.BearerTokenAuthenticator(session);
|
||||
authenticator.setTokenString(null);
|
||||
OID4VCIssuerEndpoint oid4VCIssuerEndpoint = prepareIssuerEndpoint(session, authenticator);
|
||||
oid4VCIssuerEndpoint.getCredentialOfferURI("test-credential");
|
||||
})));
|
||||
}
|
||||
|
||||
@Test(expected = BadRequestException.class)
|
||||
public void testGetCredentialOfferUriInvalidToken() throws Throwable {
|
||||
withCausePropagation(() -> testingClient.server(TEST_REALM_NAME)
|
||||
.run((session -> {
|
||||
AppAuthManager.BearerTokenAuthenticator authenticator = new AppAuthManager.BearerTokenAuthenticator(session);
|
||||
authenticator.setTokenString("invalid-token");
|
||||
OID4VCIssuerEndpoint oid4VCIssuerEndpoint = prepareIssuerEndpoint(session, authenticator);
|
||||
oid4VCIssuerEndpoint.getCredentialOfferURI("test-credential");
|
||||
})));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetCredentialOfferURI() {
|
||||
String token = getBearerToken(oauth);
|
||||
testingClient
|
||||
.server(TEST_REALM_NAME)
|
||||
.run((session) -> {
|
||||
try {
|
||||
AppAuthManager.BearerTokenAuthenticator authenticator = new AppAuthManager.BearerTokenAuthenticator(session);
|
||||
authenticator.setTokenString(token);
|
||||
OID4VCIssuerEndpoint oid4VCIssuerEndpoint = prepareIssuerEndpoint(session, authenticator);
|
||||
|
||||
Response response = oid4VCIssuerEndpoint.getCredentialOfferURI("test-credential");
|
||||
|
||||
assertEquals("An offer uri should have been returned.", HttpStatus.SC_OK, response.getStatus());
|
||||
CredentialOfferURI credentialOfferURI = new ObjectMapper().convertValue(response.getEntity(), CredentialOfferURI.class);
|
||||
assertNotNull("A nonce should be included.", credentialOfferURI.getNonce());
|
||||
assertNotNull("The issuer uri should be provided.", credentialOfferURI.getIssuer());
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
private static String getBearerToken(OAuthClient oAuthClient) {
|
||||
OAuthClient.AuthorizationEndpointResponse authorizationEndpointResponse = oAuthClient.doLogin("john", "password");
|
||||
return oAuthClient.doAccessTokenRequest(authorizationEndpointResponse.getCode(), "password").getAccessToken();
|
||||
}
|
||||
|
||||
// ----- getCredentialOffer
|
||||
|
||||
@Test(expected = BadRequestException.class)
|
||||
public void testGetCredentialOfferUnauthorized() throws Throwable {
|
||||
withCausePropagation(() -> {
|
||||
testingClient
|
||||
.server(TEST_REALM_NAME)
|
||||
.run((session) -> {
|
||||
AppAuthManager.BearerTokenAuthenticator authenticator = new AppAuthManager.BearerTokenAuthenticator(session);
|
||||
authenticator.setTokenString(null);
|
||||
OID4VCIssuerEndpoint issuerEndpoint = prepareIssuerEndpoint(session, authenticator);
|
||||
issuerEndpoint.getCredentialOffer("nonce");
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@Test(expected = BadRequestException.class)
|
||||
public void testGetCredentialOfferWithoutNonce() throws Throwable {
|
||||
String token = getBearerToken(oauth);
|
||||
withCausePropagation(() -> {
|
||||
testingClient
|
||||
.server(TEST_REALM_NAME)
|
||||
.run((session -> {
|
||||
AppAuthManager.BearerTokenAuthenticator authenticator = new AppAuthManager.BearerTokenAuthenticator(session);
|
||||
authenticator.setTokenString(token);
|
||||
OID4VCIssuerEndpoint issuerEndpoint = prepareIssuerEndpoint(session, authenticator);
|
||||
issuerEndpoint.getCredentialOffer(null);
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
@Test(expected = BadRequestException.class)
|
||||
public void testGetCredentialOfferWithoutAPreparedOffer() throws Throwable {
|
||||
String token = getBearerToken(oauth);
|
||||
withCausePropagation(() -> {
|
||||
testingClient
|
||||
.server(TEST_REALM_NAME)
|
||||
.run((session -> {
|
||||
AppAuthManager.BearerTokenAuthenticator authenticator = new AppAuthManager.BearerTokenAuthenticator(session);
|
||||
authenticator.setTokenString(token);
|
||||
OID4VCIssuerEndpoint issuerEndpoint = prepareIssuerEndpoint(session, authenticator);
|
||||
issuerEndpoint.getCredentialOffer("unpreparedNonce");
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
@Test(expected = BadRequestException.class)
|
||||
public void testGetCredentialOfferWithABrokenNote() throws Throwable {
|
||||
String token = getBearerToken(oauth);
|
||||
withCausePropagation(() -> {
|
||||
testingClient
|
||||
.server(TEST_REALM_NAME)
|
||||
.run((session -> {
|
||||
AppAuthManager.BearerTokenAuthenticator authenticator = new AppAuthManager.BearerTokenAuthenticator(session);
|
||||
authenticator.setTokenString(token);
|
||||
String nonce = prepareNonce(authenticator, "invalidNote");
|
||||
OID4VCIssuerEndpoint issuerEndpoint = prepareIssuerEndpoint(session, authenticator);
|
||||
issuerEndpoint.getCredentialOffer(nonce);
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetCredentialOffer() {
|
||||
String token = getBearerToken(oauth);
|
||||
String rootURL = suiteContext.getAuthServerInfo().getContextRoot().toString();
|
||||
testingClient
|
||||
.server(TEST_REALM_NAME)
|
||||
.run((session) -> {
|
||||
AppAuthManager.BearerTokenAuthenticator authenticator = new AppAuthManager.BearerTokenAuthenticator(session);
|
||||
authenticator.setTokenString(token);
|
||||
|
||||
SupportedCredentialConfiguration supportedCredentialConfiguration = new SupportedCredentialConfiguration()
|
||||
.setId("test-credential")
|
||||
.setScope("VerifiableCredential")
|
||||
.setFormat(Format.JWT_VC);
|
||||
String nonce = prepareNonce(authenticator, OBJECT_MAPPER.writeValueAsString(supportedCredentialConfiguration));
|
||||
|
||||
OID4VCIssuerEndpoint issuerEndpoint = prepareIssuerEndpoint(session, authenticator);
|
||||
Response credentialOfferResponse = issuerEndpoint.getCredentialOffer(nonce);
|
||||
assertEquals("The offer should have been returned.", HttpStatus.SC_OK, credentialOfferResponse.getStatus());
|
||||
Object credentialOfferEntity = credentialOfferResponse.getEntity();
|
||||
assertNotNull("An actual offer should be in the response.", credentialOfferEntity);
|
||||
|
||||
CredentialsOffer credentialsOffer = OBJECT_MAPPER.convertValue(credentialOfferEntity, CredentialsOffer.class);
|
||||
assertNotNull("Credentials should have been offered.", credentialsOffer.getCredentialConfigurationIds());
|
||||
assertFalse("Credentials should have been offered.", credentialsOffer.getCredentialConfigurationIds().isEmpty());
|
||||
List<String> supportedCredentials = credentialsOffer.getCredentialConfigurationIds();
|
||||
assertEquals("Exactly one credential should have been returned.", 1, supportedCredentials.size());
|
||||
String offeredCredentialId = supportedCredentials.get(0);
|
||||
assertEquals("The credential should be as defined in the note.", supportedCredentialConfiguration.getId(), offeredCredentialId);
|
||||
|
||||
PreAuthorizedGrant grant = credentialsOffer.getGrants();
|
||||
assertNotNull("The grant should be included.", grant);
|
||||
assertNotNull("The grant should contain the pre-authorized code.", grant.getPreAuthorizedCode());
|
||||
assertNotNull("The actual pre-authorized code should be included.", grant
|
||||
.getPreAuthorizedCode()
|
||||
.getPreAuthorizedCode());
|
||||
|
||||
assertEquals("The correct issuer should be included.", rootURL + "/auth/realms/" + TEST_REALM_NAME, credentialsOffer.getCredentialIssuer());
|
||||
});
|
||||
}
|
||||
|
||||
// ----- requestCredential
|
||||
|
||||
@Test(expected = BadRequestException.class)
|
||||
public void testRequestCredentialUnauthorized() throws Throwable {
|
||||
withCausePropagation(() -> {
|
||||
testingClient
|
||||
.server(TEST_REALM_NAME)
|
||||
.run((session -> {
|
||||
AppAuthManager.BearerTokenAuthenticator authenticator = new AppAuthManager.BearerTokenAuthenticator(session);
|
||||
authenticator.setTokenString(null);
|
||||
OID4VCIssuerEndpoint issuerEndpoint = prepareIssuerEndpoint(session, authenticator);
|
||||
issuerEndpoint.requestCredential(new CredentialRequest()
|
||||
.setFormat(Format.JWT_VC)
|
||||
.setCredentialIdentifier("test-credential"));
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
@Test(expected = BadRequestException.class)
|
||||
public void testRequestCredentialInvalidToken() throws Throwable {
|
||||
withCausePropagation(() -> {
|
||||
testingClient
|
||||
.server(TEST_REALM_NAME)
|
||||
.run((session -> {
|
||||
AppAuthManager.BearerTokenAuthenticator authenticator = new AppAuthManager.BearerTokenAuthenticator(session);
|
||||
authenticator.setTokenString("token");
|
||||
OID4VCIssuerEndpoint issuerEndpoint = prepareIssuerEndpoint(session, authenticator);
|
||||
issuerEndpoint.requestCredential(new CredentialRequest()
|
||||
.setFormat(Format.JWT_VC)
|
||||
.setCredentialIdentifier("test-credential"));
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
@Test(expected = BadRequestException.class)
|
||||
public void testRequestCredentialUnsupportedFormat() throws Throwable {
|
||||
String token = getBearerToken(oauth);
|
||||
withCausePropagation(() -> {
|
||||
testingClient
|
||||
.server(TEST_REALM_NAME)
|
||||
.run((session -> {
|
||||
AppAuthManager.BearerTokenAuthenticator authenticator = new AppAuthManager.BearerTokenAuthenticator(session);
|
||||
authenticator.setTokenString(token);
|
||||
OID4VCIssuerEndpoint issuerEndpoint = prepareIssuerEndpoint(session, authenticator);
|
||||
issuerEndpoint.requestCredential(new CredentialRequest()
|
||||
.setFormat(Format.SD_JWT_VC)
|
||||
.setCredentialIdentifier("test-credential"));
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
@Test(expected = BadRequestException.class)
|
||||
public void testRequestCredentialUnsupportedCredential() throws Throwable {
|
||||
String token = getBearerToken(oauth);
|
||||
withCausePropagation(() -> {
|
||||
testingClient
|
||||
.server(TEST_REALM_NAME)
|
||||
.run((session -> {
|
||||
AppAuthManager.BearerTokenAuthenticator authenticator = new AppAuthManager.BearerTokenAuthenticator(session);
|
||||
authenticator.setTokenString(token);
|
||||
OID4VCIssuerEndpoint issuerEndpoint = prepareIssuerEndpoint(session, authenticator);
|
||||
issuerEndpoint.requestCredential(new CredentialRequest()
|
||||
.setFormat(Format.JWT_VC)
|
||||
.setCredentialIdentifier("no-such-credential"));
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRequestCredential() {
|
||||
String token = getBearerToken(oauth);
|
||||
testingClient
|
||||
.server(TEST_REALM_NAME)
|
||||
.run((session -> {
|
||||
AppAuthManager.BearerTokenAuthenticator authenticator = new AppAuthManager.BearerTokenAuthenticator(session);
|
||||
authenticator.setTokenString(token);
|
||||
OID4VCIssuerEndpoint issuerEndpoint = prepareIssuerEndpoint(session, authenticator);
|
||||
CredentialRequest credentialRequest = new CredentialRequest()
|
||||
.setFormat(Format.JWT_VC)
|
||||
.setCredentialIdentifier("test-credential");
|
||||
Response credentialResponse = issuerEndpoint.requestCredential(credentialRequest);
|
||||
assertEquals("The credential request should be answered successfully.", HttpStatus.SC_OK, credentialResponse.getStatus());
|
||||
assertNotNull("A credential should be responded.", credentialResponse.getEntity());
|
||||
CredentialResponse credentialResponseVO = OBJECT_MAPPER.convertValue(credentialResponse.getEntity(), CredentialResponse.class);
|
||||
JsonWebToken jsonWebToken = TokenVerifier.create((String) credentialResponseVO.getCredential(), JsonWebToken.class).getToken();
|
||||
// correct signing and contents are verified in the JwtSigningServiceTest, thus we only check that it is a JWT
|
||||
assertNotNull("A valid credential string should have been responded", jsonWebToken);
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
// Tests the complete flow from
|
||||
// 1. Retrieving the credential-offer-uri
|
||||
// 2. Using the uri to get the actual credential offer
|
||||
// 3. Get the issuer metadata
|
||||
// 4. Get the openid-configuration
|
||||
// 5. Get an access token for the pre-authorized code
|
||||
// 6. Get the credential
|
||||
@Test
|
||||
public void testCredentialIssuance() throws Exception {
|
||||
|
||||
String token = getBearerToken(oauth);
|
||||
|
||||
// 1. Retrieving the credential-offer-uri
|
||||
HttpGet getCredentialOfferURI = new HttpGet(getBasePath(TEST_REALM_NAME) + "credential-offer-uri?credential_configuration_id=test-credential");
|
||||
getCredentialOfferURI.addHeader("Authorization", "Bearer " + token);
|
||||
CloseableHttpResponse credentialOfferURIResponse = httpClient.execute(getCredentialOfferURI);
|
||||
|
||||
assertEquals("A valid offer uri should be returned", HttpStatus.SC_OK, credentialOfferURIResponse.getStatusLine().getStatusCode());
|
||||
String s = IOUtils.toString(credentialOfferURIResponse.getEntity().getContent(), StandardCharsets.UTF_8);
|
||||
CredentialOfferURI credentialOfferURI = JsonSerialization.readValue(s, CredentialOfferURI.class);
|
||||
|
||||
// 2. Using the uri to get the actual credential offer
|
||||
HttpGet getCredentialOffer = new HttpGet(credentialOfferURI.getIssuer() + "/" + credentialOfferURI.getNonce());
|
||||
getCredentialOffer.addHeader("Authorization", "Bearer " + token);
|
||||
CloseableHttpResponse credentialOfferResponse = httpClient.execute(getCredentialOffer);
|
||||
|
||||
assertEquals("A valid offer should be returned", HttpStatus.SC_OK, credentialOfferResponse.getStatusLine().getStatusCode());
|
||||
s = IOUtils.toString(credentialOfferResponse.getEntity().getContent(), StandardCharsets.UTF_8);
|
||||
CredentialsOffer credentialsOffer = JsonSerialization.readValue(s, CredentialsOffer.class);
|
||||
|
||||
// 3. Get the issuer metadata
|
||||
HttpGet getIssuerMetadata = new HttpGet(credentialsOffer.getCredentialIssuer() + "/.well-known/openid-credential-issuer");
|
||||
CloseableHttpResponse issuerMetadataResponse = httpClient.execute(getIssuerMetadata);
|
||||
assertEquals(HttpStatus.SC_OK, issuerMetadataResponse.getStatusLine().getStatusCode());
|
||||
s = IOUtils.toString(issuerMetadataResponse.getEntity().getContent(), StandardCharsets.UTF_8);
|
||||
CredentialIssuer credentialIssuer = JsonSerialization.readValue(s, CredentialIssuer.class);
|
||||
|
||||
assertEquals("We only expect one authorization server.", 1, credentialIssuer.getAuthorizationServers().size());
|
||||
|
||||
// 4. Get the openid-configuration
|
||||
HttpGet getOpenidConfiguration = new HttpGet(credentialIssuer.getAuthorizationServers().get(0) + "/.well-known/openid-configuration");
|
||||
CloseableHttpResponse openidConfigResponse = httpClient.execute(getOpenidConfiguration);
|
||||
assertEquals(HttpStatus.SC_OK, openidConfigResponse.getStatusLine().getStatusCode());
|
||||
s = IOUtils.toString(openidConfigResponse.getEntity().getContent(), StandardCharsets.UTF_8);
|
||||
OIDCConfigurationRepresentation openidConfig = JsonSerialization.readValue(s, OIDCConfigurationRepresentation.class);
|
||||
|
||||
assertNotNull("A token endpoint should be included.", openidConfig.getTokenEndpoint());
|
||||
assertTrue("The pre-authorized code should be supported.", openidConfig.getGrantTypesSupported().contains(PreAuthorizedCodeGrantTypeFactory.GRANT_TYPE));
|
||||
|
||||
// 5. Get an access token for the pre-authorized code
|
||||
HttpPost postPreAuthorizedCode = new HttpPost(openidConfig.getTokenEndpoint());
|
||||
List<NameValuePair> parameters = new LinkedList<>();
|
||||
parameters.add(new BasicNameValuePair(OAuth2Constants.GRANT_TYPE, PreAuthorizedCodeGrantTypeFactory.GRANT_TYPE));
|
||||
parameters.add(new BasicNameValuePair("code", credentialsOffer.getGrants().getPreAuthorizedCode().getPreAuthorizedCode()));
|
||||
UrlEncodedFormEntity formEntity = new UrlEncodedFormEntity(parameters, "UTF-8");
|
||||
postPreAuthorizedCode.setEntity(formEntity);
|
||||
OAuthClient.AccessTokenResponse accessTokenResponse = new OAuthClient.AccessTokenResponse(httpClient.execute(postPreAuthorizedCode));
|
||||
assertEquals(HttpStatus.SC_OK, accessTokenResponse.getStatusCode());
|
||||
String theToken = accessTokenResponse.getAccessToken();
|
||||
|
||||
// 6. Get the credential
|
||||
credentialsOffer.getCredentialConfigurationIds().stream()
|
||||
.map(offeredCredentialId -> credentialIssuer.getCredentialsSupported().get(offeredCredentialId))
|
||||
.forEach(supportedCredential -> {
|
||||
try {
|
||||
requestOffer(theToken, credentialIssuer.getCredentialEndpoint(), supportedCredential);
|
||||
} catch (IOException e) {
|
||||
fail("Was not able to get the credential.");
|
||||
} catch (VerificationException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
private static String prepareNonce(AppAuthManager.BearerTokenAuthenticator authenticator, String note) {
|
||||
String nonce = SecretGenerator.getInstance().randomString();
|
||||
AuthenticationManager.AuthResult authResult = authenticator.authenticate();
|
||||
UserSessionModel userSessionModel = authResult.getSession();
|
||||
userSessionModel.getAuthenticatedClientSessionByClient(authResult.getClient().getId()).setNote(nonce, note);
|
||||
return nonce;
|
||||
}
|
||||
|
||||
private static OID4VCIssuerEndpoint prepareIssuerEndpoint(KeycloakSession session, AppAuthManager.BearerTokenAuthenticator authenticator) {
|
||||
JwtSigningService jwtSigningService = new JwtSigningService(
|
||||
session,
|
||||
getKeyFromSession(session).getKid(),
|
||||
Algorithm.RS256,
|
||||
"JWT",
|
||||
"did:web:issuer.org",
|
||||
TIME_PROVIDER);
|
||||
return new OID4VCIssuerEndpoint(
|
||||
session,
|
||||
"did:web:issuer.org",
|
||||
Map.of(Format.JWT_VC, jwtSigningService),
|
||||
authenticator,
|
||||
new ObjectMapper(),
|
||||
TIME_PROVIDER,
|
||||
30);
|
||||
}
|
||||
|
||||
private String getBasePath(String realm) {
|
||||
return suiteContext.getAuthServerInfo().getContextRoot().toString() + "/auth/realms/" + realm + "/protocol/oid4vc/";
|
||||
}
|
||||
|
||||
private void requestOffer(String token, String credentialEndpoint, SupportedCredentialConfiguration offeredCredential) throws IOException, VerificationException {
|
||||
CredentialRequest request = new CredentialRequest();
|
||||
request.setFormat(offeredCredential.getFormat());
|
||||
request.setCredentialIdentifier(offeredCredential.getId());
|
||||
|
||||
StringEntity stringEntity = new StringEntity(OBJECT_MAPPER.writeValueAsString(request), ContentType.APPLICATION_JSON);
|
||||
|
||||
HttpPost postCredential = new HttpPost(credentialEndpoint);
|
||||
postCredential.addHeader("Authorization", "Bearer " + token);
|
||||
postCredential.setEntity(stringEntity);
|
||||
CloseableHttpResponse credentialRequestResponse = httpClient.execute(postCredential);
|
||||
assertEquals(HttpStatus.SC_OK, credentialRequestResponse.getStatusLine().getStatusCode());
|
||||
String s = IOUtils.toString(credentialRequestResponse.getEntity().getContent(), StandardCharsets.UTF_8);
|
||||
CredentialResponse credentialResponse = JsonSerialization.readValue(s, CredentialResponse.class);
|
||||
|
||||
assertNotNull("The credential should have been responded.", credentialResponse.getCredential());
|
||||
JsonWebToken jsonWebToken = TokenVerifier.create((String) credentialResponse.getCredential(), JsonWebToken.class).getToken();
|
||||
assertEquals("did:web:test.org", jsonWebToken.getIssuer());
|
||||
VerifiableCredential credential = new ObjectMapper().convertValue(jsonWebToken.getOtherClaims().get("vc"), VerifiableCredential.class);
|
||||
assertEquals(List.of("VerifiableCredential"), credential.getType());
|
||||
assertEquals(URI.create("did:web:test.org"), credential.getIssuer());
|
||||
assertEquals("john@email.cz", credential.getCredentialSubject().getClaims().get("email"));
|
||||
}
|
||||
|
||||
@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))
|
||||
)));
|
||||
}
|
||||
ClientRepresentation clientRepresentation = getTestClient("did:web:test.org");
|
||||
if (testRealm.getClients() != null) {
|
||||
testRealm.getClients().add(clientRepresentation);
|
||||
} else {
|
||||
testRealm.setClients(List.of(clientRepresentation));
|
||||
}
|
||||
if (testRealm.getRoles() != null) {
|
||||
testRealm.getRoles().getClient()
|
||||
.put(clientRepresentation.getClientId(), List.of(getRoleRepresentation("testRole", clientRepresentation.getClientId())));
|
||||
} else {
|
||||
testRealm.getRoles()
|
||||
.setClient(Map.of(clientRepresentation.getClientId(), List.of(getRoleRepresentation("testRole", clientRepresentation.getClientId()))));
|
||||
}
|
||||
if (testRealm.getUsers() != null) {
|
||||
testRealm.getUsers().add(getUserRepresentation(Map.of(clientRepresentation.getClientId(), List.of("testRole"))));
|
||||
} else {
|
||||
testRealm.setUsers(List.of(getUserRepresentation(Map.of(clientRepresentation.getClientId(), List.of("testRole")))));
|
||||
}
|
||||
if (testRealm.getAttributes() != null) {
|
||||
testRealm.getAttributes().put("issuerDid", TEST_DID.toString());
|
||||
} else {
|
||||
testRealm.setAttributes(Map.of("issuerDid", TEST_DID.toString()));
|
||||
}
|
||||
}
|
||||
|
||||
private void withCausePropagation(Runnable r) throws Throwable {
|
||||
try {
|
||||
r.run();
|
||||
} catch (Exception e) {
|
||||
if (e instanceof RunOnServerException) {
|
||||
throw e.getCause();
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,87 @@
|
|||
/*
|
||||
* 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 org.junit.Test;
|
||||
import org.keycloak.common.util.MultivaluedHashMap;
|
||||
import org.keycloak.protocol.oid4vc.issuance.OID4VCIssuerWellKnownProvider;
|
||||
import org.keycloak.protocol.oid4vc.model.CredentialIssuer;
|
||||
import org.keycloak.protocol.oid4vc.model.Format;
|
||||
import org.keycloak.representations.idm.ClientRepresentation;
|
||||
import org.keycloak.representations.idm.RealmRepresentation;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class OID4VCIssuerWellKnownProviderTest extends OID4VCTest {
|
||||
|
||||
@Test
|
||||
public void getConfig() {
|
||||
String expectedIssuer = suiteContext.getAuthServerInfo().getContextRoot().toString() + "/auth/realms/" + TEST_REALM_NAME;
|
||||
String expectedCredentialsEndpoint = expectedIssuer + "/protocol/oid4vc/credential";
|
||||
String expectedAuthorizationServer = expectedIssuer;
|
||||
testingClient
|
||||
.server(TEST_REALM_NAME)
|
||||
.run((session -> {
|
||||
OID4VCIssuerWellKnownProvider oid4VCIssuerWellKnownProvider = new OID4VCIssuerWellKnownProvider(session);
|
||||
Object issuerConfig = oid4VCIssuerWellKnownProvider.getConfig();
|
||||
assertTrue("Valid credential-issuer metadata should be returned.", issuerConfig instanceof CredentialIssuer);
|
||||
CredentialIssuer credentialIssuer = (CredentialIssuer) issuerConfig;
|
||||
assertEquals("The correct issuer should be included.", expectedIssuer, credentialIssuer.getCredentialIssuer());
|
||||
assertEquals("The correct credentials endpoint should be included.", expectedCredentialsEndpoint, credentialIssuer.getCredentialEndpoint());
|
||||
assertEquals("Since the authorization server is equal to the issuer, just 1 should be returned.", 1, credentialIssuer.getAuthorizationServers().size());
|
||||
assertEquals("The expected server should have been returned.", expectedAuthorizationServer, credentialIssuer.getAuthorizationServers().get(0));
|
||||
assertTrue("The test-credential should be supported.", credentialIssuer.getCredentialsSupported().containsKey("test-credential"));
|
||||
assertEquals("The test-credential should offer type VerifiableCredential", "VerifiableCredential", credentialIssuer.getCredentialsSupported().get("test-credential").getScope());
|
||||
assertEquals("The test-credential should be offered in the jwt-vc format.", Format.JWT_VC, credentialIssuer.getCredentialsSupported().get("test-credential").getFormat());
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
@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))
|
||||
)));
|
||||
}
|
||||
ClientRepresentation clientRepresentation = getTestClient("did:web:test.org");
|
||||
if (testRealm.getClients() != null) {
|
||||
testRealm.getClients().add(clientRepresentation);
|
||||
} else {
|
||||
testRealm.setClients(List.of(clientRepresentation));
|
||||
}
|
||||
if (testRealm.getUsers() != null) {
|
||||
testRealm.getUsers().add(getUserRepresentation(Map.of(clientRepresentation.getClientId(), List.of("testRole"))));
|
||||
} else {
|
||||
testRealm.setUsers(List.of(getUserRepresentation(Map.of(clientRepresentation.getClientId(), List.of("testRole")))));
|
||||
}
|
||||
if (testRealm.getAttributes() != null) {
|
||||
testRealm.getAttributes().put("issuerDid", TEST_DID.toString());
|
||||
} else {
|
||||
testRealm.setAttributes(Map.of("issuerDid", TEST_DID.toString()));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -19,6 +19,7 @@ package org.keycloak.testsuite.oid4vc.issuance.signing;
|
|||
|
||||
import org.bouncycastle.jce.provider.BouncyCastleProvider;
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.common.Profile;
|
||||
import org.keycloak.common.util.CertificateUtils;
|
||||
import org.keycloak.common.util.KeyUtils;
|
||||
import org.keycloak.common.util.MultivaluedHashMap;
|
||||
|
@ -26,11 +27,20 @@ import org.keycloak.common.util.PemUtils;
|
|||
import org.keycloak.crypto.KeyUse;
|
||||
import org.keycloak.crypto.KeyWrapper;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.utils.KeycloakModelUtils;
|
||||
import org.keycloak.protocol.oid4vc.OID4VCLoginProtocolFactory;
|
||||
import org.keycloak.protocol.oid4vc.issuance.TimeProvider;
|
||||
import org.keycloak.protocol.oid4vc.model.CredentialSubject;
|
||||
import org.keycloak.protocol.oid4vc.model.Format;
|
||||
import org.keycloak.protocol.oid4vc.model.VerifiableCredential;
|
||||
import org.keycloak.representations.idm.ClientRepresentation;
|
||||
import org.keycloak.representations.idm.ComponentExportRepresentation;
|
||||
import org.keycloak.representations.idm.ProtocolMapperRepresentation;
|
||||
import org.keycloak.representations.idm.RoleRepresentation;
|
||||
import org.keycloak.representations.idm.UserRepresentation;
|
||||
import org.keycloak.testsuite.AbstractTestRealmKeycloakTest;
|
||||
import org.keycloak.testsuite.arquillian.annotation.EnableFeature;
|
||||
import org.keycloak.testsuite.util.UserBuilder;
|
||||
|
||||
import java.net.URI;
|
||||
import java.security.KeyPair;
|
||||
|
@ -48,16 +58,20 @@ import java.util.Map;
|
|||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Super class for all signing service tests. Provides convenience methods to ease the testing.
|
||||
* Super class for all OID4VC tests. Provides convenience methods to ease the testing.
|
||||
*/
|
||||
public abstract class SigningServiceTest extends AbstractTestRealmKeycloakTest {
|
||||
@EnableFeature(value = Profile.Feature.OID4VC_VCI, skipRestart = false)
|
||||
public abstract class OID4VCTest extends AbstractTestRealmKeycloakTest {
|
||||
|
||||
private static final Logger LOGGER = Logger.getLogger(OID4VCTest.class);
|
||||
protected static final String CONTEXT_URL = "https://www.w3.org/2018/credentials/v1";
|
||||
protected static final URI TEST_DID = URI.create("did:web:test.org");
|
||||
protected static final List<String> TEST_TYPES = List.of("VerifiableCredential");
|
||||
protected static final Date TEST_EXPIRATION_DATE = Date.from(Instant.ofEpochSecond(2000));
|
||||
protected static final Date TEST_ISSUANCE_DATE = Date.from(Instant.ofEpochSecond(1000));
|
||||
|
||||
protected static final KeyWrapper RSA_KEY = getRsaKey();
|
||||
|
||||
protected static CredentialSubject getCredentialSubject(Map<String, Object> claims) {
|
||||
CredentialSubject credentialSubject = new CredentialSubject();
|
||||
claims.forEach(credentialSubject::setClaims);
|
||||
|
@ -138,13 +152,12 @@ public abstract class SigningServiceTest extends AbstractTestRealmKeycloakTest {
|
|||
}
|
||||
}
|
||||
|
||||
protected ComponentExportRepresentation getRsaKeyProvider(KeyWrapper keyWrapper) {
|
||||
public static ComponentExportRepresentation getRsaKeyProvider(KeyWrapper keyWrapper) {
|
||||
ComponentExportRepresentation componentExportRepresentation = new ComponentExportRepresentation();
|
||||
componentExportRepresentation.setName("rsa-key-provider");
|
||||
componentExportRepresentation.setId(UUID.randomUUID().toString());
|
||||
componentExportRepresentation.setProviderId("rsa");
|
||||
|
||||
|
||||
Certificate certificate = CertificateUtils.generateV1SelfSignedCertificate(
|
||||
new KeyPair((PublicKey) keyWrapper.getPublicKey(), (PrivateKey) keyWrapper.getPrivateKey()), "TestKey");
|
||||
|
||||
|
@ -161,21 +174,32 @@ public abstract class SigningServiceTest extends AbstractTestRealmKeycloakTest {
|
|||
return componentExportRepresentation;
|
||||
}
|
||||
|
||||
protected static KeyWrapper getKeyFromSession(KeycloakSession keycloakSession) {
|
||||
// we only set one key to the realm, thus can just take the first one
|
||||
// if run inside the testsuite, configure is called separated from the test itself, thus we cannot just take
|
||||
// the key from the `configureTestRealm` method.
|
||||
return keycloakSession
|
||||
.keys()
|
||||
.getKeysStream(keycloakSession.getContext().getRealm())
|
||||
.findFirst()
|
||||
.orElseThrow(() -> new RuntimeException("No key was configured"));
|
||||
public static ClientRepresentation getTestClient(String clientId) {
|
||||
ClientRepresentation clientRepresentation = new ClientRepresentation();
|
||||
clientRepresentation.setClientId(clientId);
|
||||
clientRepresentation.setProtocol(OID4VCLoginProtocolFactory.PROTOCOL_ID);
|
||||
clientRepresentation.setEnabled(true);
|
||||
clientRepresentation.setAttributes(Map.of(
|
||||
"vc.test-credential.expiry_in_s", "100",
|
||||
"vc.test-credential.format", Format.JWT_VC.toString(),
|
||||
"vc.test-credential.scope", "VerifiableCredential"));
|
||||
clientRepresentation.setProtocolMappers(
|
||||
List.of(
|
||||
getRoleMapper(clientId),
|
||||
getEmailMapper(),
|
||||
getIdMapper(),
|
||||
getStaticClaimMapper()
|
||||
)
|
||||
);
|
||||
return clientRepresentation;
|
||||
}
|
||||
|
||||
protected ComponentExportRepresentation getEdDSAKeyProvider() {
|
||||
ComponentExportRepresentation componentExportRepresentation = new ComponentExportRepresentation();
|
||||
componentExportRepresentation.setName("eddsa-generated");
|
||||
componentExportRepresentation.setId(UUID.randomUUID().toString());
|
||||
componentExportRepresentation.setName("eddsa-generated");
|
||||
componentExportRepresentation.setId(UUID.randomUUID().toString());
|
||||
componentExportRepresentation.setProviderId("eddsa-generated");
|
||||
|
||||
componentExportRepresentation.setConfig(new MultivaluedHashMap<>(
|
||||
|
@ -186,6 +210,123 @@ public abstract class SigningServiceTest extends AbstractTestRealmKeycloakTest {
|
|||
return componentExportRepresentation;
|
||||
}
|
||||
|
||||
public static ProtocolMapperRepresentation getRoleMapper(String clientId) {
|
||||
ProtocolMapperRepresentation protocolMapperRepresentation = new ProtocolMapperRepresentation();
|
||||
protocolMapperRepresentation.setName("role-mapper");
|
||||
protocolMapperRepresentation.setId(UUID.randomUUID().toString());
|
||||
protocolMapperRepresentation.setProtocol("oid4vc");
|
||||
protocolMapperRepresentation.setProtocolMapper("oid4vc-target-role-mapper");
|
||||
protocolMapperRepresentation.setConfig(
|
||||
Map.of(
|
||||
"subjectProperty", "roles",
|
||||
"clientId", clientId,
|
||||
"supportedCredentialTypes", "VerifiableCredential")
|
||||
);
|
||||
return protocolMapperRepresentation;
|
||||
}
|
||||
|
||||
public static ProtocolMapperRepresentation getEmailMapper() {
|
||||
ProtocolMapperRepresentation protocolMapperRepresentation = new ProtocolMapperRepresentation();
|
||||
protocolMapperRepresentation.setName("email-mapper");
|
||||
protocolMapperRepresentation.setProtocol("oid4vc");
|
||||
protocolMapperRepresentation.setId(UUID.randomUUID().toString());
|
||||
protocolMapperRepresentation.setProtocolMapper("oid4vc-user-attribute-mapper");
|
||||
protocolMapperRepresentation.setConfig(
|
||||
Map.of(
|
||||
"subjectProperty", "email",
|
||||
"userAttribute", "email",
|
||||
"supportedCredentialTypes", "VerifiableCredential")
|
||||
);
|
||||
return protocolMapperRepresentation;
|
||||
}
|
||||
|
||||
public static ProtocolMapperRepresentation getIdMapper() {
|
||||
ProtocolMapperRepresentation protocolMapperRepresentation = new ProtocolMapperRepresentation();
|
||||
protocolMapperRepresentation.setName("id-mapper");
|
||||
protocolMapperRepresentation.setProtocol("oid4vc");
|
||||
protocolMapperRepresentation.setId(UUID.randomUUID().toString());
|
||||
protocolMapperRepresentation.setProtocolMapper("oid4vc-subject-id-mapper");
|
||||
protocolMapperRepresentation.setConfig(
|
||||
Map.of(
|
||||
"supportedCredentialTypes", "VerifiableCredential")
|
||||
);
|
||||
return protocolMapperRepresentation;
|
||||
}
|
||||
|
||||
public static ProtocolMapperRepresentation getStaticClaimMapper() {
|
||||
ProtocolMapperRepresentation protocolMapperRepresentation = new ProtocolMapperRepresentation();
|
||||
protocolMapperRepresentation.setName("static-mapper");
|
||||
protocolMapperRepresentation.setProtocol("oid4vc");
|
||||
protocolMapperRepresentation.setId(UUID.randomUUID().toString());
|
||||
protocolMapperRepresentation.setProtocolMapper("oid4vc-static-claim-mapper");
|
||||
protocolMapperRepresentation.setConfig(
|
||||
Map.of(
|
||||
"subjectProperty", "static",
|
||||
"subjectValue", "Value",
|
||||
"supportedCredentialTypes", "VerifiableCredential")
|
||||
);
|
||||
return protocolMapperRepresentation;
|
||||
}
|
||||
|
||||
public static KeyWrapper getKeyFromSession(KeycloakSession keycloakSession) {
|
||||
// we only set one key to the realm, thus can just take the first one
|
||||
// if run inside the testsuite, configure is called separated from the test itself, thus we cannot just take
|
||||
// the key from the `configureTestRealm` method.
|
||||
KeyWrapper kw = keycloakSession
|
||||
.keys()
|
||||
.getKeysStream(keycloakSession.getContext().getRealm())
|
||||
.peek(k -> LOGGER.warnf("THE KEY: %s - %s", k.getKid(), k.getAlgorithm()))
|
||||
.findFirst()
|
||||
.orElseThrow(() -> new RuntimeException("No key was configured"));
|
||||
LOGGER.warnf("Kid is %s", kw.getKid());
|
||||
return kw;
|
||||
}
|
||||
|
||||
public static ComponentExportRepresentation getJwtSigningProvider(KeyWrapper keyWrapper) {
|
||||
ComponentExportRepresentation componentExportRepresentation = new ComponentExportRepresentation();
|
||||
componentExportRepresentation.setName("jwt-signing-service");
|
||||
componentExportRepresentation.setId(UUID.randomUUID().toString());
|
||||
componentExportRepresentation.setProviderId("jwt_vc");
|
||||
|
||||
componentExportRepresentation.setConfig(new MultivaluedHashMap<>(
|
||||
Map.of(
|
||||
"keyId", List.of(keyWrapper.getKid()),
|
||||
"algorithmType", List.of(keyWrapper.getAlgorithm()),
|
||||
"tokenType", List.of("JWT"),
|
||||
"issuerDid", List.of(TEST_DID.toString())
|
||||
)
|
||||
));
|
||||
return componentExportRepresentation;
|
||||
}
|
||||
|
||||
public static UserRepresentation getUserRepresentation(Map<String, List<String>> clientRoles) {
|
||||
UserBuilder userBuilder = UserBuilder.create()
|
||||
.id(KeycloakModelUtils.generateId())
|
||||
.username("john")
|
||||
.enabled(true)
|
||||
.email("john@email.cz")
|
||||
.emailVerified(true)
|
||||
.firstName("John")
|
||||
.lastName("Doe")
|
||||
.password("password")
|
||||
.role("account", "manage-account")
|
||||
.role("account", "view-profile");
|
||||
|
||||
clientRoles.entrySet().forEach(entry -> {
|
||||
entry.getValue().forEach(role -> userBuilder.role(entry.getKey(), role));
|
||||
});
|
||||
|
||||
return userBuilder.build();
|
||||
}
|
||||
|
||||
public static RoleRepresentation getRoleRepresentation(String roleName, String clientId) {
|
||||
|
||||
RoleRepresentation role = new RoleRepresentation();
|
||||
role.setName(roleName);
|
||||
role.setId(clientId);
|
||||
role.setClientRole(true);
|
||||
return role;
|
||||
}
|
||||
|
||||
static class StaticTimeProvider implements TimeProvider {
|
||||
private final int currentTimeInS;
|
|
@ -52,7 +52,7 @@ import static org.junit.Assert.assertEquals;
|
|||
import static org.junit.Assert.assertTrue;
|
||||
import static org.junit.Assert.fail;
|
||||
|
||||
public class SdJwtSigningServiceTest extends SigningServiceTest {
|
||||
public class SdJwtSigningServiceTest extends OID4VCTest {
|
||||
|
||||
private static ObjectMapper OBJECT_MAPPER = new ObjectMapper();
|
||||
private static KeyWrapper rsaKey = getRsaKey();
|
||||
|
|
Loading…
Reference in a new issue