diff --git a/forms/common-themes/src/main/resources/theme/base/login/messages/messages_en.properties b/forms/common-themes/src/main/resources/theme/base/login/messages/messages_en.properties index 1f223aa235..bc10220b0d 100755 --- a/forms/common-themes/src/main/resources/theme/base/login/messages/messages_en.properties +++ b/forms/common-themes/src/main/resources/theme/base/login/messages/messages_en.properties @@ -81,9 +81,7 @@ emailVerifyInstruction3=to re-send the email. backToLogin=« Back to Login -temporaryEmailCode=Temporary Email Code emailInstruction=Enter your username or email address and we will send you instructions on how to create a new password. -validateResetEmailInstruction=You have just been sent an email. Clicking on the URL in the email will allow you to reset credentials and log in. Alternatively, you can manually enter in the temporary code provided in the email in the textbox to the left and hit submit. copyCodeInstruction=Please copy this code and paste it into your application: diff --git a/forms/common-themes/src/main/resources/theme/keycloak/email/html/password-reset.ftl b/forms/common-themes/src/main/resources/theme/keycloak/email/html/password-reset.ftl index 55c676f469..846b45dafd 100755 --- a/forms/common-themes/src/main/resources/theme/keycloak/email/html/password-reset.ftl +++ b/forms/common-themes/src/main/resources/theme/keycloak/email/html/password-reset.ftl @@ -1,5 +1,5 @@ -${msg("passwordResetBodyHtml",link, linkExpiration, realmName, code)} +${msg("passwordResetBodyHtml",link, linkExpiration, realmName)} \ No newline at end of file diff --git a/forms/common-themes/src/main/resources/theme/keycloak/email/messages/messages_de.properties b/forms/common-themes/src/main/resources/theme/keycloak/email/messages/messages_de.properties index fdaa81c9b8..1777078711 100755 --- a/forms/common-themes/src/main/resources/theme/keycloak/email/messages/messages_de.properties +++ b/forms/common-themes/src/main/resources/theme/keycloak/email/messages/messages_de.properties @@ -1,7 +1,7 @@ emailVerificationSubject=E-Mail verifizieren passwordResetSubject=Passwort zur\u00FCckzusetzen -passwordResetBody=Someone just requested to change your {2} account''s password. If this was you, click on the link below to set a new password or cut and paste the temporary code to the forgot password form.\n\n{0}\n\nTemporary Code: {3}\n\nThis link and code will expire within {1} minutes.\n\nIf you don''t want to reset your password, just ignore this message and nothing will be changed. -passwordResetBodyHtml=

Someone just requested to change your {2} account''s password. If this was you, click on the link below to set a new password or cut and paste the temporary code to the forgot password form

{0}

Temporary code: {3}

This link will expire within {1} minutes.

If you don''t want to reset your password, just ignore this message and nothing will be changed.

+passwordResetBody=Someone just requested to change your {2} account''s password. If this was you, click on the link below to set a new password.\n\n{0}\n\nThis link and code will expire within {1} minutes.\n\nIf you don''t want to reset your password, just ignore this message and nothing will be changed. +passwordResetBodyHtml=

Someone just requested to change your {2} account''s password. If this was you, click on the link below to set a new password.

{0}

This link will expire within {1} minutes.

If you don''t want to reset your password, just ignore this message and nothing will be changed.

changePasswordSubject=Change password changePasswordBody=Your adminstrator has just requested that you change your {2} account''s password. Click on the link below to set a new password\n\n{0}\n\nThis link will expire within {1} minutes.\n\nIf you don''t want to reset your password, just ignore this message and nothing will be changed. changePasswordBodyHtml=

Your adminstrator has just requested that you change your {2} account''s password. Click on the link below to set a new password

{0}

This link will expire within {1} minutes.

If you don''t want to reset your password, just ignore this message and nothing will be changed.

diff --git a/forms/common-themes/src/main/resources/theme/keycloak/email/messages/messages_en.properties b/forms/common-themes/src/main/resources/theme/keycloak/email/messages/messages_en.properties index eb45780410..dc353e9bf8 100755 --- a/forms/common-themes/src/main/resources/theme/keycloak/email/messages/messages_en.properties +++ b/forms/common-themes/src/main/resources/theme/keycloak/email/messages/messages_en.properties @@ -2,8 +2,8 @@ emailVerificationSubject=Verify email emailVerificationBody=Someone has created a {2} account with this email address. If this was you, click the link below to verify your email address\n\n{0}\n\nThis link will expire within {1} minutes.\n\nIf you didn''t create this account, just ignore this message. emailVerificationBodyHtml=

Someone has created a {2} account with this email address. If this was you, click the link below to verify your email address

{0}

This link will expire within {1} minutes.

If you didn''t create this account, just ignore this message.

passwordResetSubject=Reset password -passwordResetBody=Someone just requested to change your {2} account''s password. If this was you, click on the link below to set a new password or cut and paste the temporary code to the forgot password form.\n\n{0}\n\nTemporary Code: {3}\n\nThis link and code will expire within {1} minutes.\n\nIf you don''t want to reset your password, just ignore this message and nothing will be changed. -passwordResetBodyHtml=

Someone just requested to change your {2} account''s password. If this was you, click on the link below to set a new password or cut and paste the temporary code to the forgot password form

{0}

Temporary code: {3}

This link will expire within {1} minutes.

If you don''t want to reset your password, just ignore this message and nothing will be changed.

+passwordResetBody=Someone just requested to change your {2} account''s password. If this was you, click on the link below to set a new password.\n\n{0}\n\nThis link and code will expire within {1} minutes.\n\nIf you don''t want to reset your password, just ignore this message and nothing will be changed. +passwordResetBodyHtml=

Someone just requested to change your {2} account''s password. If this was you, click on the link below to set a new password.

{0}

This link will expire within {1} minutes.

If you don''t want to reset your password, just ignore this message and nothing will be changed.

