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:
Kai J. Witt 2025-05-15 08:44:38 +02:00 committed by GitHub
parent f7277315e3
commit c76bb0683c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 232 additions and 110 deletions

View File

@ -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

View File

@ -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

View File

@ -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));
}
}

View File

@ -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;
}
}

View File

@ -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";

View File

@ -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);

View File

@ -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 {

View File

@ -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));
}
}

View File

@ -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;

View File

@ -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

View File

@ -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();
}
}

View File

@ -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