mirror of
https://github.com/keycloak/keycloak.git
synced 2026-01-10 15:32:05 -03:30
Force login in reset-credentials to federated users
Closes #37207 Signed-off-by: rmartinc <rmartinc@redhat.com>
This commit is contained in:
parent
3a793916a9
commit
6850f41060
@ -16,6 +16,9 @@ include::topics/templates/release-header.adoc[]
|
||||
== {project_name_full} 26.2.0
|
||||
include::topics/26_2_0.adoc[leveloffset=2]
|
||||
|
||||
== {project_name_full} 26.1.3
|
||||
include::topics/26_1_3.adoc[leveloffset=2]
|
||||
|
||||
== {project_name_full} 26.1.1
|
||||
include::topics/26_1_1.adoc[leveloffset=2]
|
||||
|
||||
|
||||
5
docs/documentation/release_notes/topics/26_1_3.adoc
Normal file
5
docs/documentation/release_notes/topics/26_1_3.adoc
Normal file
@ -0,0 +1,5 @@
|
||||
= Send Reset Email force login again for federated users after reset credentials
|
||||
|
||||
In <<keycloak-26-1-1, version 26.1.1>> a new configuration option was added to the `reset-credential-email` (*Send Reset Email*) authenticator to allow changing the default behavior after the reset credentials flow. Now the option `force-login` (*Force login after reset*) is adding a third configuration value `only-federated`, which means that the force login is true for federated users and false for the internal database users. The new behavior is now the default. This way all users managed by user federation providers, whose implementation can be not so tightly integrated with {project_name}, are forced to login again after the reset credentials flow to avoid any issue. This change in behavior is due to the secure by default policy.
|
||||
|
||||
For more information, see link:{adminguide_link}#enabling-forgot-password[Enable forgot password].
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 25 KiB |
@ -27,7 +27,7 @@ image:images/forgot-password-page.png[Forgot Password Page]
|
||||
|
||||
The text sent in the email is configurable. See link:{developerguide_link}[{developerguide_name}] for more information.
|
||||
|
||||
When users click the email link, {project_name} asks them to update their password, and if they have set up an OTP generator, {project_name} asks them to reconfigure the OTP generator. By default, the flow maintains the user who is logged in if the same authentication session (same browser) was used. Depending on the security requirements of your organization, you may not want users to reset their OTP generator through email or you may want to force them to login again after the process.
|
||||
When users click the email link, {project_name} asks them to update their password, and if they have set up an OTP generator, {project_name} asks them to reconfigure the OTP generator. For security reasons, the flow forces federated users to login again after the reset credentials and keeps internal database users logged in if the same authentication session (same browser) is used. Depending on the security requirements of your organization, you can change the default behavior.
|
||||
|
||||
To change this behavior, perform these steps:
|
||||
|
||||
@ -44,7 +44,7 @@ If you do not want to reset the OTP, set the `Reset - Conditional OTP` sub-flow
|
||||
.Send Reset Email Configuration
|
||||
image:images/reset-credential-email-config.png[Send Reset Email Configuration]
|
||||
+
|
||||
If the user needs to login again after the password change, click the *Send Reset Email* settings icon in the flow, define an *Alias*, and enable *Force login after reset*.
|
||||
If you want to change default behavior for the force login option, click the *Send Reset Email* settings icon in the flow, define an *Alias*, and select the best *Force login after reset* option for you (`true`, always force re-authentication, `false`, keep the user logged in if the same browser was used, `only-federated`, default value that forces login again only for federated users).
|
||||
. Click *Authentication* in the menu.
|
||||
. Click the *Required actions* tab.
|
||||
. Ensure *Update Password* is enabled.
|
||||
|
||||
@ -0,0 +1,11 @@
|
||||
== Notable changes
|
||||
|
||||
Notable changes where an internal behavior changed to prevent common misconfigurations, fix bugs or simplify running {project_name}.
|
||||
|
||||
=== Send Reset Email force login again for federated users after reset credentials
|
||||
|
||||
Previously the reset credentials flow (*forgot password* feature) kept the user logged in after the reset credentials if the same authentication session (same browser) was used. For federated user providers this behavior can be a security issue. Imagine a provider implementation that detects the user as *enabled*, performs the password change successfully but the validation of the user password fails for some reason. In this scenario the reset credentials flow allowed a user to be logged in after the successful password change that would have not been allowed to login using the normal browser flow. This scenario is not a common case but should be avoided by default.
|
||||
|
||||
For this reason now the authenticator `reset-credential-email` (*Send Reset Email*) has a new configuration option called `force-login` (*Force login after reset*) with values `true` (always force the login), `false` (previous behavior that keeps the user logged in if the same authentication session is used), and `only-federated` (default value that forces federated users to authenticate again and keeps previous behavior for users stored in {project_name}'s internal database).
|
||||
|
||||
For more information about changing this option, see link:{adminguide_link}#enabling-forgot-password[Enable forgot password].
|
||||
@ -1,6 +1,10 @@
|
||||
[[migration-changes]]
|
||||
== Migration Changes
|
||||
|
||||
=== Migrating to 26.1.3
|
||||
|
||||
include::changes-26_1_3.adoc[leveloffset=2]
|
||||
|
||||
=== Migrating to 26.1.0
|
||||
|
||||
include::changes-26_1_0.adoc[leveloffset=2]
|
||||
|
||||
@ -31,6 +31,7 @@ import org.keycloak.events.Errors;
|
||||
import org.keycloak.events.EventBuilder;
|
||||
import org.keycloak.events.EventType;
|
||||
import org.keycloak.models.AuthenticationExecutionModel;
|
||||
import org.keycloak.models.AuthenticatorConfigModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.KeycloakSessionFactory;
|
||||
import org.keycloak.models.RealmModel;
|
||||
@ -43,6 +44,7 @@ import org.keycloak.services.managers.AuthenticationManager;
|
||||
import org.keycloak.services.messages.Messages;
|
||||
import org.keycloak.sessions.AuthenticationSessionCompoundId;
|
||||
import org.keycloak.sessions.AuthenticationSessionModel;
|
||||
import org.keycloak.storage.StorageId;
|
||||
|
||||
import java.util.*;
|
||||
import jakarta.ws.rs.core.Response;
|
||||
@ -60,6 +62,7 @@ public class ResetCredentialEmail implements Authenticator, AuthenticatorFactory
|
||||
|
||||
public static final String PROVIDER_ID = "reset-credential-email";
|
||||
public static final String FORCE_LOGIN = "force-login";
|
||||
public static final String FEDERATED_OPTION = "only-federated";
|
||||
|
||||
@Override
|
||||
public void authenticate(AuthenticationFlowContext context) {
|
||||
@ -77,8 +80,7 @@ public class ResetCredentialEmail implements Authenticator, AuthenticatorFactory
|
||||
String actionTokenUserId = authenticationSession.getAuthNote(DefaultActionTokenKey.ACTION_TOKEN_USER_ID);
|
||||
if (actionTokenUserId != null && Objects.equals(user.getId(), actionTokenUserId)) {
|
||||
logger.debugf("Forget-password triggered when reauthenticating user after authentication via action token. Skipping " + PROVIDER_ID + " screen and using user '%s' ", user.getUsername());
|
||||
if (context.getAuthenticatorConfig() != null
|
||||
&& Boolean.parseBoolean(context.getAuthenticatorConfig().getConfig().get(FORCE_LOGIN))) {
|
||||
if (forceLogin(context.getAuthenticatorConfig(), user)) {
|
||||
// force end of auth session after the required actions
|
||||
context.getAuthenticationSession().setAuthNote(AuthenticationManager.END_AFTER_REQUIRED_ACTIONS, "true");
|
||||
}
|
||||
@ -205,10 +207,13 @@ public class ResetCredentialEmail implements Authenticator, AuthenticatorFactory
|
||||
If this property is true, the user needs to login again after the reset credentials.
|
||||
If this property is false, the user will be automatically logged in after the succesful
|
||||
reset credentials when the same authentication session is used.
|
||||
If this property is only-federated (default), only federated users will be forced to login again,
|
||||
users stored in the internal database will be logged in if using the same authentication session.
|
||||
"""
|
||||
)
|
||||
.type(ProviderConfigProperty.BOOLEAN_TYPE)
|
||||
.defaultValue(Boolean.FALSE.toString())
|
||||
.type(ProviderConfigProperty.LIST_TYPE)
|
||||
.options(Arrays.asList(Boolean.TRUE.toString(), Boolean.FALSE.toString(), FEDERATED_OPTION))
|
||||
.defaultValue(FEDERATED_OPTION)
|
||||
.add()
|
||||
.build();
|
||||
}
|
||||
@ -237,4 +242,19 @@ public class ResetCredentialEmail implements Authenticator, AuthenticatorFactory
|
||||
public String getId() {
|
||||
return PROVIDER_ID;
|
||||
}
|
||||
|
||||
private boolean forceLogin(AuthenticatorConfigModel config, UserModel user) {
|
||||
final String forceLogin = config != null? config.getConfig().get(FORCE_LOGIN) : null;
|
||||
if (forceLogin == null || FEDERATED_OPTION.equalsIgnoreCase(forceLogin)) {
|
||||
// default is only-federated, return true only for federated users
|
||||
return !StorageId.isLocalStorage(user.getId()) || user.getFederationLink() != null;
|
||||
} else if (Boolean.TRUE.toString().equalsIgnoreCase(forceLogin)) {
|
||||
return Boolean.TRUE;
|
||||
} else if (Boolean.FALSE.toString().equalsIgnoreCase(forceLogin)) {
|
||||
return Boolean.FALSE;
|
||||
} else {
|
||||
logger.warnf("Invalid value for force-login option: %s", forceLogin);
|
||||
throw new AuthenticationFlowException("Invalid value for force-login option: " + forceLogin, AuthenticationFlowError.INTERNAL_ERROR);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -25,6 +25,7 @@ import org.keycloak.authentication.actiontoken.resetcred.ResetCredentialsActionT
|
||||
import org.keycloak.authentication.authenticators.resetcred.ResetCredentialEmail;
|
||||
import org.jboss.arquillian.graphene.page.Page;
|
||||
import org.keycloak.common.constants.ServiceAccountConstants;
|
||||
import org.keycloak.common.util.MultivaluedHashMap;
|
||||
import org.keycloak.common.util.KeycloakUriBuilder;
|
||||
import org.keycloak.events.Details;
|
||||
import org.keycloak.events.Errors;
|
||||
@ -37,14 +38,19 @@ import org.keycloak.models.utils.SystemClientUtil;
|
||||
import org.keycloak.representations.idm.AuthenticationExecutionInfoRepresentation;
|
||||
import org.keycloak.representations.idm.AuthenticatorConfigRepresentation;
|
||||
import org.keycloak.representations.idm.ClientRepresentation;
|
||||
import org.keycloak.representations.idm.ComponentRepresentation;
|
||||
import org.keycloak.representations.idm.EventRepresentation;
|
||||
import org.keycloak.representations.idm.RealmRepresentation;
|
||||
import org.keycloak.representations.idm.UserRepresentation;
|
||||
import org.keycloak.services.resources.LoginActionsService;
|
||||
import org.keycloak.storage.StorageId;
|
||||
import org.keycloak.storage.UserStorageProvider;
|
||||
import org.keycloak.storage.UserStorageProviderModel;
|
||||
import org.keycloak.testsuite.AssertEvents;
|
||||
import org.keycloak.testsuite.AbstractTestRealmKeycloakTest;
|
||||
import org.keycloak.testsuite.admin.ApiUtil;
|
||||
import org.keycloak.testsuite.arquillian.annotation.IgnoreBrowserDriver;
|
||||
import org.keycloak.testsuite.federation.UserMapStorageFactory;
|
||||
import org.keycloak.testsuite.federation.kerberos.AbstractKerberosTest;
|
||||
import org.keycloak.testsuite.pages.AppPage;
|
||||
import org.keycloak.testsuite.pages.AppPage.RequestType;
|
||||
@ -339,21 +345,41 @@ public class ResetPasswordTest extends AbstractTestRealmKeycloakTest {
|
||||
@Test
|
||||
public void resetPasswordForceLogin() throws IOException, MessagingException {
|
||||
// add the force login option in the reset-credential-email authenticator
|
||||
AuthenticationExecutionInfoRepresentation sendEmailExec = testRealm()
|
||||
.flows()
|
||||
.getExecutions(DefaultAuthenticationFlows.RESET_CREDENTIALS_FLOW)
|
||||
.stream()
|
||||
.filter(e -> ResetCredentialEmail.PROVIDER_ID.equals(e.getProviderId()))
|
||||
.findAny().orElseThrow();
|
||||
AuthenticatorConfigRepresentation config = new AuthenticatorConfigRepresentation();
|
||||
config.setAlias("reset-password-config");
|
||||
config.getConfig().put(ResetCredentialEmail.FORCE_LOGIN, Boolean.TRUE.toString());
|
||||
String configId = ApiUtil.getCreatedId(testRealm().flows().newExecutionConfig(sendEmailExec.getId(), config));
|
||||
getCleanup().addAuthenticationConfigId(configId);
|
||||
configureForceLogin(Boolean.TRUE.toString());
|
||||
|
||||
resetPassword("login-test", "resetPassword", true);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void resetPasswordForceLoginFederatedUser() throws IOException, MessagingException {
|
||||
// create the example map storage user federation
|
||||
ComponentRepresentation memProvider = new ComponentRepresentation();
|
||||
memProvider.setName(UserStorageProvider.class.getName());
|
||||
memProvider.setProviderId(UserMapStorageFactory.PROVIDER_ID);
|
||||
memProvider.setProviderType(UserStorageProvider.class.getName());
|
||||
memProvider.setConfig(new MultivaluedHashMap<>());
|
||||
memProvider.getConfig().putSingle("priority", Integer.toString(0));
|
||||
memProvider.getConfig().putSingle(UserStorageProviderModel.IMPORT_ENABLED, Boolean.toString(false));
|
||||
String componentId = ApiUtil.getCreatedId(testRealm().components().add(memProvider));
|
||||
getCleanup().addComponentId(componentId);
|
||||
|
||||
// remove the test user and create it but federated
|
||||
testRealm().users().get(userId).remove();
|
||||
UserRepresentation user = new UserRepresentation();
|
||||
user.setUsername("login-test");
|
||||
user.setEmail("login@test.com");
|
||||
user.setFederationLink(componentId);
|
||||
this.userId = ApiUtil.getCreatedId(testRealm().users().create(user));
|
||||
Assert.assertFalse(StorageId.isLocalStorage(userId));
|
||||
|
||||
// by default federated users are force to re-login
|
||||
resetPassword("login-test", "resetPassword", true);
|
||||
|
||||
// check with false the session is maintained
|
||||
configureForceLogin(Boolean.FALSE.toString());
|
||||
resetPassword("login-test", "resetPassword", false);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void resetPasswordTwiceInNewBrowser() throws IOException, MessagingException {
|
||||
String changePasswordUrl = resetPassword("login-test");
|
||||
@ -421,6 +447,20 @@ public class ResetPasswordTest extends AbstractTestRealmKeycloakTest {
|
||||
loginPage.assertCurrent();
|
||||
}
|
||||
|
||||
private void configureForceLogin(String value) {
|
||||
AuthenticationExecutionInfoRepresentation sendEmailExec = testRealm()
|
||||
.flows()
|
||||
.getExecutions(DefaultAuthenticationFlows.RESET_CREDENTIALS_FLOW)
|
||||
.stream()
|
||||
.filter(e -> ResetCredentialEmail.PROVIDER_ID.equals(e.getProviderId()))
|
||||
.findAny().orElseThrow();
|
||||
AuthenticatorConfigRepresentation config = new AuthenticatorConfigRepresentation();
|
||||
config.setAlias("reset-password-config");
|
||||
config.getConfig().put(ResetCredentialEmail.FORCE_LOGIN, value);
|
||||
String configId = ApiUtil.getCreatedId(testRealm().flows().newExecutionConfig(sendEmailExec.getId(), config));
|
||||
getCleanup().addAuthenticationConfigId(configId);
|
||||
}
|
||||
|
||||
private String resetPassword(String username) throws IOException, MessagingException {
|
||||
return resetPassword(username, "resetPassword", false);
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user