changePasswordSubject=Change password changePasswordBody=Your adminstrator has just requested that you change your {2} account''s password. Click on the link below to set a new password\n\n{0}\n\nThis link will expire within {1} minutes.\n\nIf you don''t want to reset your password, just ignore this message and nothing will be changed. changePasswordBodyHtml=

Your adminstrator has just requested that you change your {2} account''s password. Click on the link below to set a new password

{0}

This link will expire within {1} minutes.

If you don''t want to reset your password, just ignore this message and nothing will be changed.

diff --git a/forms/common-themes/src/main/resources/theme/keycloak/email/messages/messages_pt_BR.properties b/forms/common-themes/src/main/resources/theme/keycloak/email/messages/messages_pt_BR.properties index e913fa0470..91758c3a3d 100755 --- a/forms/common-themes/src/main/resources/theme/keycloak/email/messages/messages_pt_BR.properties +++ b/forms/common-themes/src/main/resources/theme/keycloak/email/messages/messages_pt_BR.properties @@ -2,8 +2,8 @@ emailVerificationSubject=Verifica\u00E7\u00E3o de e-mail emailVerificationBody=Algu\u00E9m criou uma conta {2} com este endere\u00E7o de e-mail. Se foi voc\u00EA, clique no link abaixo para verificar o seu endere\u00E7o de email\n\n{0}\n\nEste link ir\u00E1 expirar dentro de {1} minutos.\n\nSe n\u00E3o foi voc\u00EA que criou esta conta, basta ignorar esta mensagem. emailVerificationBodyHtml=

Algu\u00E9m criou uma conta {2} com este endere\u00E7o de e-mail. Se foi voc\u00EA, clique no link abaixo para verificar o seu endere\u00E7o de email

{0}

Este link ir\u00E1 expirar dentro de {1} minutos.

Se n\u00E3o foi voc\u00EA que criou esta conta, basta ignorar esta mensagem.

passwordResetSubject=Redefini\u00E7\u00E3o de senha -passwordResetBody=Someone just requested to change your {2} account''s password. If this was you, click on the link below to set a new password or cut and paste the temporary code to the forgot password form.\n\n{0}\n\nTemporary Code: {3}\n\nThis link and code will expire within {1} minutes.\n\nIf you don''t want to reset your password, just ignore this message and nothing will be changed. -passwordResetBodyHtml=

Someone just requested to change your {2} account''s password. If this was you, click on the link below to set a new password or cut and paste the temporary code to the forgot password form

{0}

Temporary code: {3}

This link will expire within {1} minutes.

If you don''t want to reset your password, just ignore this message and nothing will be changed.

+passwordResetBody=Someone just requested to change your {2} account''s password. If this was you, click on the link below to set a new password.\n\n{0}\n\nThis link and code will expire within {1} minutes.\n\nIf you don''t want to reset your password, just ignore this message and nothing will be changed. +passwordResetBodyHtml=

Someone just requested to change your {2} account''s password. If this was you, click on the link below to set a new password.

{0}

This link will expire within {1} minutes.

If you don''t want to reset your password, just ignore this message and nothing will be changed.

changePasswordSubject=Change password changePasswordBody=Your adminstrator has just requested that you change your {2} account''s password. Click on the link below to set a new password\n\n{0}\n\nThis link will expire within {1} minutes.\n\nIf you don''t want to reset your password, just ignore this message and nothing will be changed. changePasswordBodyHtml=

Your adminstrator has just requested that you change your {2} account''s password. Click on the link below to set a new password

{0}

This link will expire within {1} minutes.

If you don''t want to reset your password, just ignore this message and nothing will be changed.

