Skip to content

Commit

Permalink
Keycloak 10489 support for client secret rotation (keycloak#10603)
Browse files Browse the repository at this point in the history
  • Loading branch information
marcelomrwin authored Mar 8, 2022
1 parent fd2cd68 commit 7335aba
Show file tree
Hide file tree
Showing 27 changed files with 1,820 additions and 205 deletions.
197 changes: 96 additions & 101 deletions common/src/main/java/org/keycloak/common/Profile.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,15 @@

package org.keycloak.common;

import org.jboss.logging.Logger;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.HashSet;
import java.util.Properties;
import java.util.Set;

import org.jboss.logging.Logger;

import static org.keycloak.common.Profile.Type.DEPRECATED;

/**
Expand All @@ -34,105 +34,17 @@
*/
public class Profile {

private static final Logger logger = Logger.getLogger(Profile.class);

public static final String PRODUCT_NAME = ProductValue.RHSSO.getName();
public static final String PROJECT_NAME = ProductValue.KEYCLOAK.getName();

public enum Type {
DEFAULT,
DISABLED_BY_DEFAULT,
PREVIEW,
EXPERIMENTAL,
DEPRECATED;
}

public enum Feature {
AUTHORIZATION("Authorization Service", Type.DEFAULT),
ACCOUNT2("New Account Management Console", Type.DEFAULT),
ACCOUNT_API("Account Management REST API", Type.DEFAULT),
ADMIN_FINE_GRAINED_AUTHZ("Fine-Grained Admin Permissions", Type.PREVIEW),
ADMIN2("New Admin Console", Type.EXPERIMENTAL),
DOCKER("Docker Registry protocol", Type.DISABLED_BY_DEFAULT),
IMPERSONATION("Ability for admins to impersonate users", Type.DEFAULT),
OPENSHIFT_INTEGRATION("Extension to enable securing OpenShift", Type.PREVIEW),
SCRIPTS("Write custom authenticators using JavaScript", Type.PREVIEW),
TOKEN_EXCHANGE("Token Exchange Service", Type.PREVIEW),
UPLOAD_SCRIPTS("Ability to upload custom JavaScript through Admin REST API", DEPRECATED),
WEB_AUTHN("W3C Web Authentication (WebAuthn)", Type.DEFAULT, Type.PREVIEW),
CLIENT_POLICIES("Client configuration policies", Type.DEFAULT),
CIBA("OpenID Connect Client Initiated Backchannel Authentication (CIBA)", Type.DEFAULT),
MAP_STORAGE("New store", Type.EXPERIMENTAL),
PAR("OAuth 2.0 Pushed Authorization Requests (PAR)", Type.DEFAULT),
DECLARATIVE_USER_PROFILE("Configure user profiles using a declarative style", Type.PREVIEW),
DYNAMIC_SCOPES("Dynamic OAuth 2.0 scopes", Type.EXPERIMENTAL),
STEP_UP_AUTHENTICATION("Step-up Authentication", Type.DEFAULT);

private String label;
private final Type typeProject;
private final Type typeProduct;

Feature(String label, Type type) {
this(label, type, type);
}

Feature(String label, Type typeProject, Type typeProduct) {
this.label = label;
this.typeProject = typeProject;
this.typeProduct = typeProduct;
}

public String getLabel() {
return label;
}

public Type getTypeProject() {
return typeProject;
}

public Type getTypeProduct() {
return typeProduct;
}

public boolean hasDifferentProductType() {
return typeProject != typeProduct;
}
}

private enum ProductValue {
KEYCLOAK("Keycloak"),
RHSSO("RH-SSO");

private final String name;

ProductValue(String name) {
this.name = name;
}

public String getName() {
return name;
}
}

private enum ProfileValue {
COMMUNITY,
PRODUCT,
PREVIEW
}

private static final Logger logger = Logger.getLogger(Profile.class);
private static Profile CURRENT;

private final ProductValue product;

private final ProfileValue profile;

private final Set<Feature> disabledFeatures = new HashSet<>();
private final Set<Feature> previewFeatures = new HashSet<>();
private final Set<Feature> experimentalFeatures = new HashSet<>();
private final Set<Feature> deprecatedFeatures = new HashSet<>();

private final PropertyResolver propertyResolver;

public Profile(PropertyResolver resolver) {
this.propertyResolver = resolver;
Config config = new Config();
Expand Down Expand Up @@ -191,14 +103,14 @@ private static Profile getInstance() {
return CURRENT;
}

public static void init() {
CURRENT = new Profile(null);
}

public static void setInstance(Profile instance) {
CURRENT = instance;
}

public static void init() {
CURRENT = new Profile(null);
}

public static String getName() {
return getInstance().profile.name().toLowerCase();
}
Expand Down Expand Up @@ -227,6 +139,93 @@ public static boolean isProduct() {
return getInstance().profile.equals(ProfileValue.PRODUCT);
}

public enum Type {
DEFAULT,
DISABLED_BY_DEFAULT,
PREVIEW,
EXPERIMENTAL,
DEPRECATED;
}

public enum Feature {
AUTHORIZATION("Authorization Service", Type.DEFAULT),
ACCOUNT2("New Account Management Console", Type.DEFAULT),
ACCOUNT_API("Account Management REST API", Type.DEFAULT),
ADMIN_FINE_GRAINED_AUTHZ("Fine-Grained Admin Permissions", Type.PREVIEW),
ADMIN2("New Admin Console", Type.EXPERIMENTAL),
DOCKER("Docker Registry protocol", Type.DISABLED_BY_DEFAULT),
IMPERSONATION("Ability for admins to impersonate users", Type.DEFAULT),
OPENSHIFT_INTEGRATION("Extension to enable securing OpenShift", Type.PREVIEW),
SCRIPTS("Write custom authenticators using JavaScript", Type.PREVIEW),
TOKEN_EXCHANGE("Token Exchange Service", Type.PREVIEW),
UPLOAD_SCRIPTS("Ability to upload custom JavaScript through Admin REST API", DEPRECATED),
WEB_AUTHN("W3C Web Authentication (WebAuthn)", Type.DEFAULT, Type.PREVIEW),
CLIENT_POLICIES("Client configuration policies", Type.DEFAULT),
CIBA("OpenID Connect Client Initiated Backchannel Authentication (CIBA)", Type.DEFAULT),
MAP_STORAGE("New store", Type.EXPERIMENTAL),
PAR("OAuth 2.0 Pushed Authorization Requests (PAR)", Type.DEFAULT),
DECLARATIVE_USER_PROFILE("Configure user profiles using a declarative style", Type.PREVIEW),
DYNAMIC_SCOPES("Dynamic OAuth 2.0 scopes", Type.EXPERIMENTAL),
CLIENT_SECRET_ROTATION("Client Secret Rotation", Type.PREVIEW),
STEP_UP_AUTHENTICATION("Step-up Authentication", Type.DEFAULT);


private final Type typeProject;
private final Type typeProduct;
private String label;

Feature(String label, Type type) {
this(label, type, type);
}

Feature(String label, Type typeProject, Type typeProduct) {
this.label = label;
this.typeProject = typeProject;
this.typeProduct = typeProduct;
}

public String getLabel() {
return label;
}

public Type getTypeProject() {
return typeProject;
}

public Type getTypeProduct() {
return typeProduct;
}

public boolean hasDifferentProductType() {
return typeProject != typeProduct;
}
}

private enum ProductValue {
KEYCLOAK("Keycloak"),
RHSSO("RH-SSO");

private final String name;

ProductValue(String name) {
this.name = name;
}

public String getName() {
return name;
}
}

private enum ProfileValue {
COMMUNITY,
PRODUCT,
PREVIEW
}

public interface PropertyResolver {
String resolve(String feature);
}

private class Config {

private Properties properties;
Expand Down Expand Up @@ -287,17 +286,13 @@ private String getProperty(String name) {
if (value != null) {
return value;
}

if (propertyResolver != null) {
return propertyResolver.resolve(name);
}

return null;
}
}

public interface PropertyResolver {
String resolve(String feature);
}

}
9 changes: 5 additions & 4 deletions common/src/test/java/org/keycloak/common/ProfileTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import java.util.Comparator;
import java.util.Properties;
import java.util.Set;
import org.keycloak.common.Profile.Feature;

public class ProfileTest {

Expand All @@ -21,8 +22,8 @@ public class ProfileTest {
@Test
public void checkDefaultsKeycloak() {
Assert.assertEquals("community", Profile.getName());
assertEquals(Profile.getDisabledFeatures(), Profile.Feature.ADMIN_FINE_GRAINED_AUTHZ, Profile.Feature.DYNAMIC_SCOPES, Profile.Feature.ADMIN2, Profile.Feature.DOCKER, Profile.Feature.SCRIPTS, Profile.Feature.TOKEN_EXCHANGE, Profile.Feature.OPENSHIFT_INTEGRATION, Profile.Feature.UPLOAD_SCRIPTS, Profile.Feature.MAP_STORAGE, Profile.Feature.DECLARATIVE_USER_PROFILE);
assertEquals(Profile.getPreviewFeatures(), Profile.Feature.ADMIN_FINE_GRAINED_AUTHZ, Profile.Feature.SCRIPTS, Profile.Feature.TOKEN_EXCHANGE, Profile.Feature.OPENSHIFT_INTEGRATION, Profile.Feature.DECLARATIVE_USER_PROFILE);
assertEquals(Profile.getDisabledFeatures(), Profile.Feature.ADMIN_FINE_GRAINED_AUTHZ, Profile.Feature.DYNAMIC_SCOPES, Profile.Feature.ADMIN2, Profile.Feature.DOCKER, Profile.Feature.SCRIPTS, Profile.Feature.TOKEN_EXCHANGE, Profile.Feature.OPENSHIFT_INTEGRATION, Profile.Feature.UPLOAD_SCRIPTS, Profile.Feature.MAP_STORAGE, Profile.Feature.DECLARATIVE_USER_PROFILE, Feature.CLIENT_SECRET_ROTATION);
assertEquals(Profile.getPreviewFeatures(), Profile.Feature.ADMIN_FINE_GRAINED_AUTHZ, Profile.Feature.SCRIPTS, Profile.Feature.TOKEN_EXCHANGE, Profile.Feature.OPENSHIFT_INTEGRATION, Profile.Feature.DECLARATIVE_USER_PROFILE, Feature.CLIENT_SECRET_ROTATION);
assertEquals(Profile.getDeprecatedFeatures(), Profile.Feature.UPLOAD_SCRIPTS);

Assert.assertTrue(Profile.Feature.WEB_AUTHN.hasDifferentProductType());
Expand All @@ -37,8 +38,8 @@ public void checkDefaultsRH_SSO() {
Profile.init();

Assert.assertEquals("product", Profile.getName());
assertEquals(Profile.getDisabledFeatures(), Profile.Feature.ADMIN_FINE_GRAINED_AUTHZ, Profile.Feature.DYNAMIC_SCOPES, Profile.Feature.ADMIN2, Profile.Feature.DOCKER, Profile.Feature.SCRIPTS, Profile.Feature.TOKEN_EXCHANGE, Profile.Feature.OPENSHIFT_INTEGRATION, Profile.Feature.UPLOAD_SCRIPTS, Profile.Feature.WEB_AUTHN, Profile.Feature.MAP_STORAGE, Profile.Feature.DECLARATIVE_USER_PROFILE);
assertEquals(Profile.getPreviewFeatures(), Profile.Feature.ADMIN_FINE_GRAINED_AUTHZ, Profile.Feature.SCRIPTS, Profile.Feature.TOKEN_EXCHANGE, Profile.Feature.OPENSHIFT_INTEGRATION, Profile.Feature.WEB_AUTHN, Profile.Feature.DECLARATIVE_USER_PROFILE);
assertEquals(Profile.getDisabledFeatures(), Profile.Feature.ADMIN_FINE_GRAINED_AUTHZ, Profile.Feature.DYNAMIC_SCOPES, Profile.Feature.ADMIN2, Profile.Feature.DOCKER, Profile.Feature.SCRIPTS, Profile.Feature.TOKEN_EXCHANGE, Profile.Feature.OPENSHIFT_INTEGRATION, Profile.Feature.UPLOAD_SCRIPTS, Profile.Feature.WEB_AUTHN, Profile.Feature.MAP_STORAGE, Profile.Feature.DECLARATIVE_USER_PROFILE, Feature.CLIENT_SECRET_ROTATION);
assertEquals(Profile.getPreviewFeatures(), Profile.Feature.ADMIN_FINE_GRAINED_AUTHZ, Profile.Feature.SCRIPTS, Profile.Feature.TOKEN_EXCHANGE, Profile.Feature.OPENSHIFT_INTEGRATION, Profile.Feature.WEB_AUTHN, Profile.Feature.DECLARATIVE_USER_PROFILE, Feature.CLIENT_SECRET_ROTATION);
assertEquals(Profile.getDeprecatedFeatures(), Profile.Feature.UPLOAD_SCRIPTS);

Assert.assertTrue(Profile.Feature.WEB_AUTHN.hasDifferentProductType());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,4 +42,8 @@ public Map<String, Object> getConfigAsMap() {
public void setConfigAsMap(String name, Object value) {
this.configAsMap.put(name, value);
}

public boolean validateConfig(){
return true;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,8 @@

package org.keycloak.admin.client.resource;

import org.jboss.resteasy.annotations.cache.NoCache;
import org.keycloak.representations.adapters.action.GlobalRequestResult;
import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.representations.idm.ClientScopeRepresentation;
import org.keycloak.representations.idm.CredentialRepresentation;
import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.representations.idm.UserSessionRepresentation;
import org.keycloak.representations.idm.ManagementPermissionReference;
import org.keycloak.representations.idm.ManagementPermissionRepresentation;
import java.util.List;
import java.util.Map;

import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
Expand All @@ -37,8 +30,16 @@
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.MediaType;
import java.util.List;
import java.util.Map;

import org.jboss.resteasy.annotations.cache.NoCache;
import org.keycloak.representations.adapters.action.GlobalRequestResult;
import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.representations.idm.ClientScopeRepresentation;
import org.keycloak.representations.idm.CredentialRepresentation;
import org.keycloak.representations.idm.ManagementPermissionReference;
import org.keycloak.representations.idm.ManagementPermissionRepresentation;
import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.representations.idm.UserSessionRepresentation;

/**
* @author rodrigo.sasaki@icarros.com.br
Expand Down Expand Up @@ -207,4 +208,16 @@ public interface ClientResource {

@Path("/authz/resource-server")
AuthorizationResource authorization();


@Path("client-secret/rotated")
@GET
@Produces(MediaType.APPLICATION_JSON)
public CredentialRepresentation getClientRotatedSecret();

@Path("client-secret/rotated")
@DELETE
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public void invalidateRotatedSecret();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package org.keycloak.models;

/**
* @author <a href="mailto:masales@redhat.com">Marcelo Sales</a>
*/
public class ClientSecretConstants {

// client attribute names
public static final String CLIENT_SECRET_ROTATION_ENABLED = "client.secret.rotation.enabled";
public static final String CLIENT_SECRET_CREATION_TIME = "client.secret.creation.time";
public static final String CLIENT_SECRET_EXPIRATION = "client.secret.expiration.time";
public static final String CLIENT_ROTATED_SECRET = "client.secret.rotated";
public static final String CLIENT_ROTATED_SECRET_CREATION_TIME = "client.secret.rotated.creation.time";
public static final String CLIENT_ROTATED_SECRET_EXPIRATION_TIME = "client.secret.rotated.expiration.time";
public static final String CLIENT_SECRET_REMAINING_EXPIRATION_TIME = "client.secret.remaining.expiration.time";

}
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,13 @@
import org.keycloak.common.util.KeyUtils;
import org.keycloak.common.util.PemUtils;
import org.keycloak.common.util.SecretGenerator;
import org.keycloak.common.util.Time;
import org.keycloak.component.ComponentModel;
import org.keycloak.models.AuthenticationExecutionModel;
import org.keycloak.models.AuthenticationFlowModel;
import org.keycloak.models.ClientModel;
import org.keycloak.models.ClientScopeModel;
import org.keycloak.models.ClientSecretConstants;
import org.keycloak.models.Constants;
import org.keycloak.models.GroupModel;
import org.keycloak.models.IdentityProviderModel;
Expand Down Expand Up @@ -148,6 +150,7 @@ public static CertificateRepresentation generateKeyPairCertificate(String subjec
public static String generateSecret(ClientModel client) {
String secret = SecretGenerator.getInstance().randomString();
client.setSecret(secret);
client.setAttribute(ClientSecretConstants.CLIENT_SECRET_CREATION_TIME,String.valueOf(Time.currentTime()));
return secret;
}

Expand Down
Loading

0 comments on commit 7335aba

Please sign in to comment.