Thanks to visit codestin.com
Credit goes to Github.com

Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
import static io.restassured.RestAssured.given;
import static io.restassured.RestAssured.when;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.notNullValue;
import static org.junit.jupiter.api.Assertions.assertThrows;

@DistributionTest(keepAlive = true, requestPort = 9000, containerExposedPorts = {8080, 9000})
Expand Down Expand Up @@ -106,6 +107,21 @@ void testOpenApiFilter(KeycloakDistribution distribution) {
response
.body("components.schemas.OIDCClientRepresentation.properties.protocol.type", equalTo("string")) // the generated discriminator field
.body("paths.'/admin/api/{realmName}/clients/{version}'.get.responses.'200'.content.'application/json'.schema.type", equalTo("array"));

assertGenerateSecretEndpointPath(response);
}

private void assertGenerateSecretEndpointPath(ValidatableResponse response) {
String generateSecretPath = "paths.'/admin/api/{realmName}/clients/{version}/{id}/generate-secret'";

response
.body(generateSecretPath, notNullValue())
.body(generateSecretPath + ".post", notNullValue())
.body(generateSecretPath + ".post.summary", equalTo("Generates a new client secret"))
.body(generateSecretPath + ".post.responses.'200'", notNullValue())
.body(generateSecretPath + ".post.responses.'400'", notNullValue())
.body(generateSecretPath + ".post.responses.'403'", notNullValue())
.body(generateSecretPath + ".post.responses.'404'", notNullValue());
}

private void assertOpenAPISpecPolymorphicPaths(ValidatableResponse response, String schemaPath) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import org.keycloak.models.RealmModel;
import org.keycloak.models.RoleModel;
import org.keycloak.representations.admin.v2.BaseClientRepresentation;
import org.keycloak.representations.idm.ClientRepresentation;

/**
* @author Vaclav Muzikar <[email protected]>
Expand Down Expand Up @@ -76,4 +77,22 @@ protected ClientModel createClientModel(BaseClientRepresentation rep) {
@Override
public void close() {
}

@SuppressWarnings("unchecked")
public ClientRepresentation createOldClientRepresentation(BaseClientRepresentation client) {
var basicRep = new ClientRepresentation();
basicRep.setClientId(client.getClientId());
basicRep.setProtocol(client.getProtocol());
updateOldClientRepresentation((T) client, basicRep);
return basicRep;
}

/**
* Allows to populate the old client representation with some values.
* This should be kept to minimum as we only want to maintain relationship between
* the model and the new representation.
*/
protected void updateOldClientRepresentation(T client, ClientRepresentation oldRepresentation) {
// implemented by subclasses when needed
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,13 @@
import java.util.Set;
import java.util.stream.Collectors;

import org.keycloak.authentication.authenticators.client.ClientIdAndSecretAuthenticator;
import org.keycloak.models.ClientModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RoleModel;
import org.keycloak.representations.admin.v2.OIDCClientRepresentation;
import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.utils.StringUtil;

/**
* @author Vaclav Muzikar <[email protected]>
Expand Down Expand Up @@ -44,7 +47,12 @@ protected void toModelSpecific(OIDCClientRepresentation rep, ClientModel model)
if (rep.getAuth() != null) {
model.setPublicClient(false);
model.setClientAuthenticatorType(rep.getAuth().getMethod());
model.setSecret(rep.getAuth().getSecret());

// if the current model has client secret and the new representation doesn't, do not regenerate the secret
boolean canSetSecret = StringUtil.isNotBlank(rep.getAuth().getSecret()) || !isClientSecret(rep.getAuth().getMethod());
if (canSetSecret) {
model.setSecret(rep.getAuth().getSecret());
}
} else {
model.setPublicClient(true);
}
Expand Down Expand Up @@ -91,4 +99,19 @@ private Set<String> getServiceAccountRoles(ClientModel client) {
}
return Collections.emptySet();
}

@Override
protected void updateOldClientRepresentation(OIDCClientRepresentation client, ClientRepresentation oldRepresentation) {
var auth = client.getAuth();
if (auth != null && isClientSecret(auth.getMethod())) {
// this makes sure that client secret is generated for "create" methods if necessary
oldRepresentation.setPublicClient(false);
oldRepresentation.setClientAuthenticatorType(auth.getMethod());
oldRepresentation.setSecret(auth.getSecret());
}
}

private static boolean isClientSecret(String method) {
return ClientIdAndSecretAuthenticator.PROVIDER_ID.equals(method);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,12 @@
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.RoleModel;
import org.keycloak.models.mapper.BaseClientModelMapper;
import org.keycloak.models.mapper.ClientModelMapper;
import org.keycloak.models.utils.ModelToRepresentation;
import org.keycloak.representations.admin.v2.BaseClientRepresentation;
import org.keycloak.representations.admin.v2.OIDCClientRepresentation;
import org.keycloak.representations.admin.v2.validation.CreateClientDefault;
import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.representations.idm.RoleRepresentation;
import org.keycloak.services.ServiceException;
import org.keycloak.services.resources.admin.ClientResource;
Expand Down Expand Up @@ -89,9 +89,8 @@ public CreateOrUpdateResult createOrUpdate(RealmModel realm, BaseClientRepresent

// First, create a basic v1 representation to persist the client in the database.
// We can't use mapper.toModel(client) directly for creation because the "detached model"
var basicRep = new ClientRepresentation();
basicRep.setClientId(client.getClientId());
basicRep.setProtocol(client.getProtocol());
// TODO: avoid casting of the ClientModelMapper to BaseClientModelMapper
var basicRep = ((BaseClientModelMapper<?>) mapper).createOldClientRepresentation(client);

// Create the client in the database
model = clientsResource.createClientModel(basicRep);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,19 @@
import jakarta.ws.rs.DELETE;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.PATCH;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.PUT;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;

import org.keycloak.representations.admin.v2.BaseClientRepresentation;

import com.fasterxml.jackson.databind.JsonNode;
import org.eclipse.microprofile.openapi.annotations.Operation;
import org.eclipse.microprofile.openapi.annotations.responses.APIResponse;
import org.eclipse.microprofile.openapi.annotations.responses.APIResponses;

import static org.keycloak.admin.api.AdminApi.CONTENT_TYPE_MERGE_PATCH;

Expand Down Expand Up @@ -40,4 +45,15 @@ public interface ClientApi {
@Produces(MediaType.APPLICATION_JSON)
void deleteClient();

@Path("generate-secret")
@POST
@Produces(MediaType.TEXT_PLAIN)
@Operation(summary = "Generates a new client secret", description = "Generates a new client secret for clients using client-secret authentication method. Updates the client with this new secret and returns it.")
@APIResponses(value = {
@APIResponse(responseCode = "200", description = "Success - returns the newly generated secret"),
@APIResponse(responseCode = "400", description = "Bad Request - client authentication method is not 'client-secret'"),
@APIResponse(responseCode = "403", description = "Forbidden"),
@APIResponse(responseCode = "404", description = "Not Found - client does not exist")
})
String generateSecret();
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import jakarta.ws.rs.core.Response;

import org.keycloak.admin.api.AdminApi;
import org.keycloak.authentication.authenticators.client.ClientIdAndSecretAuthenticator;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.representations.admin.v2.BaseClientRepresentation;
Expand Down Expand Up @@ -110,6 +111,21 @@ public void deleteClient() {
clientResource.deleteClient();
}

@Override
public String generateSecret() {
if (clientResource == null) {
throw new NotFoundException("Cannot find the specified client");
}
var client = clientResource.getClient();
if (client.isPublicClient()) {
throw new WebApplicationException("Secret generation is not supported for public clients", Response.Status.BAD_REQUEST);
}
if (!ClientIdAndSecretAuthenticator.PROVIDER_ID.equals(client.getClientAuthenticatorType())) {
throw new WebApplicationException("Secret generation is only supported for authentication method '%s', but got '%s'".formatted(ClientIdAndSecretAuthenticator.PROVIDER_ID, client.getClientAuthenticatorType()), Response.Status.BAD_REQUEST);
}
return clientResource.regenerateSecret().getValue();
}

static void validateUnknownFields(BaseClientRepresentation rep) {
if (!rep.getAdditionalFields().keySet().isEmpty()) {
throw new WebApplicationException("Payload contains unknown fields: " + rep.getAdditionalFields().keySet(), Response.Status.BAD_REQUEST);
Expand Down
Loading
Loading