keycloak-memoizeit

Details

diff --git a/services/src/main/java/org/keycloak/services/resources/LoginActionsService.java b/services/src/main/java/org/keycloak/services/resources/LoginActionsService.java
index 6d9fee9..b6e4ce3 100755
--- a/services/src/main/java/org/keycloak/services/resources/LoginActionsService.java
+++ b/services/src/main/java/org/keycloak/services/resources/LoginActionsService.java
@@ -24,6 +24,7 @@ package org.keycloak.services.resources;
 import org.jboss.logging.Logger;
 import org.jboss.resteasy.spi.HttpRequest;
 import org.keycloak.ClientConnection;
+import org.keycloak.OAuth2Constants;
 import org.keycloak.authentication.AuthenticationProcessor;
 import org.keycloak.authentication.RequiredActionContext;
 import org.keycloak.authentication.RequiredActionContextResult;
@@ -40,6 +41,7 @@ import org.keycloak.login.LoginFormsProvider;
 import org.keycloak.models.AuthenticationFlowModel;
 import org.keycloak.models.ClientModel;
 import org.keycloak.models.ClientSessionModel;
+import org.keycloak.models.Constants;
 import org.keycloak.models.KeycloakSession;
 import org.keycloak.models.ModelException;
 import org.keycloak.models.ProtocolMapperModel;
@@ -52,10 +54,14 @@ import org.keycloak.models.UserModel.RequiredAction;
 import org.keycloak.models.UserSessionModel;
 import org.keycloak.models.utils.CredentialValidation;
 import org.keycloak.models.utils.FormMessage;
+import org.keycloak.models.utils.KeycloakModelUtils;
 import org.keycloak.protocol.LoginProtocol;
 import org.keycloak.protocol.RestartLoginCookie;
+import org.keycloak.protocol.oidc.OIDCLoginProtocol;
 import org.keycloak.protocol.oidc.TokenManager;
+import org.keycloak.protocol.oidc.utils.RedirectUtils;
 import org.keycloak.services.ErrorPage;
+import org.keycloak.services.ErrorResponse;
 import org.keycloak.services.Urls;
 import org.keycloak.services.managers.AuthenticationManager;
 import org.keycloak.services.managers.ClientSessionCode;
@@ -341,10 +347,43 @@ public class LoginActionsService {
         return resetCredentials(code, execution);
     }
 
+    /**
+     * Endpoint for executing reset credentials flow.  If code is null, a client session is created with the account
+     * service as the client.  Successful reset sends you to the account page.  Note, account service must be enabled.
+     *
+     * @param code
+     * @param execution
+     * @return
+     */
     @Path(RESET_CREDENTIALS_PATH)
     @GET
     public Response resetCredentialsGET(@QueryParam("code") String code,
                                          @QueryParam("execution") String execution) {
+        // we allow applications to link to reset credentials without going through OAuth or SAML handshakes
+        //
+        if (code == null) {
+            if (!realm.isResetPasswordAllowed()) {
+                event.event(EventType.RESET_PASSWORD);
+                event.error(Errors.NOT_ALLOWED);
+                return ErrorPage.error(session, Messages.RESET_CREDENTIAL_NOT_ALLOWED);
+
+            }
+            // set up the account service as the endpoint to call.
+            ClientModel client = realm.getClientByClientId(Constants.ACCOUNT_MANAGEMENT_CLIENT_ID);
+            ClientSessionModel clientSession = session.sessions().createClientSession(realm, client);
+            clientSession.setAction(ClientSessionModel.Action.AUTHENTICATE.name());
+            clientSession.setNote(ClientSessionCode.ACTION_KEY, KeycloakModelUtils.generateCodeSecret());
+            //clientSession.setNote(AuthenticationManager.END_AFTER_REQUIRED_ACTIONS, "true");
+            clientSession.setAuthMethod(OIDCLoginProtocol.LOGIN_PROTOCOL);
+            String redirectUri = Urls.accountBase(uriInfo.getBaseUri()).path("/").build(realm.getName()).toString();
+            clientSession.setRedirectUri(redirectUri);
+            clientSession.setAction(ClientSessionModel.Action.AUTHENTICATE.name());
+            clientSession.setNote(ClientSessionCode.ACTION_KEY, KeycloakModelUtils.generateCodeSecret());
+            clientSession.setNote(OIDCLoginProtocol.RESPONSE_TYPE_PARAM, OAuth2Constants.CODE);
+            clientSession.setNote(OIDCLoginProtocol.REDIRECT_URI_PARAM, redirectUri);
+            clientSession.setNote(OIDCLoginProtocol.ISSUER, Urls.realmIssuer(uriInfo.getBaseUri(), realm.getName()));
+            return processResetCredentials(null, clientSession, null);
+        }
         return resetCredentials(code, execution);
     }
 
