Fix authenticatorConfig for javascript providers

Closes #20005
This commit is contained in:
mposolda 2023-07-27 14:15:31 +02:00 committed by Marek Posolda
parent b2e11735ed
commit 6f6b5e8e84
18 changed files with 500 additions and 69 deletions

View file

@ -75,7 +75,7 @@ public class ExportUtils {
public static RealmRepresentation exportRealm(KeycloakSession session, RealmModel realm, ExportOptions options, boolean internal) {
RealmRepresentation rep = ModelToRepresentation.toRepresentation(session, realm, internal);
ModelToRepresentation.exportAuthenticationFlows(realm, rep);
ModelToRepresentation.exportAuthenticationFlows(session, realm, rep);
ModelToRepresentation.exportRequiredActions(realm, rep);
// Project/product version

View file

@ -21,6 +21,7 @@ import org.jboss.logging.Logger;
import org.keycloak.common.enums.SslRequired;
import org.keycloak.common.util.MultivaluedHashMap;
import org.keycloak.component.ComponentModel;
import org.keycloak.deployment.DeployedConfigurationsManager;
import org.keycloak.exportimport.ExportAdapter;
import org.keycloak.exportimport.ExportOptions;
import org.keycloak.exportimport.util.ExportUtils;
@ -307,7 +308,7 @@ public class LegacyExportImportManager implements ExportImportManager {
updateParSettings(rep, newRealm);
Map<String, String> mappedFlows = importAuthenticationFlows(newRealm, rep);
Map<String, String> mappedFlows = importAuthenticationFlows(session, newRealm, rep);
if (rep.getRequiredActions() != null) {
for (RequiredActionProviderRepresentation action : rep.getRequiredActions()) {
RequiredActionProviderModel model = toModel(action);
@ -1257,7 +1258,7 @@ public class LegacyExportImportManager implements ExportImportManager {
return webAuthnPolicy;
}
public static Map<String, String> importAuthenticationFlows(RealmModel newRealm, RealmRepresentation rep) {
public static Map<String, String> importAuthenticationFlows(KeycloakSession session, RealmModel newRealm, RealmRepresentation rep) {
Map<String, String> mappedFlows = new HashMap<>();
if (rep.getAuthenticationFlows() == null) {
// assume this is an old version being imported
@ -1285,7 +1286,7 @@ public class LegacyExportImportManager implements ExportImportManager {
for (AuthenticationFlowRepresentation flowRep : rep.getAuthenticationFlows()) {
AuthenticationFlowModel model = newRealm.getFlowByAlias(flowRep.getAlias());
for (AuthenticationExecutionExportRepresentation exeRep : flowRep.getAuthenticationExecutions()) {
AuthenticationExecutionModel execution = toModel(newRealm, model, exeRep);
AuthenticationExecutionModel execution = toModel(session, newRealm, model, exeRep);
newRealm.addAuthenticatorExecution(execution);
}
}
@ -1371,10 +1372,10 @@ public class LegacyExportImportManager implements ExportImportManager {
return mappedFlows;
}
private static AuthenticationExecutionModel toModel(RealmModel realm, AuthenticationFlowModel parentFlow, AuthenticationExecutionExportRepresentation rep) {
private static AuthenticationExecutionModel toModel(KeycloakSession session, RealmModel realm, AuthenticationFlowModel parentFlow, AuthenticationExecutionExportRepresentation rep) {
AuthenticationExecutionModel model = new AuthenticationExecutionModel();
if (rep.getAuthenticatorConfig() != null) {
AuthenticatorConfigModel config = realm.getAuthenticatorConfigByAlias(rep.getAuthenticatorConfig());
AuthenticatorConfigModel config = new DeployedConfigurationsManager(session).getAuthenticatorConfigByAlias(realm, rep.getAuthenticatorConfig());
model.setAuthenticatorConfig(config.getId());
}
model.setAuthenticator(rep.getAuthenticator());

View file

@ -24,6 +24,7 @@ import org.keycloak.Config;
import org.keycloak.common.enums.SslRequired;
import org.keycloak.common.util.MultivaluedHashMap;
import org.keycloak.component.ComponentModel;
import org.keycloak.deployment.DeployedConfigurationsManager;
import org.keycloak.models.AdminRoles;
import org.keycloak.models.ClientProvider;
import org.keycloak.models.ClientScopeProvider;
@ -314,7 +315,7 @@ public class MapExportImportManager implements ExportImportManager {
updateParSettings(rep, newRealm);
Map<String, String> mappedFlows = importAuthenticationFlows(newRealm, rep);
Map<String, String> mappedFlows = importAuthenticationFlows(session, newRealm, rep);
if (rep.getRequiredActions() != null) {
for (RequiredActionProviderRepresentation action : rep.getRequiredActions()) {
RequiredActionProviderModel model = toModel(action);
@ -1461,7 +1462,7 @@ public class MapExportImportManager implements ExportImportManager {
return webAuthnPolicy;
}
public static Map<String, String> importAuthenticationFlows(RealmModel newRealm, RealmRepresentation rep) {
public static Map<String, String> importAuthenticationFlows(KeycloakSession session, RealmModel newRealm, RealmRepresentation rep) {
Map<String, String> mappedFlows = new HashMap<>();
if (rep.getAuthenticationFlows() == null) {
// assume this is an old version being imported
@ -1489,7 +1490,7 @@ public class MapExportImportManager implements ExportImportManager {
for (AuthenticationFlowRepresentation flowRep : rep.getAuthenticationFlows()) {
AuthenticationFlowModel model = newRealm.getFlowByAlias(flowRep.getAlias());
for (AuthenticationExecutionExportRepresentation exeRep : flowRep.getAuthenticationExecutions()) {
AuthenticationExecutionModel execution = toModel(newRealm, model, exeRep);
AuthenticationExecutionModel execution = toModel(session, newRealm, model, exeRep);
newRealm.addAuthenticatorExecution(execution);
}
}
@ -1575,10 +1576,10 @@ public class MapExportImportManager implements ExportImportManager {
return mappedFlows;
}
private static AuthenticationExecutionModel toModel(RealmModel realm, AuthenticationFlowModel parentFlow, AuthenticationExecutionExportRepresentation rep) {
private static AuthenticationExecutionModel toModel(KeycloakSession session, RealmModel realm, AuthenticationFlowModel parentFlow, AuthenticationExecutionExportRepresentation rep) {
AuthenticationExecutionModel model = new AuthenticationExecutionModel();
if (rep.getAuthenticatorConfig() != null) {
AuthenticatorConfigModel config = realm.getAuthenticatorConfigByAlias(rep.getAuthenticatorConfig());
AuthenticatorConfigModel config = new DeployedConfigurationsManager(session).getAuthenticatorConfigByAlias(realm, rep.getAuthenticatorConfig());
model.setAuthenticatorConfig(config.getId());
}
model.setAuthenticator(rep.getAuthenticator());

View file

@ -0,0 +1,78 @@
/*
* Copyright 2023 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package org.keycloak.deployment;
import org.jboss.logging.Logger;
import org.keycloak.models.AuthenticatorConfigModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
/**
* Allows to CRUD for configurations (like Authenticator configs). Those are typically saved in the store (realm), but can be also
* deployed and hence not saved in the DB
*
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class DeployedConfigurationsManager {
private static final Logger log = Logger.getLogger(DeployedConfigurationsManager.class);
private final KeycloakSession session;
public DeployedConfigurationsManager(KeycloakSession session) {
this.session = session;
}
public void registerDeployedAuthenticatorConfig(AuthenticatorConfigModel model) {
log.debugf("Register deployed authenticator config: %s", model.getId());
session.getProvider(DeployedConfigurationsProvider.class).registerDeployedAuthenticatorConfig(model);
}
public AuthenticatorConfigModel getDeployedAuthenticatorConfig(String configId) {
return session.getProvider(DeployedConfigurationsProvider.class).getDeployedAuthenticatorConfigs()
.filter(config -> configId.equals(config.getId()))
.findFirst().orElse(null);
}
public AuthenticatorConfigModel getAuthenticatorConfig(RealmModel realm, String configId) {
AuthenticatorConfigModel cfgModel = getDeployedAuthenticatorConfig(configId);
if (cfgModel != null) {
log.tracef("Found deployed configuration by id: %s", configId);
return cfgModel;
} else {
return realm.getAuthenticatorConfigById(configId);
}
}
public AuthenticatorConfigModel getAuthenticatorConfigByAlias(RealmModel realm, String alias) {
if (alias == null) return null;
AuthenticatorConfigModel cfgModel = session.getProvider(DeployedConfigurationsProvider.class).getDeployedAuthenticatorConfigs()
.filter(config -> alias.equals(config.getAlias()))
.findFirst().orElse(null);
if (cfgModel != null) {
log.debugf("Found deployed configuration by alias: %s", alias);
return cfgModel;
} else {
return realm.getAuthenticatorConfigByAlias(alias);
}
}
}

View file

@ -0,0 +1,38 @@
/*
* Copyright 2023 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package org.keycloak.deployment;
import java.util.stream.Stream;
import org.keycloak.models.AuthenticatorConfigModel;
import org.keycloak.provider.Provider;
/**
* Allows to register "deployed configurations", which are retrieved in runtime from deployed providers and hence are not saved in the DB
*
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public interface DeployedConfigurationsProvider extends Provider {
void registerDeployedAuthenticatorConfig(AuthenticatorConfigModel model);
Stream<AuthenticatorConfigModel> getDeployedAuthenticatorConfigs();
}

View file

@ -0,0 +1,28 @@
/*
* Copyright 2023 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package org.keycloak.deployment;
import org.keycloak.provider.ProviderFactory;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public interface DeployedConfigurationsProviderFactory extends ProviderFactory<DeployedConfigurationsProvider> {
}

View file

@ -0,0 +1,50 @@
/*
* Copyright 2023 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package org.keycloak.deployment;
import org.keycloak.provider.Provider;
import org.keycloak.provider.ProviderFactory;
import org.keycloak.provider.Spi;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class DeployedConfigurationsSpi implements Spi {
@Override
public boolean isInternal() {
return true;
}
@Override
public String getName() {
return "deployed-configurations";
}
@Override
public Class<? extends Provider> getProviderClass() {
return DeployedConfigurationsProvider.class;
}
@Override
public Class<? extends ProviderFactory> getProviderFactoryClass() {
return DeployedConfigurationsProviderFactory.class;
}
}

View file

@ -35,6 +35,7 @@ import org.keycloak.common.util.Time;
import org.keycloak.component.ComponentModel;
import org.keycloak.credential.CredentialMetadata;
import org.keycloak.credential.CredentialModel;
import org.keycloak.deployment.DeployedConfigurationsManager;
import org.keycloak.events.Event;
import org.keycloak.events.admin.AdminEvent;
import org.keycloak.events.admin.AuthDetails;
@ -547,7 +548,7 @@ public class ModelToRepresentation {
rep.setSupportedLocales(realm.getSupportedLocalesStream().collect(Collectors.toSet()));
rep.setDefaultLocale(realm.getDefaultLocale());
if (internal) {
exportAuthenticationFlows(realm, rep);
exportAuthenticationFlows(session, realm, rep);
exportRequiredActions(realm, rep);
exportGroups(realm, rep);
}
@ -586,10 +587,10 @@ public class ModelToRepresentation {
rep.setGroups(toGroupHierarchy(realm, true).collect(Collectors.toList()));
}
public static void exportAuthenticationFlows(RealmModel realm, RealmRepresentation rep) {
public static void exportAuthenticationFlows(KeycloakSession session, RealmModel realm, RealmRepresentation rep) {
List<AuthenticationFlowRepresentation> authenticationFlows = realm.getAuthenticationFlowsStream()
.sorted(AuthenticationFlowModel.AuthenticationFlowComparator.SINGLETON)
.map(flow -> toRepresentation(realm, flow))
.map(flow -> toRepresentation(session, realm, flow))
.collect(Collectors.toList());
rep.setAuthenticationFlows(authenticationFlows);
@ -882,7 +883,7 @@ public class ModelToRepresentation {
return consentRep;
}
public static AuthenticationFlowRepresentation toRepresentation(RealmModel realm, AuthenticationFlowModel model) {
public static AuthenticationFlowRepresentation toRepresentation(KeycloakSession session, RealmModel realm, AuthenticationFlowModel model) {
AuthenticationFlowRepresentation rep = new AuthenticationFlowRepresentation();
rep.setId(model.getId());
rep.setBuiltIn(model.isBuiltIn());
@ -891,14 +892,14 @@ public class ModelToRepresentation {
rep.setAlias(model.getAlias());
rep.setDescription(model.getDescription());
rep.setAuthenticationExecutions(realm.getAuthenticationExecutionsStream(model.getId())
.map(e -> toRepresentation(realm, e)).collect(Collectors.toList()));
.map(e -> toRepresentation(session, realm, e)).collect(Collectors.toList()));
return rep;
}
public static AuthenticationExecutionExportRepresentation toRepresentation(RealmModel realm, AuthenticationExecutionModel model) {
public static AuthenticationExecutionExportRepresentation toRepresentation(KeycloakSession session, RealmModel realm, AuthenticationExecutionModel model) {
AuthenticationExecutionExportRepresentation rep = new AuthenticationExecutionExportRepresentation();
if (model.getAuthenticatorConfig() != null) {
AuthenticatorConfigModel config = realm.getAuthenticatorConfigById(model.getAuthenticatorConfig());
AuthenticatorConfigModel config = new DeployedConfigurationsManager(session).getAuthenticatorConfig(realm, model.getAuthenticatorConfig());
rep.setAuthenticatorConfig(config.getAlias());
}
rep.setAuthenticator(model.getAuthenticator());

View file

@ -57,6 +57,7 @@ import org.keycloak.common.util.MultivaluedHashMap;
import org.keycloak.common.util.UriUtils;
import org.keycloak.component.ComponentModel;
import org.keycloak.credential.CredentialModel;
import org.keycloak.deployment.DeployedConfigurationsManager;
import org.keycloak.migration.migrators.MigrationUtils;
import org.keycloak.models.AuthenticationExecutionModel;
import org.keycloak.models.AuthenticationFlowModel;
@ -926,7 +927,7 @@ public class RepresentationToModel {
}
public static AuthenticationExecutionModel toModel(RealmModel realm, AuthenticationExecutionRepresentation rep) {
public static AuthenticationExecutionModel toModel(KeycloakSession session, RealmModel realm, AuthenticationExecutionRepresentation rep) {
AuthenticationExecutionModel model = new AuthenticationExecutionModel();
model.setId(rep.getId());
model.setFlowId(rep.getFlowId());
@ -938,7 +939,7 @@ public class RepresentationToModel {
model.setRequirement(AuthenticationExecutionModel.Requirement.valueOf(rep.getRequirement()));
if (rep.getAuthenticatorConfig() != null) {
AuthenticatorConfigModel cfg = realm.getAuthenticatorConfigByAlias(rep.getAuthenticatorConfig());
AuthenticatorConfigModel cfg = new DeployedConfigurationsManager(session).getAuthenticatorConfigByAlias(realm, rep.getAuthenticatorConfig());
model.setAuthenticatorConfig(cfg.getId());
}
return model;

View file

@ -62,6 +62,7 @@ org.keycloak.authentication.otp.OTPApplicationSpi
org.keycloak.authorization.policy.provider.PolicySpi
org.keycloak.authorization.store.StoreFactorySpi
org.keycloak.authorization.AuthorizationSpi
org.keycloak.deployment.DeployedConfigurationsSpi
org.keycloak.models.cache.authorization.CachedStoreFactorySpi
org.keycloak.protocol.oidc.TokenExchangeSpi
org.keycloak.protocol.oidc.TokenIntrospectionSpi

View file

@ -24,9 +24,14 @@ import org.keycloak.Config;
import org.keycloak.authentication.AuthenticationFlowContext;
import org.keycloak.authentication.Authenticator;
import org.keycloak.common.Profile;
import org.keycloak.deployment.DeployedConfigurationsManager;
import org.keycloak.models.AuthenticatorConfigModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.models.utils.PostMigrationEvent;
import org.keycloak.provider.ProviderConfigProperty;
import org.keycloak.provider.ProviderEvent;
import org.keycloak.representations.provider.ScriptProviderMetadata;
/**
@ -88,6 +93,13 @@ public final class DeployedScriptAuthenticatorFactory extends ScriptBasedAuthent
configProperties = super.getConfigProperties();
}
@Override
public void postInit(KeycloakSessionFactory factory) {
KeycloakModelUtils.runJobInTransaction(factory, session -> {
new DeployedConfigurationsManager(session).registerDeployedAuthenticatorConfig(model);
});
}
@Override
public List<ProviderConfigProperty> getConfigProperties() {
return configProperties;

View file

@ -0,0 +1,51 @@
/*
* Copyright 2023 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package org.keycloak.deployment;
import java.util.Map;
import java.util.stream.Stream;
import org.keycloak.models.AuthenticatorConfigModel;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class DefaultDeployedConfigurationsProvider implements DeployedConfigurationsProvider {
private final Map<String, AuthenticatorConfigModel> deployedAuthenticatorConfigs;
public DefaultDeployedConfigurationsProvider(Map<String, AuthenticatorConfigModel> deployedAuthenticatorConfigs) {
this.deployedAuthenticatorConfigs = deployedAuthenticatorConfigs;
}
@Override
public void registerDeployedAuthenticatorConfig(AuthenticatorConfigModel model) {
deployedAuthenticatorConfigs.put(model.getId(), model);
}
@Override
public Stream<AuthenticatorConfigModel> getDeployedAuthenticatorConfigs() {
return deployedAuthenticatorConfigs.values().stream();
}
@Override
public void close() {
}
}

View file

@ -0,0 +1,62 @@
/*
* Copyright 2023 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package org.keycloak.deployment;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Stream;
import org.keycloak.Config;
import org.keycloak.models.AuthenticatorConfigModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class DefaultDeployedConfigurationsProviderFactory implements DeployedConfigurationsProviderFactory {
public static final String PROVIDER_ID = "default";
private final Map<String, AuthenticatorConfigModel> deployedAuthenticatorConfigs = new ConcurrentHashMap<>();
@Override
public DeployedConfigurationsProvider create(KeycloakSession session) {
return new DefaultDeployedConfigurationsProvider(deployedAuthenticatorConfigs);
}
@Override
public void init(Config.Scope config) {
}
@Override
public void postInit(KeycloakSessionFactory factory) {
}
@Override
public void close() {
}
@Override
public String getId() {
return PROVIDER_ID;
}
}

View file

@ -33,11 +33,13 @@ import org.keycloak.authentication.FormAction;
import org.keycloak.authentication.FormAuthenticator;
import org.keycloak.authentication.RequiredActionFactory;
import org.keycloak.authentication.RequiredActionProvider;
import org.keycloak.deployment.DeployedConfigurationsManager;
import org.keycloak.events.admin.OperationType;
import org.keycloak.events.admin.ResourceType;
import org.keycloak.models.AuthenticationExecutionModel;
import org.keycloak.models.AuthenticationFlowModel;
import org.keycloak.models.AuthenticatorConfigModel;
import org.keycloak.models.Constants;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.RequiredActionProviderModel;
@ -205,7 +207,7 @@ public class AuthenticationManagementResource {
return realm.getAuthenticationFlowsStream()
.filter(flow -> flow.isTopLevel() && !Objects.equals(flow.getAlias(), DefaultAuthenticationFlows.SAML_ECP_FLOW))
.map(flow -> ModelToRepresentation.toRepresentation(realm, flow));
.map(flow -> ModelToRepresentation.toRepresentation(session, realm, flow));
}
/**
@ -264,7 +266,7 @@ public class AuthenticationManagementResource {
if (flow == null) {
throw new NotFoundException("Could not find flow with id");
}
return ModelToRepresentation.toRepresentation(realm, flow);
return ModelToRepresentation.toRepresentation(session, realm, flow);
}
/**
@ -375,7 +377,7 @@ public class AuthenticationManagementResource {
logger.debug("flow not found: " + flowAlias);
return Response.status(NOT_FOUND).build();
}
AuthenticationFlowModel copy = copyFlow(realm, flow, newName);
AuthenticationFlowModel copy = copyFlow(session, realm, flow, newName);
data.put("id", copy.getId());
adminEvent.operation(OperationType.CREATE).resourcePath(session.getContext().getUri()).representation(data).success();
@ -384,7 +386,7 @@ public class AuthenticationManagementResource {
}
public static AuthenticationFlowModel copyFlow(RealmModel realm, AuthenticationFlowModel flow, String newName) {
public static AuthenticationFlowModel copyFlow(KeycloakSession session, RealmModel realm, AuthenticationFlowModel flow, String newName) {
AuthenticationFlowModel copy = new AuthenticationFlowModel();
copy.setAlias(newName);
copy.setDescription(flow.getDescription());
@ -392,11 +394,11 @@ public class AuthenticationManagementResource {
copy.setBuiltIn(false);
copy.setTopLevel(flow.isTopLevel());
copy = realm.addAuthenticationFlow(copy);
copy(realm, newName, flow, copy);
copy(session, realm, newName, flow, copy);
return copy;
}
public static void copy(RealmModel realm, String newName, AuthenticationFlowModel from, AuthenticationFlowModel to) {
public static void copy(KeycloakSession session, RealmModel realm, String newName, AuthenticationFlowModel from, AuthenticationFlowModel to) {
realm.getAuthenticationExecutionsStream(from.getId()).forEachOrdered(execution -> {
if (execution.isAuthenticatorFlow()) {
AuthenticationFlowModel subFlow = realm.getAuthenticationFlowById(execution.getFlowId());
@ -408,26 +410,32 @@ public class AuthenticationManagementResource {
copy.setTopLevel(false);
copy = realm.addAuthenticationFlow(copy);
execution.setFlowId(copy.getId());
copy(realm, newName, subFlow, copy);
copy(session, realm, newName, subFlow, copy);
}
if (execution.getAuthenticatorConfig() != null) {
AuthenticatorConfigModel config = realm.getAuthenticatorConfigById(execution.getAuthenticatorConfig());
DeployedConfigurationsManager configManager = new DeployedConfigurationsManager(session);
AuthenticatorConfigModel config = configManager.getAuthenticatorConfig(realm, execution.getAuthenticatorConfig());
if (config == null) {
logger.debugf("Authentication execution with id [%s] not found", config.getId());
throw new IllegalStateException("Authentication execution configuration not found");
}
config.setId(null);
if (configManager.getDeployedAuthenticatorConfig(execution.getAuthenticatorConfig()) != null) {
// Shared configuration of deployed provider
execution.setAuthenticatorConfig(config.getId());
} else {
config.setId(null);
if (config.getAlias() != null) {
config.setAlias(newName + " " + config.getAlias());
if (config.getAlias() != null) {
config.setAlias(newName + " " + config.getAlias());
}
AuthenticatorConfigModel newConfig = realm.addAuthenticatorConfig(config);
execution.setAuthenticatorConfig(newConfig.getId());
}
AuthenticatorConfigModel newConfig = realm.addAuthenticatorConfig(config);
execution.setAuthenticatorConfig(newConfig.getId());
}
execution.setId(null);
@ -524,17 +532,7 @@ public class AuthenticationManagementResource {
String provider = data.get("provider");
// make sure provider is one of the registered providers
ProviderFactory f;
if (parentFlow.getProviderId().equals(AuthenticationFlow.CLIENT_FLOW)) {
f = session.getKeycloakSessionFactory().getProviderFactory(ClientAuthenticator.class, provider);
} else if (parentFlow.getProviderId().equals(AuthenticationFlow.FORM_FLOW)) {
f = session.getKeycloakSessionFactory().getProviderFactory(FormAction.class, provider);
} else {
f = session.getKeycloakSessionFactory().getProviderFactory(Authenticator.class, provider);
}
if (f == null) {
throw new BadRequestException("No authentication provider found for id: " + provider);
}
ProviderFactory f = getProviderFactory( parentFlow, provider);
AuthenticationExecutionModel execution = new AuthenticationExecutionModel();
execution.setParentFlow(parentFlow.getId());
@ -551,18 +549,7 @@ public class AuthenticationManagementResource {
execution = realm.addAuthenticatorExecution(execution);
if (f instanceof ConfiguredProvider) {
ConfiguredProvider internalProviderFactory = (ConfiguredProvider) f;
AuthenticatorConfigModel config = internalProviderFactory.getConfig();
if (config != null) {
// creates a default configuration if the factory defines one
// useful for internal providers that already provide a built-in configuration
AuthenticatorConfigRepresentation configRepresentation = ModelToRepresentation.toRepresentation(
config);
newExecutionConfig(execution.getId(), configRepresentation).close();
}
}
checkConfigForDeployedProvider(f, execution);
data.put("id", execution.getId());
adminEvent.operation(OperationType.CREATE).resource(ResourceType.AUTH_EXECUTION).resourcePath(session.getContext().getUri()).representation(data).success();
@ -571,6 +558,38 @@ public class AuthenticationManagementResource {
return Response.created(session.getContext().getUri().getBaseUriBuilder().path(session.getContext().getUri().getPath().replace(addExecutionPathSegment, "")).path("executions").path(execution.getId()).build()).build();
}
private ProviderFactory getProviderFactory(AuthenticationFlowModel parentFlow, String provider) {
ProviderFactory f = null;
if (parentFlow.getProviderId().equals(AuthenticationFlow.CLIENT_FLOW)) {
f = session.getKeycloakSessionFactory().getProviderFactory(ClientAuthenticator.class, provider);
} else if (parentFlow.getProviderId().equals(AuthenticationFlow.FORM_FLOW)) {
f = session.getKeycloakSessionFactory().getProviderFactory(FormAction.class, provider);
} else {
f = session.getKeycloakSessionFactory().getProviderFactory(Authenticator.class, provider);
}
if (f == null) {
throw new BadRequestException("No authentication provider found for id: " + provider);
}
return f;
}
private void checkConfigForDeployedProvider(ProviderFactory f, AuthenticationExecutionModel execution) {
if (f instanceof ConfiguredProvider) {
ConfiguredProvider internalProviderFactory = (ConfiguredProvider) f;
AuthenticatorConfigModel config = internalProviderFactory.getConfig();
if (config != null) {
// use a default configuration if the factory defines one
// Assumption is that this is registered in DeployedConfigurationsProvider
// useful for internal providers that already provide a built-in configuration
logger.tracef("Updating execution of provider '%s' with shared configuration.", execution.getAuthenticator());
execution.setAuthenticatorConfig(config.getId());
realm.updateAuthenticatorExecution(execution);
}
}
}
/**
* Get authentication executions for a flow
*
@ -649,7 +668,7 @@ public class AuthenticationManagementResource {
if (factory.isConfigurable()) {
String authenticatorConfigId = execution.getAuthenticatorConfig();
if(authenticatorConfigId != null) {
AuthenticatorConfigModel authenticatorConfig = realm.getAuthenticatorConfigById(authenticatorConfigId);
AuthenticatorConfigModel authenticatorConfig = new DeployedConfigurationsManager(session).getAuthenticatorConfig(realm, authenticatorConfigId);
if (authenticatorConfig != null) {
rep.setAlias(authenticatorConfig.getAlias());
@ -779,7 +798,7 @@ public class AuthenticationManagementResource {
public Response addExecution(@Parameter(description = "JSON model describing authentication execution") AuthenticationExecutionRepresentation execution) {
auth.realm().requireManageRealm();
AuthenticationExecutionModel model = RepresentationToModel.toModel(realm, execution);
AuthenticationExecutionModel model = RepresentationToModel.toModel(session, realm, execution);
AuthenticationFlowModel parentFlow = getParentFlow(model);
if (parentFlow.isBuiltIn()) {
throw new BadRequestException("It is illegal to add execution to a built in flow");
@ -787,6 +806,11 @@ public class AuthenticationManagementResource {
model.setPriority(getNextPriority(parentFlow));
model = realm.addAuthenticatorExecution(model);
if (!execution.isAuthenticatorFlow()) {
ProviderFactory f = getProviderFactory(parentFlow, execution.getAuthenticator());
checkConfigForDeployedProvider(f, model);
}
adminEvent.operation(OperationType.CREATE).resource(ResourceType.AUTH_EXECUTION).resourcePath(session.getContext().getUri(), model.getId()).representation(execution).success();
return Response.created(session.getContext().getUri().getAbsolutePathBuilder().path(model.getId()).build()).build();
}
@ -976,7 +1000,7 @@ public class AuthenticationManagementResource {
public AuthenticatorConfigRepresentation getAuthenticatorConfig(@Parameter(description = "Execution id") @PathParam("executionId") String execution, @Parameter(description = "Configuration id") @PathParam("id") String id) {
auth.realm().requireViewRealm();
AuthenticatorConfigModel config = realm.getAuthenticatorConfigById(id);
AuthenticatorConfigModel config = new DeployedConfigurationsManager(session).getAuthenticatorConfig(realm, id);
if (config == null) {
throw new NotFoundException("Could not find authenticator config");
@ -1317,7 +1341,7 @@ public class AuthenticationManagementResource {
public AuthenticatorConfigRepresentation getAuthenticatorConfig(@Parameter(description = "Configuration id") @PathParam("id") String id) {
auth.realm().requireViewRealm();
AuthenticatorConfigModel config = realm.getAuthenticatorConfigById(id);
AuthenticatorConfigModel config = new DeployedConfigurationsManager(session).getAuthenticatorConfig(realm, id);
if (config == null) {
throw new NotFoundException("Could not find authenticator config");
@ -1369,6 +1393,10 @@ public class AuthenticationManagementResource {
auth.realm().requireManageRealm();
ReservedCharValidator.validate(rep.getAlias());
if (new DeployedConfigurationsManager(session).getDeployedAuthenticatorConfig(id) != null) {
throw new BadRequestException("Authenticator config is read-only");
}
AuthenticatorConfigModel exists = realm.getAuthenticatorConfigById(id);
if (exists == null) {
throw new NotFoundException("Could not find authenticator config");

View file

@ -0,0 +1,20 @@
#
# Copyright 2023 Red Hat, Inc. and/or its affiliates
# and other contributors as indicated by the @author tags.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#
# See the License for the specific language governing permissions and
# limitations under the License.
#
#
org.keycloak.deployment.DefaultDeployedConfigurationsProviderFactory

View file

@ -674,7 +674,7 @@ public class TestingResourceProvider implements RealmResourceProvider {
RealmModel realm = getRealmByName(realmName);
AuthenticationFlowModel flow = realm.getClientAuthenticationFlow();
if (flow == null) return null;
return ModelToRepresentation.toRepresentation(realm, flow);
return ModelToRepresentation.toRepresentation(session, realm, flow);
}
@GET
@ -684,7 +684,7 @@ public class TestingResourceProvider implements RealmResourceProvider {
RealmModel realm = getRealmByName(realmName);
AuthenticationFlowModel flow = realm.getResetCredentialsFlow();
if (flow == null) return null;
return ModelToRepresentation.toRepresentation(realm, flow);
return ModelToRepresentation.toRepresentation(session, realm, flow);
}
@GET

View file

@ -21,7 +21,12 @@ import static org.keycloak.common.Profile.Feature.SCRIPTS;
import static org.keycloak.testsuite.arquillian.DeploymentTargetModifier.AUTH_SERVER_CURRENT;
import java.io.IOException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import jakarta.ws.rs.BadRequestException;
import jakarta.ws.rs.core.Response;
import org.jboss.arquillian.container.test.api.Deployment;
@ -34,18 +39,23 @@ import org.junit.Assert;
import org.junit.BeforeClass;
import org.junit.Rule;
import org.junit.Test;
import org.keycloak.admin.client.resource.AuthenticationManagementResource;
import org.keycloak.authentication.authenticators.browser.ScriptBasedAuthenticatorFactory;
import org.keycloak.authentication.authenticators.browser.UsernamePasswordFormFactory;
import org.keycloak.events.Details;
import org.keycloak.events.Errors;
import org.keycloak.events.EventType;
import org.keycloak.models.AuthenticationExecutionModel;
import org.keycloak.representations.idm.AuthenticationExecutionInfoRepresentation;
import org.keycloak.representations.idm.AuthenticationExecutionRepresentation;
import org.keycloak.representations.idm.AuthenticationFlowRepresentation;
import org.keycloak.representations.idm.AuthenticatorConfigRepresentation;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.representations.provider.ScriptProviderDescriptor;
import org.keycloak.testsuite.AssertEvents;
import org.keycloak.testsuite.admin.ApiUtil;
import org.keycloak.testsuite.admin.authentication.AbstractAuthenticationTest;
import org.keycloak.testsuite.arquillian.annotation.DisableFeature;
import org.keycloak.testsuite.arquillian.annotation.EnableFeature;
import org.keycloak.testsuite.forms.AbstractFlowTest;
@ -180,6 +190,53 @@ public class DeployedScriptAuthenticatorTest extends AbstractFlowTest {
events.expectLogin().user(okayUser()).detail(Details.USERNAME, "user").assertEvent();
}
// Issue 20005
@Test
public void testManyScriptAuthenticatorInstances() throws Exception {
configureFlows();
AuthenticationManagementResource authMgmtResource = adminClient.realm(TEST_REALM_NAME).flows();
// Endpoint used by admin console
Map<String, String> scriptExecution = new HashMap<>();
scriptExecution.put("provider", "script-authenticator-a.js");
// It should be possible to add another script-authenticator to the flow
authMgmtResource.addExecution("scriptBrowser", scriptExecution);
List<AuthenticationExecutionInfoRepresentation> executions = authMgmtResource.getExecutions("scriptBrowser");
List<AuthenticationExecutionInfoRepresentation> scriptExecutions = executions.stream()
.filter(execution -> execution.getDisplayName().equals("My Authenticator"))
.collect(Collectors.toList());
// Both executions refers to same config of deployed script provider
Assert.assertEquals(2, scriptExecutions.size());
for (AuthenticationExecutionInfoRepresentation execution : scriptExecutions) {
Assert.assertEquals(execution.getAuthenticationConfig(), "script-authenticator-a.js");
}
// Assert updating config should fail due it's read-only
AuthenticatorConfigRepresentation configRep = authMgmtResource.getAuthenticatorConfig("script-authenticator-a.js");
configRep.getConfig().put("scriptCode", "Something");
try {
authMgmtResource.updateAuthenticatorConfig("script-authenticator-a.js", configRep);
Assert.fail("Update of configuration should have failed");
} catch (BadRequestException bre) {
// Expected
}
// Test copy flow is OK
Map<String, String> newFlow = new HashMap<>();
newFlow.put("newName", "Copy of script flow");
Response resp = authMgmtResource.copy("scriptBrowser", newFlow);
Assert.assertEquals(201, resp.getStatus());
resp.close();
AuthenticationFlowRepresentation copiedFlow = AbstractAuthenticationTest.findFlowByAlias("Copy of script flow", authMgmtResource.getFlows());
// Cleanup
authMgmtResource.deleteFlow(copiedFlow.getId());
authMgmtResource.removeExecution(scriptExecutions.get(1).getId());
}
private UserRepresentation okayUser() {
return adminClient.realm(TEST_REALM_NAME).users().search("user", true).get(0);
}

View file

@ -27,7 +27,8 @@ import static org.keycloak.models.utils.DefaultAuthenticationFlows.REGISTRATION_
import static org.keycloak.models.utils.DefaultAuthenticationFlows.RESET_CREDENTIALS_FLOW;
public class FlowUtil {
private RealmModel realm;
private final KeycloakSession session;
private final RealmModel realm;
private AuthenticationFlowModel currentFlow;
private String flowAlias;
private int maxPriority = 0;
@ -42,7 +43,8 @@ public class FlowUtil {
}
}
public FlowUtil(RealmModel realm) {
private FlowUtil(KeycloakSession session, RealmModel realm) {
this.session = session;
this.realm = realm;
}
@ -55,11 +57,11 @@ public class FlowUtil {
}
public static FlowUtil inCurrentRealm(KeycloakSession session) {
return new FlowUtil(session.getContext().getRealm());
return new FlowUtil(session, session.getContext().getRealm());
}
private FlowUtil newFlowUtil(AuthenticationFlowModel flowModel) {
FlowUtil subflow = new FlowUtil(realm);
FlowUtil subflow = new FlowUtil(session, realm);
subflow.currentFlow = flowModel;
return subflow;
}
@ -112,7 +114,7 @@ public class FlowUtil {
realm.removeAuthenticationFlow(foundFlow);
}
currentFlow = AuthenticationManagementResource.copyFlow(realm, existingBrowserFlow, newFlowAlias);
currentFlow = AuthenticationManagementResource.copyFlow(session, realm, existingBrowserFlow, newFlowAlias);
return this;
}