mirror of
https://github.com/keycloak/keycloak.git
synced 2026-01-09 23:12:06 -03:30
Make max auth age configurable for all required actions by default
Moved the current configuration implementation for the update password Closes #39408 Signed-off-by: Kai Josef Witt <KWitt@vhv.de> Signed-off-by: Marek Posolda <mposolda@gmail.com> Co-authored-by: Kai Josef Witt <KWitt@vhv.de> Co-authored-by: Marek Posolda <mposolda@gmail.com>
This commit is contained in:
parent
f7277315e3
commit
c76bb0683c
@ -45,6 +45,9 @@ by checking the claims like `acr` in the tokens.
|
||||
In case the user is already authenticated due to an active SSO session, that user usually does not need to actively re-authenticate. However, if that user actively authenticated longer than five minutes ago,
|
||||
the client can still request re-authentication when some AIA is requested. Exceptions exist from this guideline as follows:
|
||||
|
||||
* For every required action it is possible to configure the max age on the required action itself in the <<proc-setting-default-required-actions_{context}, Required actions tab>>.
|
||||
If the policy is not configured, it defaults to five minutes.
|
||||
|
||||
* The action `delete_account` will always require the user to actively re-authenticate
|
||||
|
||||
* The action `UPDATE_PASSWORD` might require the user to actively re-authenticate according to the configured <<maximum-authentication-age,Maximum Authentication Age Password policy>>.
|
||||
@ -52,7 +55,7 @@ In case the policy is not configured, it is also possible to configure it on the
|
||||
when configuring the particular required action. If the policy is not configured in any of those places, it defaults to five minutes.
|
||||
|
||||
* If you want to use a shorter re-authentication, you can still use a parameter query parameter such as `max_age` with the specified shorter value or eventually `prompt=login`, which will always require user to
|
||||
actively re-authenticate as described in the OIDC specification. Note that using `max_age` for a longer value than the default five minutes (or the one prescribed by password policy) is not supported.
|
||||
actively re-authenticate as described in the OIDC specification. Note that using `max_age` for a longer value than the default five minutes (or the one specifically configured for the required action) is not supported.
|
||||
The `max_age` can be currently used only to make the value shorter than the default five minutes.
|
||||
|
||||
* If <<_step-up-flow,Step-up authentication>> is enabled and the action is to add or delete a credential, authentication is required with the level corresponding
|
||||
|
||||
@ -56,6 +56,10 @@ Both system properties have been used internally within Keycloak and have not be
|
||||
Instead, use the command line option `spi-cache-embedded-default-site-name` as `jboss.site.name` replacement, and `spi-cache-embedded-default-node-name` as `jboss.node.name` replacement.
|
||||
See the https://www.keycloak.org/server/all-provider-config[All provider configuration] for more details on these options.
|
||||
|
||||
|
||||
=== Deprecation of `method RequiredActionProvider.getMaxAuthAge()`
|
||||
The method `RequiredActionProvider.getMaxAuthAge()` is deprecated. It is effectively not used now. Please use the method `RequiredActionProvider.getMaxAuthAge(KeycloakSession session)` instead. This is due to enable individual configuration for required actions.
|
||||
|
||||
=== User searches through the User API are now respecting the user profile settings
|
||||
|
||||
When querying users through the User API, the user representation and their attributes are now taking into account the
|
||||
|
||||
@ -17,11 +17,16 @@
|
||||
|
||||
package org.keycloak.authentication;
|
||||
|
||||
import org.keycloak.models.Constants;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.RequiredActionConfigModel;
|
||||
import org.keycloak.policy.MaxAuthAgePasswordPolicyProviderFactory;
|
||||
import org.keycloak.provider.ProviderConfigProperty;
|
||||
import org.keycloak.provider.ProviderConfigurationBuilder;
|
||||
import org.keycloak.provider.ProviderFactory;
|
||||
import org.keycloak.userprofile.ValidationException;
|
||||
import org.keycloak.validate.ValidationError;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@ -33,6 +38,18 @@ import java.util.List;
|
||||
*/
|
||||
public interface RequiredActionFactory extends ProviderFactory<RequiredActionProvider> {
|
||||
|
||||
List<ProviderConfigProperty> MAX_AUTH_AGE_CONFIG_PROPERTIES = ProviderConfigurationBuilder.create()
|
||||
.property()
|
||||
.name(Constants.MAX_AUTH_AGE_KEY)
|
||||
.label("Maximum Age of Authentication")
|
||||
.helpText("Configures the duration in seconds this action can be used after the last authentication before the user is required to re-authenticate. " +
|
||||
"This parameter is used just in the context of AIA when the kc_action parameter is available in the request, which is for instance when user " +
|
||||
"himself updates his password in the account console.")
|
||||
.type(ProviderConfigProperty.STRING_TYPE)
|
||||
.defaultValue(MaxAuthAgePasswordPolicyProviderFactory.DEFAULT_MAX_AUTH_AGE)
|
||||
.add()
|
||||
.build();
|
||||
|
||||
/**
|
||||
* Display text used in admin console to reference this required action
|
||||
*
|
||||
@ -58,13 +75,37 @@ public interface RequiredActionFactory extends ProviderFactory<RequiredActionPro
|
||||
return configMetadata != null && !configMetadata.isEmpty();
|
||||
}
|
||||
|
||||
@Override
|
||||
default List<ProviderConfigProperty> getConfigMetadata() {
|
||||
return List.copyOf(MAX_AUTH_AGE_CONFIG_PROPERTIES);
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows users to validate the provided configuration for this required action. Users can throw a {@link org.keycloak.models.ModelValidationException} to indicate that the configuration is invalid.
|
||||
* Defaults validating max_auth_age value.
|
||||
*
|
||||
* @param session
|
||||
* @param realm
|
||||
* @param model
|
||||
*/
|
||||
default void validateConfig(KeycloakSession session, RealmModel realm, RequiredActionConfigModel model) {
|
||||
if (model.getConfigValue(Constants.MAX_AUTH_AGE_KEY) == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
int parsedMaxAuthAge;
|
||||
try {
|
||||
parsedMaxAuthAge = parseMaxAuthAge(model);
|
||||
} catch (NumberFormatException ex) {
|
||||
throw new ValidationException(new ValidationError(getId(), Constants.MAX_AUTH_AGE_KEY, "error-invalid-value"));
|
||||
}
|
||||
|
||||
if (parsedMaxAuthAge < 0) {
|
||||
throw new ValidationException(new ValidationError(getId(), Constants.MAX_AUTH_AGE_KEY, "error-number-out-of-range-too-small", 0));
|
||||
}
|
||||
}
|
||||
|
||||
static int parseMaxAuthAge(RequiredActionConfigModel model) throws NumberFormatException {
|
||||
return Integer.parseInt(model.getConfigValue(Constants.MAX_AUTH_AGE_KEY));
|
||||
}
|
||||
}
|
||||
|
||||
@ -18,9 +18,16 @@
|
||||
package org.keycloak.authentication;
|
||||
|
||||
import org.keycloak.models.Constants;
|
||||
import org.keycloak.models.KeycloakContext;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.RequiredActionConfigModel;
|
||||
import org.keycloak.models.RequiredActionProviderModel;
|
||||
import org.keycloak.provider.Provider;
|
||||
import org.keycloak.sessions.AuthenticationSessionModel;
|
||||
import org.keycloak.utils.RequiredActionHelper;
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* RequiredAction provider. Required actions are one-time actions that a user must perform before they are logged in.
|
||||
@ -29,6 +36,7 @@ import org.keycloak.sessions.AuthenticationSessionModel;
|
||||
* @version $Revision: 1 $
|
||||
*/
|
||||
public interface RequiredActionProvider extends Provider {
|
||||
|
||||
/**
|
||||
* Determines what type of support is provided for application-initiated
|
||||
* actions.
|
||||
@ -77,9 +85,53 @@ public interface RequiredActionProvider extends Provider {
|
||||
*/
|
||||
void processAction(RequiredActionContext context);
|
||||
|
||||
|
||||
/**
|
||||
* @deprecated in favor of {@link #getMaxAuthAge(KeycloakSession)} to support individual configuration of max auth age for all required actions. This method has no effect anymore.
|
||||
*
|
||||
* Defines the max time after a user login, after which re-authentication is requested for an AIA. 0 means that re-authentication is always requested.
|
||||
* On default uses configured max_auth_age value from the required action config. If not configured, it uses the default max_auth_age value from the KeycloakConstants class.
|
||||
*/
|
||||
@Deprecated(since = "26.3.0", forRemoval = true)
|
||||
default int getMaxAuthAge() {
|
||||
return Constants.KC_ACTION_MAX_AGE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Defines the max time after a user login, after which re-authentication is requested for an AIA. 0 means that re-authentication is always requested.
|
||||
*
|
||||
* On default uses configured max_auth_age value from the required action config. If not configured, it uses the default max_auth_age value from the KeycloakConstants class.
|
||||
*/
|
||||
default int getMaxAuthAge() { return Constants.KC_ACTION_MAX_AGE; }
|
||||
default int getMaxAuthAge(KeycloakSession session) {
|
||||
if (session == null) {
|
||||
// session is null, support for legacy implementation, fallback to default maxAuthAge
|
||||
return Constants.KC_ACTION_MAX_AGE;
|
||||
}
|
||||
|
||||
KeycloakContext keycloakContext = session.getContext();
|
||||
RealmModel realm = keycloakContext.getRealm();
|
||||
int maxAge;
|
||||
|
||||
// try required action config
|
||||
AuthenticationSessionModel authSession = keycloakContext.getAuthenticationSession();
|
||||
if (authSession != null) {
|
||||
|
||||
// we need to figure out the alias for the current required action
|
||||
String providerId = authSession.getClientNote(Constants.KC_ACTION);
|
||||
RequiredActionProviderModel requiredAction = RequiredActionHelper.getRequiredActionByProviderId(realm, providerId);
|
||||
|
||||
if (requiredAction != null) {
|
||||
RequiredActionConfigModel configModel = realm.getRequiredActionConfigByAlias(requiredAction.getAlias());
|
||||
if (configModel != null && configModel.containsConfigKey(Constants.MAX_AUTH_AGE_KEY)) {
|
||||
maxAge = RequiredActionFactory.parseMaxAuthAge(configModel);
|
||||
if (maxAge >= 0) {
|
||||
return maxAge;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// fallback to default
|
||||
return Constants.KC_ACTION_MAX_AGE;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -95,6 +95,8 @@ public final class Constants {
|
||||
*/
|
||||
public static final String KC_ACTION_ENFORCED = "kc_action_enforced";
|
||||
public static final int KC_ACTION_MAX_AGE = 300;
|
||||
public static final String MAX_AUTH_AGE_KEY = "max_auth_age";
|
||||
|
||||
|
||||
public static final String IS_AIA_REQUEST = "IS_AIA_REQUEST";
|
||||
public static final String AIA_SILENT_CANCEL = "silent_cancel";
|
||||
|
||||
@ -17,10 +17,7 @@
|
||||
|
||||
package org.keycloak.authentication.requiredactions;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
import jakarta.ws.rs.ForbiddenException;
|
||||
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.Config;
|
||||
import org.keycloak.authentication.AuthenticationProcessor;
|
||||
@ -41,11 +38,15 @@ import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.RoleModel;
|
||||
import org.keycloak.models.UserManager;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.provider.ProviderConfigProperty;
|
||||
import org.keycloak.services.managers.AuthenticationManager;
|
||||
import org.keycloak.services.managers.AuthenticationSessionManager;
|
||||
import org.keycloak.services.messages.Messages;
|
||||
import org.keycloak.sessions.AuthenticationSessionModel;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
public class DeleteAccount implements RequiredActionProvider, RequiredActionFactory {
|
||||
|
||||
public static final String PROVIDER_ID = "delete_account";
|
||||
@ -54,7 +55,7 @@ public class DeleteAccount implements RequiredActionProvider, RequiredActionFact
|
||||
|
||||
private static final Logger logger = Logger.getLogger(DeleteAccount.class);
|
||||
|
||||
@Override
|
||||
@Override
|
||||
public String getDisplayText() {
|
||||
return "Delete Account";
|
||||
}
|
||||
@ -184,10 +185,15 @@ public class DeleteAccount implements RequiredActionProvider, RequiredActionFact
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getMaxAuthAge() {
|
||||
public int getMaxAuthAge(KeycloakSession session) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ProviderConfigProperty> getConfigMetadata() {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
private void removeAuthenticationSession(RequiredActionContext context, KeycloakSession session) {
|
||||
AuthenticationSessionModel authSession = context.getAuthenticationSession();
|
||||
new AuthenticationSessionManager(session).removeAuthenticationSession(authSession.getRealm(), authSession, true);
|
||||
|
||||
@ -1,8 +1,7 @@
|
||||
package org.keycloak.authentication.requiredactions;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import jakarta.ws.rs.core.MultivaluedMap;
|
||||
import jakarta.ws.rs.core.Response;
|
||||
import org.keycloak.Config;
|
||||
import org.keycloak.authentication.AuthenticatorUtil;
|
||||
import org.keycloak.authentication.CredentialRegistrator;
|
||||
@ -22,15 +21,16 @@ import org.keycloak.models.RequiredActionConfigModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.models.credential.RecoveryAuthnCodesCredentialModel;
|
||||
import org.keycloak.provider.EnvironmentDependentProviderFactory;
|
||||
|
||||
import jakarta.ws.rs.core.MultivaluedMap;
|
||||
import jakarta.ws.rs.core.Response;
|
||||
import org.keycloak.provider.ProviderConfigProperty;
|
||||
import org.keycloak.provider.ProviderConfigurationBuilder;
|
||||
import org.keycloak.sessions.AuthenticationSessionModel;
|
||||
import org.keycloak.userprofile.ValidationException;
|
||||
import org.keycloak.validate.ValidationError;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static org.keycloak.utils.CredentialHelper.createRecoveryCodesCredential;
|
||||
|
||||
public class RecoveryAuthnCodesAction implements RequiredActionProvider, RequiredActionFactory, EnvironmentDependentProviderFactory, CredentialRegistrator {
|
||||
@ -160,11 +160,15 @@ public class RecoveryAuthnCodesAction implements RequiredActionProvider, Require
|
||||
|
||||
@Override
|
||||
public List<ProviderConfigProperty> getConfigMetadata() {
|
||||
return List.copyOf(CONFIG_PROPERTIES);
|
||||
return Stream.concat(
|
||||
List.copyOf(CONFIG_PROPERTIES).stream(),
|
||||
RequiredActionFactory.super.getConfigMetadata().stream()
|
||||
).toList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void validateConfig(KeycloakSession session, RealmModel realm, RequiredActionConfigModel model) {
|
||||
RequiredActionFactory.super.validateConfig(session, realm, model);
|
||||
|
||||
int parsedMaxAuthAge;
|
||||
try {
|
||||
|
||||
@ -41,21 +41,14 @@ import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.KeycloakSessionFactory;
|
||||
import org.keycloak.models.ModelException;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.RequiredActionConfigModel;
|
||||
import org.keycloak.models.RequiredActionProviderModel;
|
||||
import org.keycloak.models.UserCredentialModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.models.credential.PasswordCredentialModel;
|
||||
import org.keycloak.models.utils.FormMessage;
|
||||
import org.keycloak.policy.MaxAuthAgePasswordPolicyProviderFactory;
|
||||
import org.keycloak.provider.ProviderConfigProperty;
|
||||
import org.keycloak.provider.ProviderConfigurationBuilder;
|
||||
import org.keycloak.services.messages.Messages;
|
||||
import org.keycloak.services.validation.Validation;
|
||||
import org.keycloak.sessions.AuthenticationSessionModel;
|
||||
import org.keycloak.userprofile.ValidationException;
|
||||
import org.keycloak.utils.RequiredActionHelper;
|
||||
import org.keycloak.validate.ValidationError;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
@ -68,45 +61,11 @@ public class UpdatePassword implements RequiredActionProvider, RequiredActionFac
|
||||
|
||||
private static final Logger logger = Logger.getLogger(UpdatePassword.class);
|
||||
|
||||
private static final List<ProviderConfigProperty> CONFIG_PROPERTIES;
|
||||
|
||||
public static final String MAX_AUTH_AGE_KEY = "max_auth_age";
|
||||
|
||||
static {
|
||||
List<ProviderConfigProperty> properties = ProviderConfigurationBuilder.create() //
|
||||
.property() //
|
||||
.name(MAX_AUTH_AGE_KEY) //
|
||||
.label("Maximum Age of Authentication") //
|
||||
.helpText("Configures the duration in seconds this action can be used after the last authentication before the user is required to re-authenticate. " + //
|
||||
"This parameter is used just in the context of AIA when the kc_action parameter is available in the request, which is for instance when user " + //
|
||||
"himself updates his password in the account console. When the 'Maximum Authentication Age' password policy is used in the realm, it's value has " + //
|
||||
"precedence over the value configured here.") //
|
||||
.type(ProviderConfigProperty.STRING_TYPE) //
|
||||
.defaultValue(MaxAuthAgePasswordPolicyProviderFactory.DEFAULT_MAX_AUTH_AGE) //
|
||||
.add() //
|
||||
.build();
|
||||
|
||||
CONFIG_PROPERTIES = properties;
|
||||
}
|
||||
|
||||
private final KeycloakSession session;
|
||||
|
||||
@Override
|
||||
public InitiatedActionSupport initiatedActionSupport() {
|
||||
return InitiatedActionSupport.SUPPORTED;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated use {@link #UpdatePassword(KeycloakSession)} instead
|
||||
*/
|
||||
@Deprecated
|
||||
public UpdatePassword() {
|
||||
this(null);
|
||||
}
|
||||
|
||||
public UpdatePassword(KeycloakSession session) {
|
||||
this.session = session;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void evaluateTriggers(RequiredActionContext context) {
|
||||
@ -197,7 +156,6 @@ public class UpdatePassword implements RequiredActionProvider, RequiredActionFac
|
||||
.setError(me.getMessage(), me.getParameters())
|
||||
.createResponse(UserModel.RequiredAction.UPDATE_PASSWORD);
|
||||
context.challenge(challenge);
|
||||
return;
|
||||
} catch (Exception ape) {
|
||||
errorEvent.detail(Details.REASON, ape.getMessage()).error(Errors.PASSWORD_REJECTED);
|
||||
deprecatedErrorEvent.detail(Details.REASON, ape.getMessage()).error(Errors.PASSWORD_REJECTED);
|
||||
@ -206,7 +164,6 @@ public class UpdatePassword implements RequiredActionProvider, RequiredActionFac
|
||||
.setError(ape.getMessage())
|
||||
.createResponse(UserModel.RequiredAction.UPDATE_PASSWORD);
|
||||
context.challenge(challenge);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@ -217,7 +174,7 @@ public class UpdatePassword implements RequiredActionProvider, RequiredActionFac
|
||||
|
||||
@Override
|
||||
public RequiredActionProvider create(KeycloakSession session) {
|
||||
return new UpdatePassword(session);
|
||||
return new UpdatePassword();
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -247,11 +204,10 @@ public class UpdatePassword implements RequiredActionProvider, RequiredActionFac
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getMaxAuthAge() {
|
||||
|
||||
public int getMaxAuthAge(KeycloakSession session) {
|
||||
if (session == null) {
|
||||
// session is null, support for legacy implementation, fallback to default maxAuthAge
|
||||
return MaxAuthAgePasswordPolicyProviderFactory.DEFAULT_MAX_AUTH_AGE;
|
||||
return Constants.KC_ACTION_MAX_AGE;
|
||||
}
|
||||
|
||||
// try password policy
|
||||
@ -262,51 +218,8 @@ public class UpdatePassword implements RequiredActionProvider, RequiredActionFac
|
||||
return maxAge;
|
||||
}
|
||||
|
||||
// try required action config
|
||||
AuthenticationSessionModel authSession = keycloakContext.getAuthenticationSession();
|
||||
if (authSession != null) {
|
||||
|
||||
// we need to figure out the alias for the current required action
|
||||
String providerId = authSession.getClientNote(Constants.KC_ACTION);
|
||||
RequiredActionProviderModel requiredAction = RequiredActionHelper.getRequiredActionByProviderId(realm, providerId);
|
||||
|
||||
if (requiredAction != null) {
|
||||
RequiredActionConfigModel configModel = realm.getRequiredActionConfigByAlias(requiredAction.getAlias());
|
||||
if (configModel != null && configModel.containsConfigKey(MAX_AUTH_AGE_KEY)) {
|
||||
maxAge = parseMaxAuthAge(configModel);
|
||||
if (maxAge >= 0) {
|
||||
return maxAge;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// fallback to default
|
||||
return MaxAuthAgePasswordPolicyProviderFactory.DEFAULT_MAX_AUTH_AGE;
|
||||
}
|
||||
return RequiredActionProvider.super.getMaxAuthAge(session);
|
||||
|
||||
|
||||
@Override
|
||||
public List<ProviderConfigProperty> getConfigMetadata() {
|
||||
return List.copyOf(CONFIG_PROPERTIES);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void validateConfig(KeycloakSession session, RealmModel realm, RequiredActionConfigModel model) {
|
||||
|
||||
int parsedMaxAuthAge;
|
||||
try {
|
||||
parsedMaxAuthAge = parseMaxAuthAge(model);
|
||||
} catch (Exception ex) {
|
||||
throw new ValidationException(new ValidationError(getId(), MAX_AUTH_AGE_KEY, "error-invalid-value"));
|
||||
}
|
||||
|
||||
if (parsedMaxAuthAge < 0) {
|
||||
throw new ValidationException(new ValidationError(getId(), MAX_AUTH_AGE_KEY, "error-number-out-of-range-too-small", 0));
|
||||
}
|
||||
}
|
||||
|
||||
private int parseMaxAuthAge(RequiredActionConfigModel model) throws NumberFormatException {
|
||||
return Integer.parseInt(model.getConfigValue(MAX_AUTH_AGE_KEY));
|
||||
}
|
||||
}
|
||||
|
||||
@ -539,7 +539,7 @@ public class OIDCLoginProtocol implements LoginProtocol {
|
||||
}
|
||||
String authTime = userSession.getNote(AuthenticationManager.AUTH_TIME);
|
||||
int authTimeInt = authTime == null ? 0 : Integer.parseInt(authTime);
|
||||
int maxAgeInt = requiredActionProvider.getMaxAuthAge();
|
||||
int maxAgeInt = requiredActionProvider.getMaxAuthAge(session);
|
||||
return authTimeInt + maxAgeInt < Time.currentTime();
|
||||
} else {
|
||||
return false;
|
||||
|
||||
@ -189,7 +189,7 @@ public class RequiredActionsTest extends AbstractAuthenticationTest {
|
||||
RequiredActionConfigInfoRepresentation requiredActionConfigDescription = authMgmtResource.getRequiredActionConfigDescription(providerId);
|
||||
Assertions.assertNotNull(requiredActionConfigDescription);
|
||||
Assertions.assertNotNull(requiredActionConfigDescription.getProperties());
|
||||
Assertions.assertTrue(requiredActionConfigDescription.getProperties().size() == 2);
|
||||
Assertions.assertEquals(3, requiredActionConfigDescription.getProperties().size());
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@ -29,6 +29,7 @@ import org.keycloak.provider.ProviderConfigurationBuilder;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
public class DummyConfigurableRequiredActionFactory implements RequiredActionFactory {
|
||||
|
||||
@ -115,6 +116,9 @@ public class DummyConfigurableRequiredActionFactory implements RequiredActionFac
|
||||
|
||||
@Override
|
||||
public List<ProviderConfigProperty> getConfigMetadata() {
|
||||
return CONFIG_PROPERTIES;
|
||||
return Stream.concat(
|
||||
List.copyOf(CONFIG_PROPERTIES).stream(),
|
||||
List.copyOf(RequiredActionFactory.super.getConfigMetadata()).stream()
|
||||
).toList();
|
||||
}
|
||||
}
|
||||
|
||||
@ -31,11 +31,13 @@ import org.keycloak.events.Details;
|
||||
import org.keycloak.events.Errors;
|
||||
import org.keycloak.events.EventType;
|
||||
import org.keycloak.events.email.EmailEventListenerProviderFactory;
|
||||
import org.keycloak.models.Constants;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.models.credential.PasswordCredentialModel;
|
||||
import org.keycloak.representations.idm.EventRepresentation;
|
||||
import org.keycloak.representations.idm.RealmRepresentation;
|
||||
import org.keycloak.representations.idm.RequiredActionProviderRepresentation;
|
||||
import org.keycloak.representations.idm.UserRepresentation;
|
||||
import org.keycloak.representations.idm.UserSessionRepresentation;
|
||||
import org.keycloak.services.managers.AuthenticationSessionManager;
|
||||
@ -47,16 +49,18 @@ import org.keycloak.testsuite.updaters.RealmAttributeUpdater;
|
||||
import org.keycloak.testsuite.updaters.UserAttributeUpdater;
|
||||
import org.keycloak.testsuite.util.GreenMailRule;
|
||||
import org.keycloak.testsuite.util.MailUtils;
|
||||
import org.keycloak.testsuite.util.SecondBrowser;
|
||||
import org.keycloak.testsuite.util.URLUtils;
|
||||
import org.keycloak.testsuite.util.UserBuilder;
|
||||
import org.keycloak.testsuite.util.oauth.AccessTokenResponse;
|
||||
import org.keycloak.testsuite.util.oauth.OAuthClient;
|
||||
import org.keycloak.testsuite.util.SecondBrowser;
|
||||
import org.openqa.selenium.Cookie;
|
||||
import org.openqa.selenium.WebDriver;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
import static org.hamcrest.Matchers.contains;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
@ -96,6 +100,22 @@ public class AppInitiatedActionResetPasswordTest extends AbstractAppInitiatedAct
|
||||
@After
|
||||
public void after() {
|
||||
ApiUtil.resetUserPassword(testRealm().users().get(findUser("test-user@localhost").getId()), "password", false);
|
||||
|
||||
// reset password required action max auth age back to default
|
||||
Optional<RequiredActionProviderRepresentation> passwordRequiredAction = testRealm().flows().getRequiredActions()
|
||||
.stream()
|
||||
.filter(requiredAction -> requiredAction.getProviderId().equals(UserModel.RequiredAction.UPDATE_PASSWORD.name()))
|
||||
.findFirst();
|
||||
if (passwordRequiredAction.isPresent()) {
|
||||
passwordRequiredAction.get().getConfig().remove(Constants.MAX_AUTH_AGE_KEY);
|
||||
testRealm().flows().updateRequiredAction(UserModel.RequiredAction.UPDATE_PASSWORD.name(), passwordRequiredAction.get());
|
||||
}
|
||||
|
||||
// remove all required action from the user
|
||||
UserResource user = ApiUtil.findUserByUsernameId(testRealm(), "test-user@localhost");
|
||||
UserRepresentation userRepresentation = user.toRepresentation();
|
||||
userRepresentation.setRequiredActions(Collections.emptyList());
|
||||
user.update(userRepresentation);
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -189,6 +209,79 @@ public class AppInitiatedActionResetPasswordTest extends AbstractAppInitiatedAct
|
||||
assertKcActionStatus(SUCCESS);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void resetPasswordRequiresReAuthWithIndividualMaxAuthAgeConfig() throws Exception {
|
||||
// retrieve the password required action
|
||||
RequiredActionProviderRepresentation passwordRequiredAction = testRealm().flows().getRequiredActions()
|
||||
.stream()
|
||||
.filter(requiredAction -> requiredAction.getProviderId().equals(UserModel.RequiredAction.UPDATE_PASSWORD.name()))
|
||||
.findFirst()
|
||||
.orElseThrow(() -> new Exception("Required action not found"));
|
||||
|
||||
// override default max auth age to 500 seconds for the password required action
|
||||
passwordRequiredAction.getConfig().put(Constants.MAX_AUTH_AGE_KEY, "500");
|
||||
testRealm().flows().updateRequiredAction(UserModel.RequiredAction.UPDATE_PASSWORD.name(), passwordRequiredAction);
|
||||
|
||||
loginPage.open();
|
||||
loginPage.login("test-user@localhost", "password");
|
||||
|
||||
events.expectLogin().assertEvent();
|
||||
|
||||
setTimeOffset(550);
|
||||
|
||||
// Should prompt for re-authentication
|
||||
doAIA();
|
||||
|
||||
loginPage.assertCurrent();
|
||||
Assert.assertEquals("test-user@localhost", loginPage.getAttemptedUsername());
|
||||
loginPage.login("password");
|
||||
|
||||
|
||||
changePasswordPage.assertCurrent();
|
||||
assertTrue(changePasswordPage.isCancelDisplayed());
|
||||
|
||||
changePasswordPage.changePassword("new-password", "new-password");
|
||||
|
||||
events.expectRequiredAction(EventType.UPDATE_PASSWORD).assertEvent();
|
||||
events.expectRequiredAction(EventType.UPDATE_CREDENTIAL).detail(Details.CREDENTIAL_TYPE, PasswordCredentialModel.TYPE).assertEvent();
|
||||
assertKcActionStatus(SUCCESS);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void resetPasswordRequiresNoReAuthWithIndividualMaxAuthAgeConfig() throws Exception {
|
||||
// retrieve the password required action
|
||||
RequiredActionProviderRepresentation passwordRequiredAction = testRealm().flows().getRequiredActions()
|
||||
.stream()
|
||||
.filter(requiredAction -> requiredAction.getProviderId().equals(UserModel.RequiredAction.UPDATE_PASSWORD.name()))
|
||||
.findFirst()
|
||||
.orElseThrow(() -> new Exception("Required action not found"));
|
||||
|
||||
// override default max auth age to 500 seconds for the password required action
|
||||
passwordRequiredAction.getConfig().put(Constants.MAX_AUTH_AGE_KEY, "500");
|
||||
testRealm().flows().updateRequiredAction(UserModel.RequiredAction.UPDATE_PASSWORD.name(), passwordRequiredAction);
|
||||
|
||||
|
||||
loginPage.open();
|
||||
loginPage.login("test-user@localhost", "password");
|
||||
|
||||
events.expectLogin().assertEvent();
|
||||
|
||||
setTimeOffset(350);
|
||||
|
||||
// Should not prompt for re-authentication
|
||||
doAIA();
|
||||
|
||||
changePasswordPage.assertCurrent();
|
||||
assertTrue(changePasswordPage.isCancelDisplayed());
|
||||
|
||||
changePasswordPage.changePassword("new-password", "new-password");
|
||||
|
||||
events.expectRequiredAction(EventType.UPDATE_PASSWORD).assertEvent();
|
||||
events.expectRequiredAction(EventType.UPDATE_CREDENTIAL).detail(Details.CREDENTIAL_TYPE, PasswordCredentialModel.TYPE).assertEvent();
|
||||
assertKcActionStatus(SUCCESS);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* See GH-12943
|
||||
* @throws Exception
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user