@@ -357,6 +396,13 @@ public class LoginActionsService {
         final ClientSessionCode clientCode = checks.clientCode;
         final ClientSessionModel clientSession = clientCode.getClientSession();
 
+        if (!realm.isResetPasswordAllowed()) {
+            event.client(clientCode.getClientSession().getClient());
+            event.error(Errors.NOT_ALLOWED);
+            return ErrorPage.error(session, Messages.RESET_CREDENTIAL_NOT_ALLOWED);
+
+        }
+
         return processResetCredentials(execution, clientSession, null);
     }
 
@@ -573,7 +619,7 @@ public class LoginActionsService {
                 return checks.response;
             }
             ClientSessionModel clientSession = checks.clientCode.getClientSession();
-            clientSession.setNote("END_AFTER_REQUIRED_ACTIONS", "true");
+            clientSession.setNote(AuthenticationManager.END_AFTER_REQUIRED_ACTIONS, "true");
             clientSession.setNote(ClientSessionModel.Action.EXECUTE_ACTIONS.name(), "true");
             return AuthenticationManager.nextActionAfterAuthentication(session, clientSession.getUserSession(), clientSession, clientConnection, request, uriInfo, event);
         } else {
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/ResetPasswordTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/ResetPasswordTest.java
index 04b0c65..788b788 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/ResetPasswordTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/ResetPasswordTest.java
@@ -36,6 +36,7 @@ import org.keycloak.models.UserModel;
 import org.keycloak.representations.idm.CredentialRepresentation;
 import org.keycloak.services.managers.RealmManager;
 import org.keycloak.testsuite.AssertEvents;
+import org.keycloak.testsuite.Constants;
 import org.keycloak.testsuite.MailUtil;
 import org.keycloak.testsuite.OAuthClient;
 import org.keycloak.testsuite.pages.AppPage;
@@ -141,6 +142,64 @@ public class ResetPasswordTest {
     }
 
     @Test
+    public void resetPasswordLink() throws IOException, MessagingException {
+        String username = "login-test";
+        String resetUri = Constants.AUTH_SERVER_ROOT + "/realms/test/login-actions/reset-credentials";
+        driver.navigate().to(resetUri);
+
+        resetPasswordPage.assertCurrent();
+
+        resetPasswordPage.changePassword(username);
+
+        loginPage.assertCurrent();
+        assertEquals("You should receive an email shortly with further instructions.", loginPage.getSuccessMessage());
+
+        events.expectRequiredAction(EventType.SEND_RESET_PASSWORD)
+                .user(userId)
+                .detail(Details.REDIRECT_URI,  Constants.AUTH_SERVER_ROOT + "/realms/test/account/")
+                .client("account")
+                .detail(Details.USERNAME, username)
+                .detail(Details.EMAIL, "login@test.com")
+                .session((String)null)
+                .assertEvent();
+
+        assertEquals(1, greenMail.getReceivedMessages().length);
+
+        MimeMessage message = greenMail.getReceivedMessages()[0];
+
+        String changePasswordUrl = getPasswordResetEmailLink(message);
+
+        driver.navigate().to(changePasswordUrl.trim());
+
+        updatePasswordPage.assertCurrent();
+
+        updatePasswordPage.changePassword("resetPassword", "resetPassword");
+
+        String sessionId = events.expectRequiredAction(EventType.UPDATE_PASSWORD)
+                .detail(Details.REDIRECT_URI,  Constants.AUTH_SERVER_ROOT + "/realms/test/account/")
+                .client("account")
+                .user(userId).detail(Details.USERNAME, username).assertEvent().getSessionId();
+
+        events.expectLogin().user(userId).detail(Details.USERNAME, username)
+                .detail(Details.REDIRECT_URI,  Constants.AUTH_SERVER_ROOT + "/realms/test/account/")
+                .client("account")
+                .session(sessionId).assertEvent();
+
+        oauth.openLogout();
+
+        events.expectLogout(sessionId).user(userId).session(sessionId).assertEvent();
+
+        loginPage.open();
+
+        loginPage.login("login-test", "resetPassword");
+
+        events.expectLogin().user(userId).detail(Details.USERNAME, "login-test").assertEvent();
+
+        assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
+    }
+
+
+    @Test
     public void resetPassword() throws IOException, MessagingException {
         resetPassword("login-test");
     }