Handle reset password flow with logged in user

Closes #8887

Signed-off-by: Giuseppe Graziano <g.graziano94@gmail.com>
This commit is contained in:
Giuseppe Graziano 2024-05-02 16:14:42 +02:00 committed by Marek Posolda
parent 00bd6224fa
commit c6d3e56cda
4 changed files with 52 additions and 7 deletions

View file

@ -71,6 +71,10 @@ public class AuthenticatorUtil {
return "true".equals(authSession.getAuthNote(PASSWORD_VALIDATED)); return "true".equals(authSession.getAuthNote(PASSWORD_VALIDATED));
} }
public static boolean isForkedFlow(AuthenticationSessionModel authSession) {
return authSession.getAuthNote(AuthenticationProcessor.FORKED_FROM) != null;
}
/** /**
* Set authentication session note for callbacks defined for {@link AuthenticationFlowCallbackFactory) factories * Set authentication session note for callbacks defined for {@link AuthenticationFlowCallbackFactory) factories
* *

View file

@ -19,6 +19,7 @@ package org.keycloak.authentication.authenticators.browser;
import org.keycloak.authentication.AuthenticationFlowContext; import org.keycloak.authentication.AuthenticationFlowContext;
import org.keycloak.authentication.Authenticator; import org.keycloak.authentication.Authenticator;
import org.keycloak.authentication.AuthenticatorUtil;
import org.keycloak.authentication.authenticators.util.AcrStore; import org.keycloak.authentication.authenticators.util.AcrStore;
import org.keycloak.authentication.authenticators.util.AuthenticatorUtils; import org.keycloak.authentication.authenticators.util.AuthenticatorUtils;
import org.keycloak.models.Constants; import org.keycloak.models.Constants;
@ -61,6 +62,8 @@ public class CookieAuthenticator implements Authenticator {
authSession.setAuthNote(AuthenticationManager.FORCED_REAUTHENTICATION, "true"); authSession.setAuthNote(AuthenticationManager.FORCED_REAUTHENTICATION, "true");
context.setForwardedInfoMessage(Messages.REAUTHENTICATE); context.setForwardedInfoMessage(Messages.REAUTHENTICATE);
context.attempted(); context.attempted();
} else if(AuthenticatorUtil.isForkedFlow(authSession)){
context.attempted();
} else { } else {
int previouslyAuthenticatedLevel = acrStore.getHighestAuthenticatedLevelFromPreviousAuthentication(); int previouslyAuthenticatedLevel = acrStore.getHighestAuthenticatedLevelFromPreviousAuthentication();
AuthenticatorUtils.updateCompletedExecutions(context.getAuthenticationSession(), authResult.getSession(), context.getExecution().getId()); AuthenticatorUtils.updateCompletedExecutions(context.getAuthenticationSession(), authResult.getSession(), context.getExecution().getId());

View file

@ -33,6 +33,7 @@ import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel; import org.keycloak.models.UserModel;
import org.keycloak.models.utils.FormMessage; import org.keycloak.models.utils.FormMessage;
import org.keycloak.provider.ProviderConfigProperty; import org.keycloak.provider.ProviderConfigProperty;
import org.keycloak.services.managers.AuthenticationManager;
import org.keycloak.services.messages.Messages; import org.keycloak.services.messages.Messages;
import org.keycloak.services.validation.Validation; import org.keycloak.services.validation.Validation;
@ -75,6 +76,15 @@ public class ResetCredentialChooseUser implements Authenticator, AuthenticatorFa
return; return;
} }
AuthenticationManager.AuthResult authResult = AuthenticationManager.authenticateIdentityCookie(context.getSession(), context.getRealm(), true);
//skip user choice if sso session exists
if (authResult != null) {
context.getAuthenticationSession().setAuthNote(AbstractUsernameFormAuthenticator.ATTEMPTED_USERNAME, authResult.getUser().getUsername());
context.setUser(authResult.getUser());
context.success();
return;
}
Response challenge = context.form().createPasswordReset(); Response challenge = context.form().createPasswordReset();
context.challenge(challenge); context.challenge(challenge);
} }

View file

@ -160,7 +160,30 @@ public class ResetPasswordTest extends AbstractTestRealmKeycloakTest {
String username = "login-test"; String username = "login-test";
String resetUri = oauth.AUTH_SERVER_ROOT + "/realms/test/login-actions/reset-credentials"; String resetUri = oauth.AUTH_SERVER_ROOT + "/realms/test/login-actions/reset-credentials";
openResetPasswordUrlAndDoFlow(resetUri, "account", oauth.AUTH_SERVER_ROOT + "/realms/test/account/"); openResetPasswordUrlAndDoFlow(resetUri, "account", oauth.AUTH_SERVER_ROOT + "/realms/test/account/", false);
AccountHelper.logout(testRealm(), username);
WaitUtils.waitForPageToLoad();
TestAppHelper testAppHelper = new TestAppHelper(oauth, loginPage, appPage);
testAppHelper.login(username, "resetPassword");
appPage.assertCurrent();
assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
}
@Test
public void resetPasswordLoggedUser() throws IOException {
String username = "login-test";
loginPage.open();
loginPage.login(username, "password");
events.expectLogin().user(userId).detail(Details.USERNAME, username).assertEvent();
String resetUri = oauth.AUTH_SERVER_ROOT + "/realms/test/login-actions/reset-credentials";
openResetPasswordUrlAndDoFlow(resetUri, "account", oauth.AUTH_SERVER_ROOT + "/realms/test/account/", true);
AccountHelper.logout(testRealm(), username); AccountHelper.logout(testRealm(), username);
WaitUtils.waitForPageToLoad(); WaitUtils.waitForPageToLoad();
@ -174,13 +197,14 @@ public class ResetPasswordTest extends AbstractTestRealmKeycloakTest {
} }
// Starts by opening "reset-password-url". Then go through the successful reset-password flow for the particular user. After user confirms new password, this method ends. // Starts by opening "reset-password-url". Then go through the successful reset-password flow for the particular user. After user confirms new password, this method ends.
private void openResetPasswordUrlAndDoFlow(String resetUri, String expectedClientId, String expectedRedirectUri) throws IOException { private void openResetPasswordUrlAndDoFlow(String resetUri, String expectedClientId, String expectedRedirectUri, boolean userAuthenticated) throws IOException {
String username = "login-test"; String username = "login-test";
driver.navigate().to(resetUri); driver.navigate().to(resetUri);
resetPasswordPage.assertCurrent(); if (!userAuthenticated) {
resetPasswordPage.assertCurrent();
resetPasswordPage.changePassword(username); resetPasswordPage.changePassword(username);
}
loginPage.assertCurrent(); loginPage.assertCurrent();
assertEquals("You should receive an email shortly with further instructions.", loginPage.getSuccessMessage()); assertEquals("You should receive an email shortly with further instructions.", loginPage.getSuccessMessage());
@ -208,6 +232,10 @@ public class ResetPasswordTest extends AbstractTestRealmKeycloakTest {
updatePasswordPage.assertCurrent(); updatePasswordPage.assertCurrent();
if(userAuthenticated) {
updatePasswordPage.uncheckLogoutSessions();
}
updatePasswordPage.changePassword("resetPassword", "resetPassword"); updatePasswordPage.changePassword("resetPassword", "resetPassword");
event = events.expectRequiredAction(EventType.UPDATE_PASSWORD) event = events.expectRequiredAction(EventType.UPDATE_PASSWORD)
@ -225,7 +253,7 @@ public class ResetPasswordTest extends AbstractTestRealmKeycloakTest {
public void resetPasswordLinkTestAppWithoutRedirectUriParam() throws IOException { public void resetPasswordLinkTestAppWithoutRedirectUriParam() throws IOException {
String resetUri = oauth.AUTH_SERVER_ROOT + "/realms/test/login-actions/reset-credentials?client_id=test-app"; String resetUri = oauth.AUTH_SERVER_ROOT + "/realms/test/login-actions/reset-credentials?client_id=test-app";
openResetPasswordUrlAndDoFlow(resetUri, "test-app", null); openResetPasswordUrlAndDoFlow(resetUri, "test-app", null, false);
// Link "Back to application" with the baseUrl of client "test-app" // Link "Back to application" with the baseUrl of client "test-app"
infoPage.assertCurrent(); infoPage.assertCurrent();
@ -240,7 +268,7 @@ public class ResetPasswordTest extends AbstractTestRealmKeycloakTest {
public void resetPasswordLinkTestAppWithRedirectUriParam() throws IOException { public void resetPasswordLinkTestAppWithRedirectUriParam() throws IOException {
String resetUri = oauth.AUTH_SERVER_ROOT + "/realms/test/login-actions/reset-credentials?client_id=test-app&redirect_uri=" + oauth.getRedirectUri(); String resetUri = oauth.AUTH_SERVER_ROOT + "/realms/test/login-actions/reset-credentials?client_id=test-app&redirect_uri=" + oauth.getRedirectUri();
openResetPasswordUrlAndDoFlow(resetUri, "test-app", oauth.getRedirectUri()); openResetPasswordUrlAndDoFlow(resetUri, "test-app", oauth.getRedirectUri(), false);
// Should be directly redirected to "application because of "redirect_uri" parameter // Should be directly redirected to "application because of "redirect_uri" parameter
appPage.assertCurrent(); appPage.assertCurrent();