Introduce client secret rotation dynamic registration (#10952)
Closes #10609
This commit is contained in:
parent
6a657e6472
commit
091b1472ce
6 changed files with 747 additions and 529 deletions
|
@ -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) {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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;
|
||||
|
|
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue