mirror of
https://github.com/keycloak/keycloak.git
synced 2026-02-03 20:39:33 -05:00
Reverting format changes, updating docs, and only exposing the method to fetch first-factor credentials
Signed-off-by: Pedro Igor <pigor.craveiro@gmail.com>
This commit is contained in:
parent
348670ae32
commit
c8a41dea99
5 changed files with 117 additions and 222 deletions
|
|
@ -35,6 +35,7 @@ In addition to identifying the user once the username is provided, the identity-
|
|||
* Matching an email domain to an organization.
|
||||
* Deciding if the authentication flow should continue or not if an account already exists for the username provided
|
||||
* Deciding how the user should be authenticated depending on how the domains and the identity providers are configured to an organization
|
||||
and the set of credentials configured to the user account.
|
||||
* Seamlessly authenticating users through an identity provider associated with an organization if the email domain matches the domain set to the identity provider
|
||||
|
||||
The identity-first login provides the same capabilities that are provided by the usual login page with the username and
|
||||
|
|
@ -65,7 +66,9 @@ For more details, see <<_managing_identity_provider_,Managing identity providers
|
|||
|
||||
In a similar situation to the previous section, the organization may have an identity provider set with one of the organization domains.
|
||||
In this situation, the user is redirected to the identity provider if that user's email matches a specific domain from the organization.
|
||||
Once the flow completes, an account is created and the user joins the organization.
|
||||
Once the flow completes, an account is created and the user joins the organization. In case the user has any first-factor credentials
|
||||
configured (e.g.: password, passwordless, kerberos) to the account, the user is not automatically redirected to the identity provider but asked to authenticate using their
|
||||
credentials.
|
||||
|
||||
== Configuring existing authentication flows
|
||||
|
||||
|
|
|
|||
|
|
@ -44,22 +44,24 @@ import io.opentelemetry.api.trace.StatusCode;
|
|||
*
|
||||
* @author Alexander Schwartz
|
||||
*/
|
||||
public class UserCredentialManager extends AbstractStorageManager<UserStorageProvider, UserStorageProviderModel>
|
||||
implements org.keycloak.models.UserCredentialManager {
|
||||
public class UserCredentialManager extends AbstractStorageManager<UserStorageProvider, UserStorageProviderModel> implements org.keycloak.models.UserCredentialManager {
|
||||
|
||||
private static final List<String> FIRST_FACTOR_CREDENTIAL_TYPES = List.of(
|
||||
PasswordCredentialModel.TYPE,
|
||||
CredentialModel.CLIENT_CERT,
|
||||
CredentialModel.KERBEROS,
|
||||
WebAuthnCredentialModel.TYPE_PASSWORDLESS
|
||||
);
|
||||
|
||||
private final UserModel user;
|
||||
private final KeycloakSession session;
|
||||
private final RealmModel realm;
|
||||
|
||||
/**
|
||||
* It is not recommended to use this method directly from your user-storage
|
||||
* providers! Please use
|
||||
* {@link org.keycloak.models.UserProvider#getUserCredentialManager(UserModel)
|
||||
* session.users().getUserCredentialManager(user)} instead.
|
||||
* It is not recommended to use this method directly from your user-storage providers! Please use {@link org.keycloak.models.UserProvider#getUserCredentialManager(UserModel) session.users().getUserCredentialManager(user)} instead.
|
||||
*/
|
||||
public UserCredentialManager(KeycloakSession session, RealmModel realm, UserModel user) {
|
||||
super(session, UserStorageProviderFactory.class, UserStorageProvider.class, UserStorageProviderModel::new,
|
||||
"user");
|
||||
super(session, UserStorageProviderFactory.class, UserStorageProvider.class, UserStorageProviderModel::new, "user");
|
||||
this.user = user;
|
||||
this.session = session;
|
||||
this.realm = realm;
|
||||
|
|
@ -75,8 +77,7 @@ public class UserCredentialManager extends AbstractStorageManager<UserStoragePro
|
|||
|
||||
if (user.isFederated()) {
|
||||
UserStorageProviderModel model = getStorageProviderModel(realm, user.getFederationLink());
|
||||
if (model == null || !model.isEnabled())
|
||||
return false;
|
||||
if (model == null || !model.isEnabled()) return false;
|
||||
|
||||
CredentialInputValidator validator = getStorageProviderInstance(model, CredentialInputValidator.class);
|
||||
if (validator != null) {
|
||||
|
|
@ -92,18 +93,15 @@ public class UserCredentialManager extends AbstractStorageManager<UserStoragePro
|
|||
|
||||
@Override
|
||||
public boolean updateCredential(CredentialInput input) {
|
||||
if (!StorageId.isLocalStorage(user.getId()))
|
||||
throwExceptionIfInvalidUser(user);
|
||||
if (!StorageId.isLocalStorage(user.getId())) throwExceptionIfInvalidUser(user);
|
||||
|
||||
if (user.isFederated()) {
|
||||
UserStorageProviderModel model = getStorageProviderModel(realm, user.getFederationLink());
|
||||
if (model == null || !model.isEnabled())
|
||||
return false;
|
||||
if (model == null || !model.isEnabled()) return false;
|
||||
|
||||
CredentialInputUpdater updater = getStorageProviderInstance(model, CredentialInputUpdater.class);
|
||||
if (updater != null && updater.supportsCredentialType(input.getType())) {
|
||||
if (updater.updateCredential(realm, user, input))
|
||||
return true;
|
||||
if (updater.updateCredential(realm, user, input)) return true;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -185,12 +183,10 @@ public class UserCredentialManager extends AbstractStorageManager<UserStoragePro
|
|||
|
||||
@Override
|
||||
public void disableCredentialType(String credentialType) {
|
||||
if (!StorageId.isLocalStorage(user.getId()))
|
||||
throwExceptionIfInvalidUser(user);
|
||||
if (!StorageId.isLocalStorage(user.getId())) throwExceptionIfInvalidUser(user);
|
||||
if (user.isFederated()) {
|
||||
UserStorageProviderModel model = getStorageProviderModel(realm, user.getFederationLink());
|
||||
if (model == null || !model.isEnabled())
|
||||
return;
|
||||
if (model == null || !model.isEnabled()) return;
|
||||
|
||||
CredentialInputUpdater updater = getStorageProviderInstance(model, CredentialInputUpdater.class);
|
||||
if (updater.supportsCredentialType(credentialType)) {
|
||||
|
|
@ -208,16 +204,14 @@ public class UserCredentialManager extends AbstractStorageManager<UserStoragePro
|
|||
Stream<String> types = Stream.empty();
|
||||
if (user.isFederated()) {
|
||||
UserStorageProviderModel model = getStorageProviderModel(realm, user.getFederationLink());
|
||||
if (model == null || !model.isEnabled())
|
||||
return types;
|
||||
if (model == null || !model.isEnabled()) return types;
|
||||
|
||||
CredentialInputUpdater updater = getStorageProviderInstance(model, CredentialInputUpdater.class);
|
||||
if (updater != null)
|
||||
types = updater.getDisableableCredentialTypesStream(realm, user);
|
||||
if (updater != null) types = updater.getDisableableCredentialTypesStream(realm, user);
|
||||
}
|
||||
|
||||
return Stream.concat(types, getCredentialProviders(session, CredentialInputUpdater.class)
|
||||
.flatMap(updater -> updater.getDisableableCredentialTypesStream(realm, user)))
|
||||
.flatMap(updater -> updater.getDisableableCredentialTypesStream(realm, user)))
|
||||
.distinct();
|
||||
}
|
||||
|
||||
|
|
@ -225,13 +219,10 @@ public class UserCredentialManager extends AbstractStorageManager<UserStoragePro
|
|||
public boolean isConfiguredFor(String type) {
|
||||
UserStorageCredentialConfigured userStorageConfigured = isConfiguredThroughUserStorage(realm, user, type);
|
||||
|
||||
// Check if we can rely just on userStorage to decide if credential is
|
||||
// configured for the user or not
|
||||
// Check if we can rely just on userStorage to decide if credential is configured for the user or not
|
||||
switch (userStorageConfigured) {
|
||||
case CONFIGURED:
|
||||
return true;
|
||||
case USER_STORAGE_DISABLED:
|
||||
return false;
|
||||
case CONFIGURED: return true;
|
||||
case USER_STORAGE_DISABLED: return false;
|
||||
}
|
||||
|
||||
// Check locally as a fallback
|
||||
|
|
@ -241,15 +232,13 @@ public class UserCredentialManager extends AbstractStorageManager<UserStoragePro
|
|||
@Override
|
||||
public boolean isConfiguredLocally(String type) {
|
||||
return getCredentialProviders(session, CredentialInputValidator.class)
|
||||
.anyMatch(validator -> validator.supportsCredentialType(type)
|
||||
&& validator.isConfiguredFor(realm, user, type));
|
||||
.anyMatch(validator -> validator.supportsCredentialType(type) && validator.isConfiguredFor(realm, user, type));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Stream<String> getConfiguredUserStorageCredentialTypesStream() {
|
||||
return getCredentialProviders(session, CredentialProvider.class).map(CredentialProvider::getType)
|
||||
.filter(credentialType -> UserStorageCredentialConfigured.CONFIGURED == isConfiguredThroughUserStorage(
|
||||
realm, user, credentialType));
|
||||
.filter(credentialType -> UserStorageCredentialConfigured.CONFIGURED == isConfiguredThroughUserStorage(realm, user, credentialType));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -270,16 +259,13 @@ public class UserCredentialManager extends AbstractStorageManager<UserStoragePro
|
|||
NOT_CONFIGURED
|
||||
}
|
||||
|
||||
private UserStorageCredentialConfigured isConfiguredThroughUserStorage(RealmModel realm, UserModel user,
|
||||
String type) {
|
||||
private UserStorageCredentialConfigured isConfiguredThroughUserStorage(RealmModel realm, UserModel user, String type) {
|
||||
if (user.isFederated()) {
|
||||
UserStorageProviderModel model = getStorageProviderModel(realm, user.getFederationLink());
|
||||
if (model == null || !model.isEnabled())
|
||||
return UserStorageCredentialConfigured.USER_STORAGE_DISABLED;
|
||||
if (model == null || !model.isEnabled()) return UserStorageCredentialConfigured.USER_STORAGE_DISABLED;
|
||||
|
||||
CredentialInputValidator validator = getStorageProviderInstance(model, CredentialInputValidator.class);
|
||||
if (validator != null && validator.supportsCredentialType(type)
|
||||
&& validator.isConfiguredFor(realm, user, type)) {
|
||||
if (validator != null && validator.supportsCredentialType(type) && validator.isConfiguredFor(realm, user, type)) {
|
||||
return UserStorageCredentialConfigured.CONFIGURED;
|
||||
}
|
||||
}
|
||||
|
|
@ -293,10 +279,9 @@ public class UserCredentialManager extends AbstractStorageManager<UserStoragePro
|
|||
return user.getServiceAccountClientLink() == null;
|
||||
}
|
||||
|
||||
private void validate(RealmModel realm, UserModel user, List<CredentialInput> toValidate,
|
||||
CredentialInputValidator validator) {
|
||||
private void validate(RealmModel realm, UserModel user, List<CredentialInput> toValidate, CredentialInputValidator validator) {
|
||||
toValidate.removeIf(input -> {
|
||||
if (validator.supportsCredentialType(input.getType())) {
|
||||
if(validator.supportsCredentialType(input.getType())) {
|
||||
return session.getProvider(TracingProvider.class).trace(validator.getClass(), "isValid", span -> {
|
||||
boolean valid = validator.isValid(realm, user, input);
|
||||
if (!valid) {
|
||||
|
|
@ -310,7 +295,7 @@ public class UserCredentialManager extends AbstractStorageManager<UserStoragePro
|
|||
}
|
||||
|
||||
private static <T> Stream<T> getCredentialProviders(KeycloakSession session, Class<T> type) {
|
||||
// noinspection unchecked
|
||||
//noinspection unchecked
|
||||
return session.getKeycloakSessionFactory().getProviderFactoriesStream(CredentialProvider.class)
|
||||
.filter(f -> Types.supports(type, f, CredentialProviderFactory.class))
|
||||
.map(f -> (T) session.getProvider(CredentialProvider.class, f.getId()));
|
||||
|
|
@ -333,17 +318,7 @@ public class UserCredentialManager extends AbstractStorageManager<UserStoragePro
|
|||
|
||||
@Override
|
||||
public Stream<CredentialModel> getFirstFactorCredentialsStream() {
|
||||
List<String> firstFactorTypes = getFirstFactorCredentialTypes();
|
||||
return getStoredCredentialsStream().filter(c -> firstFactorTypes.contains(c.getType()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> getFirstFactorCredentialTypes() {
|
||||
return List.of(
|
||||
PasswordCredentialModel.TYPE,
|
||||
CredentialModel.CLIENT_CERT,
|
||||
CredentialModel.KERBEROS,
|
||||
WebAuthnCredentialModel.TYPE_PASSWORDLESS
|
||||
);
|
||||
return getStoredCredentialsStream()
|
||||
.filter(c -> FIRST_FACTOR_CREDENTIAL_TYPES.contains(c.getType()));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -119,9 +119,4 @@ class EmptyCredentialManager implements SubjectCredentialManager {
|
|||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> getFirstFactorCredentialTypes() {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -138,13 +138,6 @@ public interface SubjectCredentialManager {
|
|||
return getStoredCredentialsStream();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of types for first-factor credentials.
|
||||
*
|
||||
* @return a list of first-factor credential types
|
||||
*/
|
||||
List<String> getFirstFactorCredentialTypes();
|
||||
|
||||
/**
|
||||
* Check if the credential type is configured for this entity.
|
||||
* @param type credential type to check
|
||||
|
|
|
|||
|
|
@ -17,28 +17,23 @@
|
|||
package org.keycloak.testsuite.organization.broker;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.keycloak.admin.client.resource.AuthenticationManagementResource;
|
||||
import org.keycloak.admin.client.resource.RealmResource;
|
||||
import org.keycloak.authentication.authenticators.broker.IdpCreateUserIfUniqueAuthenticatorFactory;
|
||||
import org.keycloak.credential.CredentialModel;
|
||||
import org.keycloak.admin.client.resource.UsersResource;
|
||||
import org.keycloak.models.AuthenticationExecutionModel;
|
||||
import org.keycloak.models.AuthenticationFlowModel;
|
||||
import org.keycloak.models.IdentityProviderModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.OrganizationModel;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.models.utils.DefaultAuthenticationFlows;
|
||||
import org.keycloak.organization.OrganizationProvider;
|
||||
import org.keycloak.models.credential.OTPCredentialModel;
|
||||
import org.keycloak.organization.utils.Organizations;
|
||||
import org.keycloak.representations.idm.AuthenticationExecutionInfoRepresentation;
|
||||
import org.keycloak.representations.idm.AuthenticatorConfigRepresentation;
|
||||
import org.keycloak.representations.idm.CredentialRepresentation;
|
||||
import org.keycloak.representations.idm.IdentityProviderRepresentation;
|
||||
import org.keycloak.representations.idm.OrganizationDomainRepresentation;
|
||||
import org.keycloak.representations.idm.OrganizationRepresentation;
|
||||
import org.keycloak.representations.idm.RealmRepresentation;
|
||||
import org.keycloak.representations.idm.UserRepresentation;
|
||||
import org.keycloak.testsuite.AssertEvents;
|
||||
import org.keycloak.testsuite.broker.AbstractBrokerTest;
|
||||
import org.keycloak.testsuite.broker.AbstractInitializedBaseBrokerTest;
|
||||
|
|
@ -47,12 +42,16 @@ import org.keycloak.testsuite.pages.LoginTotpPage;
|
|||
import org.keycloak.testsuite.runonserver.RunOnServer;
|
||||
import org.keycloak.testsuite.util.AccountHelper;
|
||||
|
||||
import org.hamcrest.Matchers;
|
||||
import org.jboss.arquillian.graphene.page.Page;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
|
||||
import static org.keycloak.testsuite.broker.BrokerTestTools.waitForPage;
|
||||
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.hamcrest.Matchers.equalTo;
|
||||
|
||||
/**
|
||||
* Tests interaction between organization linked identity provider (redirect on email domain match + hide on login)
|
||||
* and post-broker login flow which requires OTP setup. Ensures that after OTP is configured the user WILL be
|
||||
|
|
@ -66,7 +65,7 @@ public class OrganizationPostBrokerLoginTest extends AbstractInitializedBaseBrok
|
|||
private final String ORG_ALIAS = "org-post-broker";
|
||||
|
||||
@Page
|
||||
private LoginConfigTotpPage totpPage;
|
||||
private LoginConfigTotpPage totpConfigPage;
|
||||
|
||||
@Page
|
||||
private LoginTotpPage loginTotpPage;
|
||||
|
|
@ -108,159 +107,89 @@ public class OrganizationPostBrokerLoginTest extends AbstractInitializedBaseBrok
|
|||
consumerRealm.identityProviders().get(bc.getIDPAlias()).update(idp);
|
||||
|
||||
// 3) Configure post-broker login flow for this idp to require OTP setup
|
||||
final String idpAlias = bc.getIDPAlias();
|
||||
|
||||
String idpAlias = bc.getIDPAlias();
|
||||
testingClient.server(bc.consumerRealmName()).run(new ConfigureOtpPostBrokerFlow(idpAlias));
|
||||
|
||||
// Disable update profile prompts so we can reach the post-broker OTP flow directly
|
||||
updateExecutions(AbstractBrokerTest::disableUpdateProfileOnFirstLogin);
|
||||
|
||||
try {
|
||||
// 4) First login via broker, run post-broker OTP setup
|
||||
oauth.clientId("broker-app");
|
||||
loginPage.open(bc.consumerRealmName());
|
||||
// 4) First login via broker, run post-broker OTP setup
|
||||
oauth.clientId("broker-app");
|
||||
loginPage.open(bc.consumerRealmName());
|
||||
logInWithBroker(bc);
|
||||
|
||||
logInWithBroker(bc);
|
||||
// Post broker flow should require TOTPs
|
||||
totpPage.assertCurrent();
|
||||
final String totpSecret = totpPage.getTotpSecret();
|
||||
totpPage.configure(totp.generateTOTP(totpSecret));
|
||||
// Post broker flow should require TOTPs
|
||||
totpConfigPage.assertCurrent();
|
||||
String totpSecret = totpConfigPage.getTotpSecret();
|
||||
totpConfigPage.configure(totp.generateTOTP(totpSecret));
|
||||
UsersResource usersApi = realmsResouce().realm(bc.consumerRealmName()).users();
|
||||
List<UserRepresentation> users = usersApi.search(bc.getUserLogin(), true);
|
||||
assertThat(1, equalTo(users.size()));
|
||||
UserRepresentation user = users.get(0);
|
||||
List<CredentialRepresentation> credentials = usersApi.get(user.getId()).credentials();
|
||||
assertThat("Expected exactly one credential after TOTP setup", credentials.size(), equalTo(1));
|
||||
assertThat("Expected TOTP credential type after setup", credentials.get(0).getType(), equalTo(OTPCredentialModel.TYPE));
|
||||
|
||||
testingClient.server(bc.consumerRealmName()).run(new AssertNoFirstFactorCredentials(bc.consumerRealmName(), bc.getUserLogin()));
|
||||
IdentityProviderRepresentation postTotpIdp = consumerRealm.identityProviders().get(idp.getAlias()).toRepresentation();
|
||||
postTotpIdp.getConfig().put(OrganizationModel.ORGANIZATION_DOMAIN_ATTRIBUTE, orgDomain);
|
||||
postTotpIdp.getConfig().put(OrganizationModel.IdentityProviderRedirectMode.EMAIL_MATCH.getKey(), Boolean.TRUE.toString());
|
||||
postTotpIdp.setHideOnLogin(true);
|
||||
consumerRealm.identityProviders().get(postTotpIdp.getAlias()).update(postTotpIdp);
|
||||
consumerRealm.organizations().get(orgId).identityProviders().addIdentityProvider(postTotpIdp.getAlias()).close();
|
||||
|
||||
IdentityProviderRepresentation postTotpIdp = consumerRealm.identityProviders().get(idp.getAlias()).toRepresentation();
|
||||
postTotpIdp.getConfig().put(OrganizationModel.ORGANIZATION_DOMAIN_ATTRIBUTE, orgDomain);
|
||||
postTotpIdp.getConfig().put(OrganizationModel.IdentityProviderRedirectMode.EMAIL_MATCH.getKey(), Boolean.TRUE.toString());
|
||||
postTotpIdp.setHideOnLogin(true);
|
||||
consumerRealm.identityProviders().get(postTotpIdp.getAlias()).update(postTotpIdp);
|
||||
consumerRealm.organizations().get(orgId).identityProviders().addIdentityProvider(postTotpIdp.getAlias()).close();
|
||||
AccountHelper.logout(adminClient.realm(bc.consumerRealmName()), bc.getUserLogin());
|
||||
// 5) Try re-login: user SHOULD be automatically redirected to the identity provider
|
||||
oauth.clientId("broker-app");
|
||||
loginPage.open(bc.consumerRealmName());
|
||||
// submit username/email to trigger organization resolution which should redirect to the provider
|
||||
loginPage.loginUsername(bc.getUserEmail());
|
||||
|
||||
testingClient.server(bc.consumerRealmName()).run(new LogOrganizationState(bc.consumerRealmName(), orgId, bc.getUserLogin(), orgDomain));
|
||||
// wait for provider login page (title contains "sign in to <realm>")
|
||||
waitForPage(driver, "sign in to", true);
|
||||
assertThat("Driver should be on the provider realm page right now",
|
||||
driver.getCurrentUrl(), Matchers.containsString("/auth/realms/" + bc.providerRealmName() + "/"));
|
||||
String landingUrl = driver.getCurrentUrl();
|
||||
String landingTitle = driver.getTitle();
|
||||
log.infof("Organization broker re-login landed on page title='%s' url='%s' providerRealName='%s'", landingTitle, landingUrl, bc.providerRealmName());
|
||||
loginPage.login(bc.getUserPassword());
|
||||
|
||||
AccountHelper.logout(adminClient.realm(bc.consumerRealmName()), bc.getUserLogin());
|
||||
// AccountHelper.logout(adminClient.realm(bc.providerRealmName()), bc.getUserLogin());
|
||||
|
||||
// 5) Try re-login: user SHOULD be automatically redirected to the identity provider
|
||||
oauth.clientId("broker-app");
|
||||
loginPage.open(bc.consumerRealmName());
|
||||
|
||||
// submit username/email to trigger organization resolution which should redirect to the provider
|
||||
loginPage.loginUsername(bc.getUserEmail());
|
||||
|
||||
// provide OTP code required on re-login for the configured credential
|
||||
if (loginTotpPage.isCurrent()) {
|
||||
log.infof("Submitting TOTP on re-login");
|
||||
loginTotpPage.login(totp.generateTOTP(totpSecret));
|
||||
}
|
||||
|
||||
// wait for provider login page (title contains "sign in to <realm>")
|
||||
waitForPage(driver, "sign in to", true);
|
||||
|
||||
String landingUrl = driver.getCurrentUrl();
|
||||
String landingTitle = driver.getTitle();
|
||||
log.infof("Organization broker re-login landed on page title='%s' url='%s' providerRealName='%s'", landingTitle, landingUrl, bc.providerRealmName());
|
||||
|
||||
// check we are on provider realm login page (redirected to IDP)
|
||||
org.junit.Assert.assertTrue(landingUrl.contains("/auth/realms/" + bc.providerRealmName()));
|
||||
} finally {
|
||||
updateExecutions(OrganizationPostBrokerLoginTest::restoreDefaultFirstBrokerLoginConfig);
|
||||
}
|
||||
// provide OTP code required on re-login for the configured credential
|
||||
assertThat("Driver should be on the consumer realm page right now",
|
||||
driver.getCurrentUrl(), Matchers.containsString("/auth/realms/" + bc.consumerRealmName() + "/"));
|
||||
loginTotpPage.assertCurrent();
|
||||
}
|
||||
|
||||
private static void restoreDefaultFirstBrokerLoginConfig(AuthenticationExecutionInfoRepresentation execution,
|
||||
AuthenticationManagementResource flows) {
|
||||
if (execution.getProviderId() != null && execution.getProviderId().equals(IdpCreateUserIfUniqueAuthenticatorFactory.PROVIDER_ID)) {
|
||||
execution.setRequirement(AuthenticationExecutionModel.Requirement.ALTERNATIVE.name());
|
||||
flows.updateExecutions(DefaultAuthenticationFlows.FIRST_BROKER_LOGIN_FLOW, execution);
|
||||
} else if (execution.getAlias() != null && execution.getAlias().equals(DefaultAuthenticationFlows.IDP_REVIEW_PROFILE_CONFIG_ALIAS)) {
|
||||
AuthenticatorConfigRepresentation config = flows.getAuthenticatorConfig(execution.getAuthenticationConfig());
|
||||
config.getConfig().put("update.profile.on.first.login", IdentityProviderRepresentation.UPFLM_MISSING);
|
||||
flows.updateAuthenticatorConfig(config.getId(), config);
|
||||
private static class ConfigureOtpPostBrokerFlow implements RunOnServer {
|
||||
private final String idpAlias;
|
||||
|
||||
private ConfigureOtpPostBrokerFlow(String idpAlias) {
|
||||
this.idpAlias = idpAlias;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run(KeycloakSession session) {
|
||||
RealmModel realm = session.getContext().getRealm();
|
||||
|
||||
// Build dedicated post-broker flow that enforces OTP setup before redirect
|
||||
AuthenticationFlowModel postBrokerFlow = new AuthenticationFlowModel();
|
||||
postBrokerFlow.setAlias("post-broker");
|
||||
postBrokerFlow.setDescription("post-broker flow with OTP");
|
||||
postBrokerFlow.setProviderId("basic-flow");
|
||||
postBrokerFlow.setTopLevel(true);
|
||||
postBrokerFlow.setBuiltIn(false);
|
||||
postBrokerFlow = realm.addAuthenticationFlow(postBrokerFlow);
|
||||
|
||||
AuthenticationExecutionModel execution = new AuthenticationExecutionModel();
|
||||
execution.setParentFlow(postBrokerFlow.getId());
|
||||
execution.setRequirement(AuthenticationExecutionModel.Requirement.REQUIRED);
|
||||
execution.setAuthenticator("auth-otp-form");
|
||||
execution.setPriority(20);
|
||||
execution.setAuthenticatorFlow(false);
|
||||
realm.addAuthenticatorExecution(execution);
|
||||
|
||||
IdentityProviderModel idpModel = session.identityProviders().getByAlias(idpAlias);
|
||||
idpModel.setPostBrokerLoginFlowId(postBrokerFlow.getId());
|
||||
session.identityProviders().update(idpModel);
|
||||
}
|
||||
}
|
||||
|
||||
private static class ConfigureOtpPostBrokerFlow implements RunOnServer {
|
||||
private final String idpAlias;
|
||||
|
||||
private ConfigureOtpPostBrokerFlow(String idpAlias) {
|
||||
this.idpAlias = idpAlias;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run(KeycloakSession session) {
|
||||
RealmModel realm = session.getContext().getRealm();
|
||||
|
||||
// Build dedicated post-broker flow that enforces OTP setup before redirect
|
||||
AuthenticationFlowModel postBrokerFlow = new AuthenticationFlowModel();
|
||||
postBrokerFlow.setAlias("post-broker");
|
||||
postBrokerFlow.setDescription("post-broker flow with OTP");
|
||||
postBrokerFlow.setProviderId("basic-flow");
|
||||
postBrokerFlow.setTopLevel(true);
|
||||
postBrokerFlow.setBuiltIn(false);
|
||||
postBrokerFlow = realm.addAuthenticationFlow(postBrokerFlow);
|
||||
|
||||
AuthenticationExecutionModel execution = new AuthenticationExecutionModel();
|
||||
execution.setParentFlow(postBrokerFlow.getId());
|
||||
execution.setRequirement(AuthenticationExecutionModel.Requirement.REQUIRED);
|
||||
execution.setAuthenticator("auth-otp-form");
|
||||
execution.setPriority(20);
|
||||
execution.setAuthenticatorFlow(false);
|
||||
realm.addAuthenticatorExecution(execution);
|
||||
|
||||
IdentityProviderModel idpModel = session.identityProviders().getByAlias(idpAlias);
|
||||
idpModel.setPostBrokerLoginFlowId(postBrokerFlow.getId());
|
||||
session.identityProviders().update(idpModel);
|
||||
}
|
||||
}
|
||||
|
||||
private static class AssertNoFirstFactorCredentials implements RunOnServer {
|
||||
private final String realmName;
|
||||
private final String username;
|
||||
|
||||
private AssertNoFirstFactorCredentials(String realmName, String username) {
|
||||
this.realmName = realmName;
|
||||
this.username = username;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run(KeycloakSession session) {
|
||||
RealmModel realm = session.realms().getRealmByName(realmName);
|
||||
UserModel user = session.users().getUserByUsername(realm, username);
|
||||
boolean hasFirstFactor = user.credentialManager().getFirstFactorCredentialsStream().findAny().isPresent();
|
||||
org.junit.Assert.assertFalse("Unexpected first-factor credentials present after OTP setup", hasFirstFactor);
|
||||
}
|
||||
}
|
||||
|
||||
private static class LogOrganizationState implements RunOnServer {
|
||||
private final String realmName;
|
||||
private final String organizationId;
|
||||
private final String username;
|
||||
private final String expectedDomain;
|
||||
|
||||
private LogOrganizationState(String realmName, String organizationId, String username, String expectedDomain) {
|
||||
this.realmName = realmName;
|
||||
this.organizationId = organizationId;
|
||||
this.username = username;
|
||||
this.expectedDomain = expectedDomain;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run(KeycloakSession session) {
|
||||
RealmModel realm = session.realms().getRealmByName(realmName);
|
||||
OrganizationProvider provider = session.getProvider(OrganizationProvider.class);
|
||||
OrganizationModel organization = provider.getById(organizationId);
|
||||
UserModel user = session.users().getUserByUsername(realm, username);
|
||||
|
||||
boolean managedMember = organization != null && user != null && organization.isManaged(user);
|
||||
List<String> idpInfo = organization == null ? List.of() : organization.getIdentityProviders()
|
||||
.map(broker -> broker.getAlias() + "|domain=" + broker.getConfig().get(OrganizationModel.ORGANIZATION_DOMAIN_ATTRIBUTE) +
|
||||
"|emailMatch=" + broker.getConfig().get(OrganizationModel.IdentityProviderRedirectMode.EMAIL_MATCH.getKey()))
|
||||
.collect(Collectors.toList());
|
||||
List<String> credentialTypes = user == null ? List.of() : user.credentialManager().getStoredCredentialsStream()
|
||||
.map(CredentialModel::getType)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
System.out.printf("[OrgState] managed=%s expectedDomain=%s idps=%s credentials=%s%n", managedMember, expectedDomain, idpInfo, credentialTypes);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue