[OID4VCI] Fix creation of clientScopes with protocol oid4vc (#39556)

closes #39527

Signed-off-by: Pascal Knüppel <pascal.knueppel@governikus.de>
This commit is contained in:
Pascal Knüppel
2025-06-05 08:49:05 +02:00
committed by GitHub
parent b03b9f9e3a
commit 17e2602a56
5 changed files with 75 additions and 16 deletions
@@ -361,7 +361,7 @@ public class DefaultExportImportManager implements ExportImportManager {
Map<String, ClientScopeModel> clientScopes = new HashMap<>();
if (rep.getClientScopes() != null) {
clientScopes = createClientScopes(session, rep.getClientScopes(), newRealm);
clientScopes = createClientScopes(rep.getClientScopes(), newRealm);
}
if (rep.getDefaultDefaultClientScopes() != null) {
for (String clientScopeName : rep.getDefaultDefaultClientScopes()) {
@@ -584,10 +584,10 @@ public class DefaultExportImportManager implements ExportImportManager {
return appMap;
}
private static Map<String, ClientScopeModel> createClientScopes(KeycloakSession session, List<ClientScopeRepresentation> clientScopes, RealmModel realm) {
private static Map<String, ClientScopeModel> createClientScopes(List<ClientScopeRepresentation> clientScopes, RealmModel realm) {
Map<String, ClientScopeModel> appMap = new HashMap<>();
for (ClientScopeRepresentation resourceRep : clientScopes) {
ClientScopeModel app = RepresentationToModel.createClientScope(session, realm, resourceRep);
ClientScopeModel app = RepresentationToModel.createClientScope(realm, resourceRep);
appMap.put(app.getName(), app);
}
return appMap;
@@ -702,7 +702,7 @@ public class RepresentationToModel {
// CLIENT SCOPES
public static ClientScopeModel createClientScope(KeycloakSession session, RealmModel realm, ClientScopeRepresentation resourceRep) {
public static ClientScopeModel createClientScope(RealmModel realm, ClientScopeRepresentation resourceRep) {
logger.debugv("Create client scope: {0}", resourceRep.getName());
ClientScopeModel clientScope = resourceRep.getId() != null ? realm.addClientScope(resourceRep.getId(), resourceRep.getName()) : realm.addClientScope(resourceRep.getName());
@@ -725,7 +725,6 @@ public class RepresentationToModel {
}
}
return clientScope;
}
@@ -31,12 +31,16 @@ import org.keycloak.events.admin.OperationType;
import org.keycloak.events.admin.ResourceType;
import org.keycloak.models.ClientScopeModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.models.ModelDuplicateException;
import org.keycloak.models.ModelException;
import org.keycloak.models.ModelIllegalStateException;
import org.keycloak.models.RealmModel;
import org.keycloak.models.utils.ModelToRepresentation;
import org.keycloak.models.utils.RepresentationToModel;
import org.keycloak.protocol.LoginProtocol;
import org.keycloak.protocol.LoginProtocolFactory;
import org.keycloak.protocol.oid4vc.OID4VCLoginProtocolFactory;
import org.keycloak.representations.idm.ClientScopeRepresentation;
import org.keycloak.saml.common.util.StringUtil;
import org.keycloak.services.ErrorResponse;
@@ -55,7 +59,9 @@ import jakarta.ws.rs.core.Response;
import java.util.Arrays;
import java.util.Optional;
import java.util.Set;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
/**
@@ -230,9 +236,21 @@ public class ClientScopeResource {
}
}
public static void validateClientScopeProtocol(String protocol)throws ErrorResponseException{
if(protocol==null || (!protocol.equals("openid-connect") && !protocol.equals("saml"))) throw ErrorResponse.error("Unexpected protocol",Response.Status.BAD_REQUEST);
public static void validateClientScopeProtocol(KeycloakSession session, String protocol)
throws ErrorResponseException {
KeycloakSessionFactory sessionFactory = session.getKeycloakSessionFactory();
Set<String> acceptedProtocols = sessionFactory.getProviderFactoriesStream(LoginProtocol.class)
.map(type -> (LoginProtocolFactory) type)
.map(LoginProtocolFactory::getId)
.collect(Collectors.toSet());
// the OID4VC protocol is not registered to prevent it from being displayed in the client-details ui
acceptedProtocols.add(OID4VCLoginProtocolFactory.PROTOCOL_ID);
if (protocol == null || !acceptedProtocols.contains(protocol)) {
throw ErrorResponse.error("Unexpected protocol", Response.Status.BAD_REQUEST);
}
}
/**
* Makes sure that an update that makes a Client Scope Dynamic is rejected if the Client Scope is assigned to a client
* as a default scope.
@@ -48,6 +48,7 @@ import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import java.util.stream.Stream;
/**
@@ -116,10 +117,10 @@ public class ClientScopesResource {
public Response createClientScope(ClientScopeRepresentation rep) {
auth.clients().requireManageClientScopes();
ClientScopeResource.validateClientScopeName(rep.getName());
ClientScopeResource.validateClientScopeProtocol(rep.getProtocol());
ClientScopeResource.validateClientScopeProtocol(session, rep.getProtocol());
ClientScopeResource.validateDynamicClientScope(rep);
try {
ClientScopeModel clientModel = RepresentationToModel.createClientScope(session, realm, rep);
ClientScopeModel clientModel = RepresentationToModel.createClientScope(realm, rep);
adminEvent.operation(OperationType.CREATE).resourcePath(session.getContext().getUri(), clientModel.getId()).representation(rep).success();
@@ -19,10 +19,14 @@ package org.keycloak.tests.admin.client;
import jakarta.ws.rs.ClientErrorException;
import jakarta.ws.rs.NotFoundException;
import jakarta.ws.rs.core.HttpHeaders;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.core.Response.Status;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import org.keycloak.admin.client.resource.ClientResource;
import org.keycloak.admin.client.resource.ProtocolMappersResource;
import org.keycloak.admin.client.resource.RealmResource;
@@ -52,6 +56,7 @@ import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
import java.util.stream.Collectors;
@@ -216,13 +221,6 @@ public class ClientScopeTest extends AbstractClientScopeTest {
Assertions.assertEquals("someValue", scopeRep.getAttributes().get("emptyAttr"));
}
@Test
public void testValidateClientScopeProtocol() {
org.keycloak.services.resources.admin.ClientScopeResource.validateClientScopeProtocol("saml");
org.keycloak.services.resources.admin.ClientScopeResource.validateClientScopeProtocol("openid-connect");
Assertions.assertThrows(RuntimeException.class, () -> org.keycloak.services.resources.admin.ClientScopeResource.validateClientScopeProtocol("other"));
}
@Test
public void testRenameScope() {
// Create two scopes
@@ -726,6 +724,49 @@ public class ClientScopeTest extends AbstractClientScopeTest {
}
}
@Test
public void createClientScopeWithoutProtocol() {
ClientScopeRepresentation clientScope = new ClientScopeRepresentation();
clientScope.setName("test-client-scope");
clientScope.setDescription("test-client-scope-description");
clientScope.setProtocol(null); // this should cause a BadRequestException
clientScope.setAttributes(Map.of("test-attribute", "test-value"));
try (Response response = clientScopes().create(clientScope)) {
Assertions.assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), response.getStatus());
String errorMessage = response.readEntity(String.class);
Assertions.assertTrue(errorMessage.contains("Unexpected protocol"));
}
}
@DisplayName("Create ClientScope with protocol:")
@ParameterizedTest
@ValueSource(strings = {"openid-connect", "saml", "oid4vc"})
public void createClientScopeWithOpenIdProtocol(String protocol) {
createClientScope(protocol);
}
private void createClientScope(String protocol) {
ClientScopeRepresentation clientScope = new ClientScopeRepresentation();
clientScope.setName("test-client-scope");
clientScope.setDescription("test-client-scope-description");
clientScope.setProtocol(protocol);
clientScope.setAttributes(Map.of("test-attribute", "test-value"));
String clientScopeId = null;
try (Response response = clientScopes().create(clientScope)) {
Assertions.assertEquals(Response.Status.CREATED.getStatusCode(), response.getStatus());
String location = (String) Optional.ofNullable(response.getHeaders().get(HttpHeaders.LOCATION))
.map(list -> list.get(0))
.orElse(null);
Assertions.assertNotNull(location);
clientScopeId = location.substring(location.lastIndexOf("/") + 1);
} finally {
// cleanup
clientScopes().get(clientScopeId).remove();
}
}
private void removeClientScopeMustFail(String clientScopeId) {
try {
clientScopes().get(clientScopeId).remove();