Introduce client secret rotation dynamic registration (#10952)

Closes #10609
This commit is contained in:
Marcelo Daniel Silva Sales 2022-03-28 20:39:11 +02:00 committed by GitHub
parent 6a657e6472
commit 091b1472ce
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 747 additions and 529 deletions

View file

@ -1,5 +1,6 @@
package org.keycloak.services.clientpolicy.executor;
import java.text.SimpleDateFormat;
import java.util.Objects;
import com.fasterxml.jackson.annotation.JsonProperty;
@ -15,6 +16,7 @@ import org.keycloak.services.clientpolicy.ClientPolicyContext;
import org.keycloak.services.clientpolicy.ClientPolicyException;
import org.keycloak.services.clientpolicy.context.ClientCRUDContext;
import org.keycloak.services.clientpolicy.context.ClientSecretRotationContext;
import org.keycloak.services.clientpolicy.context.DynamicClientUpdatedContext;
import static org.keycloak.services.clientpolicy.executor.ClientSecretRotationExecutorFactory.DEFAULT_SECRET_EXPIRATION_PERIOD;
import static org.keycloak.services.clientpolicy.executor.ClientSecretRotationExecutorFactory.DEFAULT_SECRET_REMAINING_ROTATION_PERIOD;
@ -49,7 +51,7 @@ public class ClientSecretRotationExecutor implements
public void executeOnEvent(ClientPolicyContext context) throws ClientPolicyException {
if (!session.getContext().getClient().isPublicClient() && !session.getContext().getClient()
.isBearerOnly()) {
session.setAttribute(ClientSecretConstants.CLIENT_SECRET_ROTATION_ENABLED,Boolean.TRUE);
session.setAttribute(ClientSecretConstants.CLIENT_SECRET_ROTATION_ENABLED, Boolean.TRUE);
switch (context.getEvent()) {
case REGISTERED:
case UPDATED:
@ -99,16 +101,29 @@ public class ClientSecretRotationExecutor implements
|| !clientConfigWrapper.hasClientSecretExpirationTime()) {
rotateSecret(adminContext, clientConfigWrapper);
} else {
//TODO validation for client dynamic registration
int secondsRemaining = clientConfigWrapper.getClientSecretExpirationTime()
- configuration.remainExpirationPeriod;
if (secondsRemaining <= configuration.remainExpirationPeriod) {
// rotateSecret(adminContext);
if (adminContext instanceof DynamicClientUpdatedContext) {
int startRemainingWindow = clientConfigWrapper.getClientSecretExpirationTime()
- configuration.remainExpirationPeriod;
debugDynamicInfo(clientConfigWrapper, startRemainingWindow);
if (Time.currentTime() >= startRemainingWindow) {
logger.debugv("Executing rotation for the dynamic client {0} due to remaining expiration time that starts at {1}", adminContext.getTargetClient().getClientId(), Time.toDate(startRemainingWindow));
rotateSecret(adminContext, clientConfigWrapper);
}
}
}
}
private void debugDynamicInfo(OIDCClientSecretConfigWrapper clientConfigWrapper, int startRemainingWindow) {
if (logger.isDebugEnabled()) {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss");
logger.debugv("client expiration time: {0}, remaining time: {1}, current time: {2}, Time offset: {3}", clientConfigWrapper.getClientSecretExpirationTime(), startRemainingWindow, Time.currentTime(), Time.getOffset());
logger.debugv("client expiration date: {0}, window remaining date: {1}, current date: {2}", sdf.format(Time.toDate(clientConfigWrapper.getClientSecretExpirationTime())), sdf.format(Time.toDate(startRemainingWindow)), sdf.format(Time.toDate(Time.currentTime())));
}
}
private void rotateSecret(ClientCRUDContext crudContext,
OIDCClientSecretConfigWrapper clientConfigWrapper) {
@ -120,10 +135,10 @@ public class ClientSecretRotationExecutor implements
updateClientConfigProperties(clientConfigWrapper);
}
} else if (!clientConfigWrapper.hasClientSecretExpirationTime()) {
logger.debugv("client {0} has no secret rotation expiration time configured",clientConfigWrapper.getId());
logger.debugv("client {0} has no secret rotation expiration time configured", clientConfigWrapper.getId());
updatedSecretExpiration(clientConfigWrapper);
} else {
logger.debugv("Execute typical secret rotation for client {0}",clientConfigWrapper.getId());
logger.debugv("Execute typical secret rotation for client {0}", clientConfigWrapper.getId());
updatedSecretExpiration(clientConfigWrapper);
updateRotateSecret(clientConfigWrapper, clientConfigWrapper.getSecret());
KeycloakModelUtils.generateSecret(crudContext.getTargetClient());
@ -135,7 +150,7 @@ public class ClientSecretRotationExecutor implements
crudContext.getProposedClientRepresentation());
}
logger.debugv("Client configured: {0}",clientConfigWrapper.toJson());
logger.debugv("Client configured: {0}", clientConfigWrapper.toJson());
}
private void updatedSecretExpiration(OIDCClientSecretConfigWrapper clientConfigWrapper) {

View file

@ -80,6 +80,7 @@ public abstract class AbstractClientRegistrationProvider implements ClientRegist
RepresentationToModel.createResourceServer(clientModel, session, true);
}
session.getContext().setClient(clientModel);
session.clientPolicy().triggerOnEvent(new DynamicClientRegisteredContext(context, clientModel, auth.getJwt(), realm));
ClientRegistrationPolicyManager.triggerAfterRegister(context, registrationAuth, clientModel);
@ -156,6 +157,8 @@ public abstract class AbstractClientRegistrationProvider implements ClientRegist
rep = ModelToRepresentation.toRepresentation(client, session);
rep.setSecret(client.getSecret());
Stream<String> defaultRolesNames = client.getDefaultRolesStream();
if (defaultRolesNames != null) {
rep.setDefaultRoles(defaultRolesNames.toArray(String[]::new));
@ -167,6 +170,7 @@ public abstract class AbstractClientRegistrationProvider implements ClientRegist
}
try {
session.getContext().setClient(client);
session.clientPolicy().triggerOnEvent(new DynamicClientUpdatedContext(session, client, auth.getJwt(), client.getRealm()));
} catch (ClientPolicyException cpe) {
throw new ErrorResponseException(cpe.getError(), cpe.getErrorDetail(), Response.Status.BAD_REQUEST);

View file

@ -317,7 +317,7 @@ public class DescriptionConverter {
if (client.getClientAuthenticatorType().equals(ClientIdAndSecretAuthenticator.PROVIDER_ID)) {
response.setClientSecret(client.getSecret());
response.setClientSecretExpiresAt(
OIDCClientSecretConfigWrapper.fromClientRepresentation(client).getClientSecretExpirationTime());
OIDCClientSecretConfigWrapper.fromClientRepresentation(client).getClientSecretExpirationTime());
}
response.setClientName(client.getName());

View file

@ -20,6 +20,7 @@ import org.jboss.logging.Logger;
import org.keycloak.OAuth2Constants;
import org.keycloak.common.util.Time;
import org.keycloak.models.ClientModel;
import org.keycloak.models.ClientSecretConstants;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.ProtocolMapperModel;
import org.keycloak.models.utils.ModelToRepresentation;
@ -129,6 +130,10 @@ public class OIDCClientRegistrationProvider extends AbstractClientRegistrationPr
updatePairwiseSubMappers(clientModel, SubjectType.parse(clientOIDC.getSubjectType()), clientOIDC.getSectorIdentifierUri());
updateClientRepWithProtocolMappers(clientModel, client);
client.setSecret(clientModel.getSecret());
client.getAttributes().put(ClientSecretConstants.CLIENT_SECRET_EXPIRATION,clientModel.getAttribute(ClientSecretConstants.CLIENT_SECRET_EXPIRATION));
client.getAttributes().put(ClientSecretConstants.CLIENT_SECRET_CREATION_TIME,clientModel.getAttribute(ClientSecretConstants.CLIENT_SECRET_CREATION_TIME));
validateClient(clientModel, clientOIDC, false);
URI uri = session.getContext().getUri().getAbsolutePathBuilder().path(client.getClientId()).build();

View file

@ -17,6 +17,35 @@
package org.keycloak.testsuite.client;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.security.KeyFactory;
import java.security.KeyPair;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.stream.Collectors;
import javax.ws.rs.BadRequestException;
import javax.ws.rs.core.Response;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
@ -116,34 +145,6 @@ import org.keycloak.testsuite.util.OAuthClient;
import org.keycloak.testsuite.util.ServerURLs;
import org.keycloak.util.JsonSerialization;
import javax.ws.rs.BadRequestException;
import javax.ws.rs.core.Response;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.security.KeyFactory;
import java.security.KeyPair;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.stream.Collectors;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;