diff --git a/forms/common-themes/src/main/resources/theme/keycloak/email/text/password-reset.ftl b/forms/common-themes/src/main/resources/theme/keycloak/email/text/password-reset.ftl index aa15723d97..aba4fd1941 100755 --- a/forms/common-themes/src/main/resources/theme/keycloak/email/text/password-reset.ftl +++ b/forms/common-themes/src/main/resources/theme/keycloak/email/text/password-reset.ftl @@ -1 +1 @@ -${msg("passwordResetBody",link, linkExpiration, realmName, code)} \ No newline at end of file +${msg("passwordResetBody",link, linkExpiration, realmName)} \ No newline at end of file diff --git a/forms/email-api/src/main/java/org/keycloak/email/EmailProvider.java b/forms/email-api/src/main/java/org/keycloak/email/EmailProvider.java index bd62a85daa..e860bde887 100755 --- a/forms/email-api/src/main/java/org/keycloak/email/EmailProvider.java +++ b/forms/email-api/src/main/java/org/keycloak/email/EmailProvider.java @@ -24,7 +24,7 @@ public interface EmailProvider extends Provider { * @param expirationInMinutes * @throws EmailException */ - public void sendPasswordReset(String code, String link, long expirationInMinutes) throws EmailException; + public void sendPasswordReset(String link, long expirationInMinutes) throws EmailException; /** * Change password email requested by admin diff --git a/forms/email-freemarker/src/main/java/org/keycloak/email/freemarker/FreeMarkerEmailProvider.java b/forms/email-freemarker/src/main/java/org/keycloak/email/freemarker/FreeMarkerEmailProvider.java index 849fcae64e..e2a639cba2 100755 --- a/forms/email-freemarker/src/main/java/org/keycloak/email/freemarker/FreeMarkerEmailProvider.java +++ b/forms/email-freemarker/src/main/java/org/keycloak/email/freemarker/FreeMarkerEmailProvider.java @@ -70,11 +70,10 @@ public class FreeMarkerEmailProvider implements EmailProvider { } @Override - public void sendPasswordReset(String code, String link, long expirationInMinutes) throws EmailException { + public void sendPasswordReset(String link, long expirationInMinutes) throws EmailException { Map attributes = new HashMap(); attributes.put("link", link); attributes.put("linkExpiration", expirationInMinutes); - attributes.put("code", code); String realmName = realm.getName().substring(0, 1).toUpperCase() + realm.getName().substring(1); attributes.put("realmName", realmName); diff --git a/forms/login-api/src/main/java/org/keycloak/login/LoginFormsProvider.java b/forms/login-api/src/main/java/org/keycloak/login/LoginFormsProvider.java index 039a5bfa42..cb5f2be8cd 100755 --- a/forms/login-api/src/main/java/org/keycloak/login/LoginFormsProvider.java +++ b/forms/login-api/src/main/java/org/keycloak/login/LoginFormsProvider.java @@ -72,6 +72,14 @@ public interface LoginFormsProvider extends Provider { LoginFormsProvider addError(FormMessage errorMessage); + /** + * Add a success message to the form + * + * @param errorMessage + * @return + */ + LoginFormsProvider addSuccess(FormMessage errorMessage); + public LoginFormsProvider setSuccess(String message, Object ... parameters); public LoginFormsProvider setUser(UserModel user); diff --git a/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/FreeMarkerLoginFormsProvider.java b/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/FreeMarkerLoginFormsProvider.java index fc8d9b09f9..1de1c7e381 100755 --- a/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/FreeMarkerLoginFormsProvider.java +++ b/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/FreeMarkerLoginFormsProvider.java @@ -473,6 +473,21 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider { } + @Override + public LoginFormsProvider addSuccess(FormMessage errorMessage) { + if (this.messageType != MessageType.SUCCESS) { + this.messageType = null; + this.messages = null; + } + if (messages == null) { + this.messageType = MessageType.SUCCESS; + this.messages = new LinkedList<>(); + } + this.messages.add(errorMessage); + return this; + + } + @Override public FreeMarkerLoginFormsProvider setSuccess(String message, Object... parameters) { setMessage(MessageType.SUCCESS, message, parameters); diff --git a/model/api/src/main/java/org/keycloak/models/utils/FormMessage.java b/model/api/src/main/java/org/keycloak/models/utils/FormMessage.java old mode 100644 new mode 100755 index 6a93c51bcd..b840de6dee --- a/model/api/src/main/java/org/keycloak/models/utils/FormMessage.java +++ b/model/api/src/main/java/org/keycloak/models/utils/FormMessage.java @@ -34,6 +34,10 @@ public class FormMessage { this(field, message); this.parameters = parameters; } + + public FormMessage(String message, Object...parameters) { + this(null, message, parameters); + } /** * Create message without parameters. diff --git a/services/src/main/java/org/keycloak/authentication/AbstractAuthenticationFlowContext.java b/services/src/main/java/org/keycloak/authentication/AbstractAuthenticationFlowContext.java old mode 100644 new mode 100755 index 6069659fdd..9b80702f65 --- a/services/src/main/java/org/keycloak/authentication/AbstractAuthenticationFlowContext.java +++ b/services/src/main/java/org/keycloak/authentication/AbstractAuthenticationFlowContext.java @@ -10,6 +10,7 @@ import org.keycloak.models.AuthenticationExecutionModel; import org.keycloak.models.AuthenticatorConfigModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; +import org.keycloak.models.utils.FormMessage; import org.keycloak.services.managers.BruteForceProtector; /** @@ -79,11 +80,19 @@ public interface AbstractAuthenticationFlowContext { AuthenticatorConfigModel getAuthenticatorConfig(); /** - * This could be an error message forwarded from brokering when the broker failed authentication + * This could be an error message forwarded from another authenticator that is restarting or continuing the flo. For example + * the brokering API sends this when the broker failed authentication * and we want to continue authentication locally. forwardedErrorMessage can then be displayed by * whatever form is challenging. */ - String getForwardedErrorMessage(); + FormMessage getForwardedErrorMessage(); + + /** + * This could be an success message forwarded from another authenticator that is restarting or continuing the flow. For example + * a reset password sends an email, then resets the flow with a success message. forwardedSuccessMessage can then be displayed by + * whatever form is challenging. + */ + FormMessage getForwardedSuccessMessage(); /** * Generates access code and updates clientsession timestamp diff --git a/services/src/main/java/org/keycloak/authentication/AuthenticationFlowContext.java b/services/src/main/java/org/keycloak/authentication/AuthenticationFlowContext.java index 2194cae0a5..9b02eb89d0 100755 --- a/services/src/main/java/org/keycloak/authentication/AuthenticationFlowContext.java +++ b/services/src/main/java/org/keycloak/authentication/AuthenticationFlowContext.java @@ -4,6 +4,8 @@ import org.keycloak.login.LoginFormsProvider; import org.keycloak.models.ClientSessionModel; import org.keycloak.models.UserModel; import org.keycloak.models.UserSessionModel; +import org.keycloak.models.utils.FormMessage; + import java.net.URI; /** @@ -70,9 +72,34 @@ public interface AuthenticationFlowContext extends AbstractAuthenticationFlowCon void cancelLogin(); /** - * Abort the current flow and restart it using the realm's browser login + * Fork the current flow. The client session will be cloned and set to point at the realm's browser login flow. The Response will be the result + * of this fork. The previous flow will still be set at the current execution. This is used by reset password when it sends an email. + * It sends an email linking to the current flow and redirects the browser to a new browser login flow. + * + * * * @return */ - void resetBrowserLogin(); + void fork(); + + /** + * Fork the current flow. The client session will be cloned and set to point at the realm's browser login flow. The Response will be the result + * of this fork. The previous flow will still be set at the current execution. This is used by reset password when it sends an email. + * It sends an email linking to the current flow and redirects the browser to a new browser login flow. + * + * This method will set up a success message that will be displayed in the first page of the new flow + * + * @param message Corresponds to raw text or a message property defined in a message bundle + */ + void forkWithSuccessMessage(FormMessage message); + /** + * Fork the current flow. The client session will be cloned and set to point at the realm's browser login flow. The Response will be the result + * of this fork. The previous flow will still be set at the current execution. This is used by reset password when it sends an email. + * It sends an email linking to the current flow and redirects the browser to a new browser login flow. + * + * This method will set up an error message that will be displayed in the first page of the new flow + * + * @param message Corresponds to raw text or a message property defined in a message bundle + */ + void forkWithErrorMessage(FormMessage message); } diff --git a/services/src/main/java/org/keycloak/authentication/AuthenticationFlowError.java b/services/src/main/java/org/keycloak/authentication/AuthenticationFlowError.java index 53c700178b..8fd0faa89f 100755 --- a/services/src/main/java/org/keycloak/authentication/AuthenticationFlowError.java +++ b/services/src/main/java/org/keycloak/authentication/AuthenticationFlowError.java @@ -17,7 +17,7 @@ public enum AuthenticationFlowError { USER_TEMPORARILY_DISABLED, INTERNAL_ERROR, UNKNOWN_USER, - RESET_TO_BROWSER_LOGIN, + FORK_FLOW, UNKNOWN_CLIENT, CLIENT_NOT_FOUND, CLIENT_DISABLED, diff --git a/services/src/main/java/org/keycloak/authentication/AuthenticationProcessor.java b/services/src/main/java/org/keycloak/authentication/AuthenticationProcessor.java index 17ba77b485..f3dd3b8072 100755 --- a/services/src/main/java/org/keycloak/authentication/AuthenticationProcessor.java +++ b/services/src/main/java/org/keycloak/authentication/AuthenticationProcessor.java @@ -19,6 +19,7 @@ import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; import org.keycloak.models.UserSessionModel; +import org.keycloak.models.utils.FormMessage; import org.keycloak.protocol.LoginProtocol; import org.keycloak.protocol.oidc.TokenManager; import org.keycloak.services.ErrorPage; @@ -33,6 +34,7 @@ import javax.ws.rs.core.Response; import javax.ws.rs.core.UriInfo; import java.net.URI; import java.util.List; +import java.util.Map; /** * @author Bill Burke @@ -53,11 +55,14 @@ public class AuthenticationProcessor { protected String flowId; protected String flowPath; /** - * This could be an error message forwarded from brokering when the broker failed authentication - * and we want to continue authentication locally. forwardedErrorMessage can then be displayed by - * whatever form is challenging. + * This could be an error message forwarded from another authenticator */ - protected String forwardedErrorMessage; + protected FormMessage forwardedErrorMessage; + + /** + * This could be an success message forwarded from another authenticator + */ + protected FormMessage forwardedSuccessMessage; protected boolean userSessionCreated; // Used for client authentication @@ -157,11 +162,16 @@ public class AuthenticationProcessor { return this; } - public AuthenticationProcessor setForwardedErrorMessage(String forwardedErrorMessage) { + public AuthenticationProcessor setForwardedErrorMessage(FormMessage forwardedErrorMessage) { this.forwardedErrorMessage = forwardedErrorMessage; return this; } + public AuthenticationProcessor setForwardedSuccessMessage(FormMessage forwardedSuccessMessage) { + this.forwardedSuccessMessage = forwardedSuccessMessage; + return this; + } + public String generateCode() { ClientSessionCode accessCode = new ClientSessionCode(getRealm(), getClientSession()); clientSession.setTimestamp(Time.currentTime()); @@ -199,6 +209,8 @@ public class AuthenticationProcessor { Response challenge; AuthenticationFlowError error; List currentExecutions; + FormMessage errorMessage; + FormMessage successMessage; private Result(AuthenticationExecutionModel execution, Authenticator authenticator, List currentExecutions) { this.execution = execution; @@ -370,7 +382,7 @@ public class AuthenticationProcessor { } @Override - public String getForwardedErrorMessage() { + public FormMessage getForwardedErrorMessage() { return AuthenticationProcessor.this.forwardedErrorMessage; } @@ -398,7 +410,9 @@ public class AuthenticationProcessor { .setActionUri(action) .setClientSessionCode(accessCode); if (getForwardedErrorMessage() != null) { - provider.setError(getForwardedErrorMessage()); + provider.addError(getForwardedErrorMessage()); + } else if (getForwardedSuccessMessage() != null) { + provider.addSuccess(getForwardedSuccessMessage()); } return provider; } @@ -429,8 +443,35 @@ public class AuthenticationProcessor { } @Override - public void resetBrowserLogin() { - this.status = FlowStatus.RESET_BROWSER_LOGIN; + public void fork() { + this.status = FlowStatus.FORK; + } + + @Override + public void forkWithSuccessMessage(FormMessage message) { + this.status = FlowStatus.FORK; + this.successMessage = message; + + } + + @Override + public void forkWithErrorMessage(FormMessage message) { + this.status = FlowStatus.FORK; + this.errorMessage = message; + + } + + @Override + public FormMessage getForwardedSuccessMessage() { + return AuthenticationProcessor.this.forwardedSuccessMessage; + } + + public FormMessage getErrorMessage() { + return errorMessage; + } + + public FormMessage getSuccessMessage() { + return successMessage; } } @@ -456,31 +497,39 @@ public class AuthenticationProcessor { public Response handleBrowserException(Exception failure) { if (failure instanceof AuthenticationFlowException) { AuthenticationFlowException e = (AuthenticationFlowException) failure; - logger.error("failed authentication: " + e.getError().toString(), e); if (e.getError() == AuthenticationFlowError.INVALID_USER) { + logger.error("failed authentication: " + e.getError().toString(), e); event.error(Errors.USER_NOT_FOUND); return ErrorPage.error(session, Messages.INVALID_USER); } else if (e.getError() == AuthenticationFlowError.USER_DISABLED) { + logger.error("failed authentication: " + e.getError().toString(), e); event.error(Errors.USER_DISABLED); return ErrorPage.error(session, Messages.ACCOUNT_DISABLED); } else if (e.getError() == AuthenticationFlowError.USER_TEMPORARILY_DISABLED) { + logger.error("failed authentication: " + e.getError().toString(), e); event.error(Errors.USER_TEMPORARILY_DISABLED); return ErrorPage.error(session, Messages.ACCOUNT_TEMPORARILY_DISABLED); } else if (e.getError() == AuthenticationFlowError.INVALID_CLIENT_SESSION) { + logger.error("failed authentication: " + e.getError().toString(), e); event.error(Errors.INVALID_CODE); return ErrorPage.error(session, Messages.INVALID_CODE); } else if (e.getError() == AuthenticationFlowError.EXPIRED_CODE) { + logger.error("failed authentication: " + e.getError().toString(), e); event.error(Errors.EXPIRED_CODE); return ErrorPage.error(session, Messages.EXPIRED_CODE); - } else if (e.getError() == AuthenticationFlowError.RESET_TO_BROWSER_LOGIN) { - resetFlow(getClientSession()); + } else if (e.getError() == AuthenticationFlowError.FORK_FLOW) { + ForkFlowException reset = (ForkFlowException)e; + ClientSessionModel clone = clone(session, clientSession); + clone.setAction(ClientSessionModel.Action.AUTHENTICATE.name()); AuthenticationProcessor processor = new AuthenticationProcessor(); - processor.setClientSession(clientSession) + processor.setClientSession(clone) .setFlowPath(LoginActionsService.AUTHENTICATE_PATH) .setFlowId(realm.getBrowserFlow().getId()) + .setForwardedErrorMessage(reset.getErrorMessage()) + .setForwardedSuccessMessage(reset.getSuccessMessage()) .setConnection(connection) .setEventBuilder(event) .setProtector(protector) @@ -491,6 +540,7 @@ public class AuthenticationProcessor { return processor.authenticate(); } else { + logger.error("failed authentication: " + e.getError().toString(), e); event.error(Errors.INVALID_USER_CREDENTIALS); return ErrorPage.error(session, Messages.INVALID_USER); } @@ -586,6 +636,20 @@ public class AuthenticationProcessor { clientSession.removeNote(CURRENT_AUTHENTICATION_EXECUTION); } + public static ClientSessionModel clone(KeycloakSession session, ClientSessionModel clientSession) { + ClientSessionModel clone = session.sessions().createClientSession(clientSession.getRealm(), clientSession.getClient()); + for (Map.Entry entry : clientSession.getNotes().entrySet()) { + clone.setNote(entry.getKey(), entry.getValue()); + } + clone.setRedirectUri(clientSession.getRedirectUri()); + clone.setAuthMethod(clientSession.getAuthMethod()); + clone.setTimestamp(Time.currentTime()); + clone.removeNote(AuthenticationProcessor.CURRENT_AUTHENTICATION_EXECUTION); + return clone; + + } + + public Response authenticationAction(String execution) { checkClientSession(); String current = clientSession.getNote(CURRENT_AUTHENTICATION_EXECUTION); @@ -710,4 +774,6 @@ public class AuthenticationProcessor { } + + } diff --git a/services/src/main/java/org/keycloak/authentication/DefaultAuthenticationFlow.java b/services/src/main/java/org/keycloak/authentication/DefaultAuthenticationFlow.java index df9a26bb02..cf825c0a07 100755 --- a/services/src/main/java/org/keycloak/authentication/DefaultAuthenticationFlow.java +++ b/services/src/main/java/org/keycloak/authentication/DefaultAuthenticationFlow.java @@ -166,9 +166,10 @@ public class DefaultAuthenticationFlow implements AuthenticationFlow { return sendChallenge(result, execution); } throw new AuthenticationFlowException(result.getError()); - } else if (status == FlowStatus.RESET_BROWSER_LOGIN) { + } else if (status == FlowStatus.FORK) { AuthenticationProcessor.logger.debugv("reset browser login from authenticator: {0}", execution.getAuthenticator()); - throw new AuthenticationFlowException(AuthenticationFlowError.RESET_TO_BROWSER_LOGIN); + processor.getClientSession().setNote(AuthenticationProcessor.CURRENT_AUTHENTICATION_EXECUTION, execution.getId()); + throw new ForkFlowException(result.getSuccessMessage(), result.getErrorMessage()); } else if (status == FlowStatus.FORCE_CHALLENGE) { processor.getClientSession().setExecutionStatus(execution.getId(), ClientSessionModel.ExecutionStatus.CHALLENGED); return sendChallenge(result, execution); diff --git a/services/src/main/java/org/keycloak/authentication/FlowStatus.java b/services/src/main/java/org/keycloak/authentication/FlowStatus.java index 31b7eee7c7..6e6ecbd517 100755 --- a/services/src/main/java/org/keycloak/authentication/FlowStatus.java +++ b/services/src/main/java/org/keycloak/authentication/FlowStatus.java @@ -45,9 +45,9 @@ public enum FlowStatus { ATTEMPTED, /** - * Aborting this flow and starting the realm's browser flow from the beginning + * This flow is being forked. The current client session is being cloned, reset, and redirected to browser login. * */ - RESET_BROWSER_LOGIN + FORK } diff --git a/services/src/main/java/org/keycloak/authentication/ForkFlowException.java b/services/src/main/java/org/keycloak/authentication/ForkFlowException.java new file mode 100755 index 0000000000..6cc7d4d0c8 --- /dev/null +++ b/services/src/main/java/org/keycloak/authentication/ForkFlowException.java @@ -0,0 +1,29 @@ +package org.keycloak.authentication; + +import org.keycloak.models.utils.FormMessage; + +/** + * Thrown internally when authenticator wants to fork the current flow. + * + * + * @author Bill Burke + * @version $Revision: 1 $ + */ +public class ForkFlowException extends AuthenticationFlowException { + protected FormMessage successMessage; + protected FormMessage errorMessage; + + public FormMessage getSuccessMessage() { + return successMessage; + } + + public FormMessage getErrorMessage() { + return errorMessage; + } + + public ForkFlowException(FormMessage successMessage, FormMessage errorMessage) { + super(AuthenticationFlowError.FORK_FLOW); + this.successMessage = successMessage; + this.errorMessage = errorMessage; + } +} diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/resetcred/ResetCredentialEmail.java b/services/src/main/java/org/keycloak/authentication/authenticators/resetcred/ResetCredentialEmail.java index f5e0165928..05055e97b2 100755 --- a/services/src/main/java/org/keycloak/authentication/authenticators/resetcred/ResetCredentialEmail.java +++ b/services/src/main/java/org/keycloak/authentication/authenticators/resetcred/ResetCredentialEmail.java @@ -1,7 +1,6 @@ package org.keycloak.authentication.authenticators.resetcred; import org.jboss.logging.Logger; -import org.keycloak.ClientConnection; import org.keycloak.Config; import org.keycloak.authentication.AuthenticationFlowContext; import org.keycloak.authentication.AuthenticationFlowError; @@ -14,24 +13,16 @@ import org.keycloak.events.Details; import org.keycloak.events.Errors; import org.keycloak.events.EventBuilder; import org.keycloak.events.EventType; -import org.keycloak.jose.jws.JWSBuilder; -import org.keycloak.jose.jws.JWSInput; -import org.keycloak.login.LoginFormsProvider; import org.keycloak.models.AuthenticationExecutionModel; -import org.keycloak.models.ClientSessionModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSessionFactory; import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; -import org.keycloak.models.UserSessionModel; +import org.keycloak.models.utils.FormMessage; import org.keycloak.models.utils.HmacOTP; -import org.keycloak.protocol.oidc.TokenManager; import org.keycloak.provider.ProviderConfigProperty; -import org.keycloak.services.Urls; import org.keycloak.services.messages.Messages; -import javax.crypto.SecretKey; -import javax.ws.rs.core.MultivaluedMap; import javax.ws.rs.core.Response; import javax.ws.rs.core.UriBuilder; import java.util.List; @@ -54,12 +45,9 @@ public class ResetCredentialEmail implements Authenticator, AuthenticatorFactory String username = context.getClientSession().getNote(AbstractUsernameFormAuthenticator.ATTEMPTED_USERNAME); // we don't want people guessing usernames, so if there was a problem obtaining the user, the user will be null. - // just redisplay this form + // just reset login for with a success message if (user == null) { - Response challenge = context.form() - .setSuccess(Messages.EMAIL_SENT) - .createForm("validate-reset-email.ftl"); - context.challenge(challenge); + context.forkWithSuccessMessage(new FormMessage(Messages.EMAIL_SENT)); return; } @@ -70,10 +58,8 @@ public class ResetCredentialEmail implements Authenticator, AuthenticatorFactory event.user(user) .detail(Details.USERNAME, username) .error(Errors.INVALID_EMAIL); - Response challenge = context.form() - .setSuccess(Messages.EMAIL_SENT) - .createForm("validate-reset-email.ftl"); - context.challenge(challenge); + + context.forkWithSuccessMessage(new FormMessage(Messages.EMAIL_SENT)); return; } @@ -85,15 +71,12 @@ public class ResetCredentialEmail implements Authenticator, AuthenticatorFactory long expiration = TimeUnit.SECONDS.toMinutes(context.getRealm().getAccessCodeLifespanUserAction()); try { - context.getSession().getProvider(EmailProvider.class).setRealm(context.getRealm()).setUser(user).sendPasswordReset(secret, link, expiration); + context.getSession().getProvider(EmailProvider.class).setRealm(context.getRealm()).setUser(user).sendPasswordReset(link, expiration); event.clone().event(EventType.SEND_RESET_PASSWORD) .user(user) .detail(Details.USERNAME, username) .detail(Details.EMAIL, user.getEmail()).detail(Details.CODE_ID, context.getClientSession().getId()).success(); - Response challenge = context.form() - .setSuccess(Messages.EMAIL_SENT) - .createForm("validate-reset-email.ftl"); - context.challenge(challenge); + context.forkWithSuccessMessage(new FormMessage(Messages.EMAIL_SENT)); } catch (EmailException e) { event.clone().event(EventType.SEND_RESET_PASSWORD) .detail(Details.USERNAME, username) @@ -110,19 +93,7 @@ public class ResetCredentialEmail implements Authenticator, AuthenticatorFactory @Override public void action(AuthenticationFlowContext context) { String secret = context.getClientSession().getNote(RESET_CREDENTIAL_SECRET); - String key = null; - if (context.getHttpRequest().getHttpMethod().equalsIgnoreCase("GET")) { - key =context.getUriInfo().getQueryParameters().getFirst(KEY); - - } else if (context.getHttpRequest().getHttpMethod().equalsIgnoreCase("POST")) { - MultivaluedMap formData = context.getHttpRequest().getDecodedFormParameters(); - if (formData.containsKey("cancel")) { - context.resetBrowserLogin(); - return; - } - - key = formData.getFirst(KEY); - } + String key = context.getUriInfo().getQueryParameters().getFirst(KEY); // Can only guess once! We remove the note so another guess can't happen context.getClientSession().removeNote(RESET_CREDENTIAL_SECRET); diff --git a/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java b/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java index 86fe75b40c..5ee81912f3 100755 --- a/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java @@ -3,6 +3,7 @@ package org.keycloak.protocol.oidc; import org.jboss.logging.Logger; import org.keycloak.ClientConnection; import org.keycloak.OAuthErrorException; +import org.keycloak.authentication.AuthenticationProcessor; import org.keycloak.events.Details; import org.keycloak.events.EventBuilder; import org.keycloak.jose.jws.JWSBuilder; @@ -225,6 +226,7 @@ public class TokenManager { } + public static void dettachClientSession(UserSessionProvider sessions, RealmModel realm, ClientSessionModel clientSession) { UserSessionModel userSession = clientSession.getUserSession(); if (userSession == null) { diff --git a/services/src/main/java/org/keycloak/services/resources/IdentityBrokerService.java b/services/src/main/java/org/keycloak/services/resources/IdentityBrokerService.java index 15c5f3800c..beec6642d7 100755 --- a/services/src/main/java/org/keycloak/services/resources/IdentityBrokerService.java +++ b/services/src/main/java/org/keycloak/services/resources/IdentityBrokerService.java @@ -46,6 +46,7 @@ import org.keycloak.models.RoleModel; import org.keycloak.models.UserModel; import org.keycloak.models.UserSessionModel; import org.keycloak.models.utils.DefaultAuthenticationFlows; +import org.keycloak.models.utils.FormMessage; import org.keycloak.protocol.oidc.TokenManager; import org.keycloak.provider.ProviderFactory; import org.keycloak.representations.AccessToken; @@ -482,7 +483,7 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal .setSession(session) .setUriInfo(uriInfo) .setRequest(request); - if (errorMessage != null) processor.setForwardedErrorMessage(errorMessage); + if (errorMessage != null) processor.setForwardedErrorMessage(new FormMessage(null, errorMessage)); try { return processor.authenticate(); 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 f097a3da31..dd2c90daf7 100755 --- a/services/src/main/java/org/keycloak/services/resources/LoginActionsService.java +++ b/services/src/main/java/org/keycloak/services/resources/LoginActionsService.java @@ -301,11 +301,11 @@ public class LoginActionsService { .setConnection(clientConnection) .setEventBuilder(event) .setProtector(authManager.getProtector()) - .setForwardedErrorMessage(errorMessage) .setRealm(realm) .setSession(session) .setUriInfo(uriInfo) .setRequest(request); + if (errorMessage != null) processor.setForwardedErrorMessage(new FormMessage(null, errorMessage)); try { if (execution != null) { 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 c8e8e4cfc5..04b0c65769 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 @@ -119,9 +119,6 @@ public class ResetPasswordTest { @WebResource protected LoginPasswordResetPage resetPasswordPage; - @WebResource - protected ValidatePassworrdEmailResetPage validateResetPage; - @WebResource protected LoginPasswordUpdatePage updatePasswordPage; @@ -148,48 +145,6 @@ public class ResetPasswordTest { resetPassword("login-test"); } - @Test - public void resetPasswordCancel() throws IOException, MessagingException { - loginPage.open(); - loginPage.resetPassword(); - - resetPasswordPage.assertCurrent(); - - resetPasswordPage.changePassword("login-test"); - - validateResetPage.assertCurrent(); - - events.expectRequiredAction(EventType.SEND_RESET_PASSWORD) - .session((String)null) - .user(userId).detail(Details.USERNAME, "login-test").detail(Details.EMAIL, "login@test.com").assertEvent().getSessionId(); - - validateResetPage.cancel(); - - assertTrue(loginPage.isCurrent()); - - loginPage.login("login-test", "password"); - - String currentUrl = driver.getCurrentUrl(); - String src = driver.getPageSource(); - - System.out.println("currentUrl: " + currentUrl); - - events.expectLogin().user(userId).detail(Details.USERNAME, "login-test").assertEvent(); - - assertEquals(1, greenMail.getReceivedMessages().length); - - MimeMessage message = greenMail.getReceivedMessages()[0]; - - final String changePasswordUrl = getPasswordResetEmailLink(message); - - driver.navigate().to(changePasswordUrl.trim()); - - events.expect(EventType.RESET_PASSWORD_ERROR).client((String) null).user((String) null).error("invalid_code").clearDetails().assertEvent(); - - assertTrue(errorPage.isCurrent()); - assertEquals("An error occurred, please login again through your application.", errorPage.getError()); - } - @Test public void resetPasswordCancelChangeUser() throws IOException, MessagingException { loginPage.open(); @@ -199,15 +154,13 @@ public class ResetPasswordTest { resetPasswordPage.changePassword("test-user@localhost"); - validateResetPage.assertCurrent(); + loginPage.assertCurrent(); + assertEquals("You should receive an email shortly with further instructions.", loginPage.getSuccessMessage()); events.expectRequiredAction(EventType.SEND_RESET_PASSWORD).detail(Details.USERNAME, "test-user@localhost") .session((String) null) .detail(Details.EMAIL, "test-user@localhost").assertEvent(); - validateResetPage.cancel(); - - assertTrue(loginPage.isCurrent()); loginPage.login("login@test.com", "password"); @@ -235,7 +188,8 @@ public class ResetPasswordTest { resetPasswordPage.changePassword(username); - validateResetPage.assertCurrent(); + loginPage.assertCurrent(); + assertEquals("You should receive an email shortly with further instructions.", loginPage.getSuccessMessage()); events.expectRequiredAction(EventType.SEND_RESET_PASSWORD) .user(userId) @@ -244,8 +198,6 @@ public class ResetPasswordTest { .session((String)null) .assertEvent(); - assertEquals("You should receive an email shortly with further instructions.", resetPasswordPage.getSuccessMessage()); - assertEquals(1, greenMail.getReceivedMessages().length); MimeMessage message = greenMail.getReceivedMessages()[0]; @@ -285,13 +237,12 @@ public class ResetPasswordTest { resetPasswordPage.changePassword(username); - validateResetPage.assertCurrent(); + loginPage.assertCurrent(); + assertEquals("You should receive an email shortly with further instructions.", loginPage.getSuccessMessage()); events.expectRequiredAction(EventType.SEND_RESET_PASSWORD).user(userId).session((String)null) .detail(Details.USERNAME, username).detail(Details.EMAIL, "login@test.com").assertEvent(); - assertEquals("You should receive an email shortly with further instructions.", resetPasswordPage.getSuccessMessage()); - MimeMessage message = greenMail.getReceivedMessages()[greenMail.getReceivedMessages().length - 1]; String changePasswordUrl = getPasswordResetEmailLink(message); @@ -322,13 +273,12 @@ public class ResetPasswordTest { resetPasswordPage.changePassword(username); - validateResetPage.assertCurrent(); + loginPage.assertCurrent(); + assertEquals("You should receive an email shortly with further instructions.", loginPage.getSuccessMessage()); events.expectRequiredAction(EventType.SEND_RESET_PASSWORD).user(userId).session((String)null) .detail(Details.USERNAME, username).detail(Details.EMAIL, "login@test.com").assertEvent(); - assertEquals("You should receive an email shortly with further instructions.", resetPasswordPage.getSuccessMessage()); - MimeMessage message = greenMail.getReceivedMessages()[greenMail.getReceivedMessages().length - 1]; String changePasswordUrl = getPasswordResetEmailLink(message); @@ -352,9 +302,8 @@ public class ResetPasswordTest { resetPasswordPage.changePassword("invalid"); - validateResetPage.assertCurrent(); - - assertEquals("You should receive an email shortly with further instructions.", resetPasswordPage.getSuccessMessage()); + loginPage.assertCurrent(); + assertEquals("You should receive an email shortly with further instructions.", loginPage.getSuccessMessage()); assertEquals(0, greenMail.getReceivedMessages().length); @@ -390,14 +339,13 @@ public class ResetPasswordTest { resetPasswordPage.changePassword("login-test"); - validateResetPage.assertCurrent(); + loginPage.assertCurrent(); + assertEquals("You should receive an email shortly with further instructions.", loginPage.getSuccessMessage()); events.expectRequiredAction(EventType.SEND_RESET_PASSWORD) .session((String)null) .user(userId).detail(Details.USERNAME, "login-test").detail(Details.EMAIL, "login@test.com").assertEvent(); - assertEquals("You should receive an email shortly with further instructions.", resetPasswordPage.getSuccessMessage()); - assertEquals(1, greenMail.getReceivedMessages().length); MimeMessage message = greenMail.getReceivedMessages()[0]; @@ -435,9 +383,8 @@ public class ResetPasswordTest { resetPasswordPage.changePassword("login-test"); - validateResetPage.assertCurrent(); - - assertEquals("You should receive an email shortly with further instructions.", resetPasswordPage.getSuccessMessage()); + loginPage.assertCurrent(); + assertEquals("You should receive an email shortly with further instructions.", loginPage.getSuccessMessage()); assertEquals(0, greenMail.getReceivedMessages().length); @@ -473,9 +420,8 @@ public class ResetPasswordTest { resetPasswordPage.changePassword("login-test"); - validateResetPage.assertCurrent(); - - assertEquals("You should receive an email shortly with further instructions.", resetPasswordPage.getSuccessMessage()); + loginPage.assertCurrent(); + assertEquals("You should receive an email shortly with further instructions.", loginPage.getSuccessMessage()); assertEquals(0, greenMail.getReceivedMessages().length); @@ -544,9 +490,8 @@ public class ResetPasswordTest { resetPasswordPage.changePassword("login-test"); - validateResetPage.assertCurrent(); - - assertEquals("You should receive an email shortly with further instructions.", resetPasswordPage.getSuccessMessage()); + loginPage.assertCurrent(); + assertEquals("You should receive an email shortly with further instructions.", loginPage.getSuccessMessage()); assertEquals(1, greenMail.getReceivedMessages().length); @@ -626,81 +571,6 @@ public class ResetPasswordTest { } } - @Test - public void resetPasswordByCode() throws IOException, MessagingException { - try { - String username = "login@test.com"; - loginPage.open(); - loginPage.resetPassword(); - - resetPasswordPage.assertCurrent(); - - resetPasswordPage.changePassword(username); - - validateResetPage.assertCurrent(); - - events.expectRequiredAction(EventType.SEND_RESET_PASSWORD) - .user(userId) - .detail(Details.USERNAME, username) - .detail(Details.EMAIL, "login@test.com") - .session((String) null) - .assertEvent(); - - assertEquals("You should receive an email shortly with further instructions.", resetPasswordPage.getSuccessMessage()); - - assertEquals(1, greenMail.getReceivedMessages().length); - - MimeMessage message = greenMail.getReceivedMessages()[0]; - - String code = getTemporaryCode(message); - - validateResetPage.submitCode(code); - - updatePasswordPage.assertCurrent(); - - updatePasswordPage.changePassword("resetPassword", "resetPassword"); - - String sessionId = events.expectRequiredAction(EventType.UPDATE_PASSWORD).user(userId).detail(Details.USERNAME, username).assertEvent().getSessionId(); - - assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType()); - - events.expectLogin().user(userId).detail(Details.USERNAME, username).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()); - - } finally { - - } - } - - - private String getTemporaryCode(MimeMessage message) throws IOException, MessagingException { - Multipart multipart = (Multipart) message.getContent(); - - final String textContentType = multipart.getBodyPart(0).getContentType(); - - assertEquals("text/plain; charset=UTF-8", textContentType); - - final String textBody = (String) multipart.getBodyPart(0).getContent(); - Pattern pattern = Pattern.compile("Temporary Code: ([^\\s]*)"); - Matcher matcher = pattern.matcher(textBody); - if (matcher.find()) { - return matcher.group(1); - } - return null; - } - - private String getPasswordResetEmailLink(MimeMessage message) throws IOException, MessagingException { Multipart multipart = (Multipart) message.getContent(); diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/LoginPage.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/LoginPage.java index 2ea8b62ffe..a0bd5a4a30 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/LoginPage.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/LoginPage.java @@ -68,6 +68,10 @@ public class LoginPage extends AbstractPage { @FindBy(className = "feedback-warning") private WebElement loginWarningMessage; + @FindBy(className = "feedback-success") + private WebElement emailSuccessMessage; + + @FindBy(id = "kc-current-locale-link") private WebElement languageText; @@ -116,6 +120,11 @@ public class LoginPage extends AbstractPage { return loginErrorMessage != null ? loginErrorMessage.getText() : null; } + public String getSuccessMessage() { + return emailSuccessMessage != null ? emailSuccessMessage.getText() : null; + } + + public boolean isCurrent() { return driver.getTitle().equals("Log in to test") || driver.getTitle().equals("Anmeldung bei test"); }