Merge pull request #1552 from patriot1burke/master

change reset password
This commit is contained in:
Bill Burke 2015-08-20 18:52:07 -04:00
commit b9d0219f04
24 changed files with 231 additions and 222 deletions

View file

@ -81,9 +81,7 @@ emailVerifyInstruction3=to re-send the email.
backToLogin=« Back to Login 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. 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: copyCodeInstruction=Please copy this code and paste it into your application:

View file

@ -1,5 +1,5 @@
<html> <html>
<body> <body>
${msg("passwordResetBodyHtml",link, linkExpiration, realmName, code)} ${msg("passwordResetBodyHtml",link, linkExpiration, realmName)}
</body> </body>
</html> </html>

View file

@ -1,7 +1,7 @@
emailVerificationSubject=E-Mail verifizieren emailVerificationSubject=E-Mail verifizieren
passwordResetSubject=Passwort zur\u00FCckzusetzen 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. 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=<p>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</p><p><a href="{0}">{0}</a></p><p>Temporary code: {3}<p>This link will expire within {1} minutes.</p><p>If you don''t want to reset your password, just ignore this message and nothing will be changed.</p> passwordResetBodyHtml=<p>Someone just requested to change your {2} account''s password. If this was you, click on the link below to set a new password.</p><p><a href="{0}">{0}</a></p><p>This link will expire within {1} minutes.</p><p>If you don''t want to reset your password, just ignore this message and nothing will be changed.</p>
changePasswordSubject=Change password 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. 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=<p>Your adminstrator has just requested that you change your {2} account''s password. Click on the link below to set a new password</p><p><a href="{0}">{0}</a></p><p>This link will expire within {1} minutes.</p><p>If you don''t want to reset your password, just ignore this message and nothing will be changed.</p> changePasswordBodyHtml=<p>Your adminstrator has just requested that you change your {2} account''s password. Click on the link below to set a new password</p><p><a href="{0}">{0}</a></p><p>This link will expire within {1} minutes.</p><p>If you don''t want to reset your password, just ignore this message and nothing will be changed.</p>

View file

@ -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. 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=<p>Someone has created a {2} account with this email address. If this was you, click the link below to verify your email address</p><p><a href="{0}">{0}</a></p><p>This link will expire within {1} minutes.</p><p>If you didn''t create this account, just ignore this message.</p> emailVerificationBodyHtml=<p>Someone has created a {2} account with this email address. If this was you, click the link below to verify your email address</p><p><a href="{0}">{0}</a></p><p>This link will expire within {1} minutes.</p><p>If you didn''t create this account, just ignore this message.</p>
passwordResetSubject=Reset password 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. 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=<p>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</p><p><a href="{0}">{0}</a></p><p>Temporary code: {3}<p>This link will expire within {1} minutes.</p><p>If you don''t want to reset your password, just ignore this message and nothing will be changed.</p> passwordResetBodyHtml=<p>Someone just requested to change your {2} account''s password. If this was you, click on the link below to set a new password.</p><p><a href="{0}">{0}</a></p><p>This link will expire within {1} minutes.</p><p>If you don''t want to reset your password, just ignore this message and nothing will be changed.</p>
changePasswordSubject=Change password 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. 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=<p>Your adminstrator has just requested that you change your {2} account''s password. Click on the link below to set a new password</p><p><a href="{0}">{0}</a></p><p>This link will expire within {1} minutes.</p><p>If you don''t want to reset your password, just ignore this message and nothing will be changed.</p> changePasswordBodyHtml=<p>Your adminstrator has just requested that you change your {2} account''s password. Click on the link below to set a new password</p><p><a href="{0}">{0}</a></p><p>This link will expire within {1} minutes.</p><p>If you don''t want to reset your password, just ignore this message and nothing will be changed.</p>

View file

@ -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. 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=<p>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</p><p><a href="{0}">{0}</a></p><p>Este link ir\u00E1 expirar dentro de {1} minutos.</p><p>Se n\u00E3o foi voc\u00EA que criou esta conta, basta ignorar esta mensagem.</p> emailVerificationBodyHtml=<p>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</p><p><a href="{0}">{0}</a></p><p>Este link ir\u00E1 expirar dentro de {1} minutos.</p><p>Se n\u00E3o foi voc\u00EA que criou esta conta, basta ignorar esta mensagem.</p>
passwordResetSubject=Redefini\u00E7\u00E3o de senha 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. 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=<p>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</p><p><a href="{0}">{0}</a></p><p>Temporary code: {3}<p>This link will expire within {1} minutes.</p><p>If you don''t want to reset your password, just ignore this message and nothing will be changed.</p> passwordResetBodyHtml=<p>Someone just requested to change your {2} account''s password. If this was you, click on the link below to set a new password.</p><p><a href="{0}">{0}</a></p><p>This link will expire within {1} minutes.</p><p>If you don''t want to reset your password, just ignore this message and nothing will be changed.</p>
changePasswordSubject=Change password 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. 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=<p>Your adminstrator has just requested that you change your {2} account''s password. Click on the link below to set a new password</p><p><a href="{0}">{0}</a></p><p>This link will expire within {1} minutes.</p><p>If you don''t want to reset your password, just ignore this message and nothing will be changed.</p> changePasswordBodyHtml=<p>Your adminstrator has just requested that you change your {2} account''s password. Click on the link below to set a new password</p><p><a href="{0}">{0}</a></p><p>This link will expire within {1} minutes.</p><p>If you don''t want to reset your password, just ignore this message and nothing will be changed.</p>

View file

@ -1 +1 @@
${msg("passwordResetBody",link, linkExpiration, realmName, code)} ${msg("passwordResetBody",link, linkExpiration, realmName)}

View file

@ -24,7 +24,7 @@ public interface EmailProvider extends Provider {
* @param expirationInMinutes * @param expirationInMinutes
* @throws EmailException * @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 * Change password email requested by admin

View file

@ -70,11 +70,10 @@ public class FreeMarkerEmailProvider implements EmailProvider {
} }
@Override @Override
public void sendPasswordReset(String code, String link, long expirationInMinutes) throws EmailException { public void sendPasswordReset(String link, long expirationInMinutes) throws EmailException {
Map<String, Object> attributes = new HashMap<String, Object>(); Map<String, Object> attributes = new HashMap<String, Object>();
attributes.put("link", link); attributes.put("link", link);
attributes.put("linkExpiration", expirationInMinutes); attributes.put("linkExpiration", expirationInMinutes);
attributes.put("code", code);
String realmName = realm.getName().substring(0, 1).toUpperCase() + realm.getName().substring(1); String realmName = realm.getName().substring(0, 1).toUpperCase() + realm.getName().substring(1);
attributes.put("realmName", realmName); attributes.put("realmName", realmName);

View file

@ -72,6 +72,14 @@ public interface LoginFormsProvider extends Provider {
LoginFormsProvider addError(FormMessage errorMessage); 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 setSuccess(String message, Object ... parameters);
public LoginFormsProvider setUser(UserModel user); public LoginFormsProvider setUser(UserModel user);

View file

@ -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 @Override
public FreeMarkerLoginFormsProvider setSuccess(String message, Object... parameters) { public FreeMarkerLoginFormsProvider setSuccess(String message, Object... parameters) {
setMessage(MessageType.SUCCESS, message, parameters); setMessage(MessageType.SUCCESS, message, parameters);

View file

@ -35,6 +35,10 @@ public class FormMessage {
this.parameters = parameters; this.parameters = parameters;
} }
public FormMessage(String message, Object...parameters) {
this(null, message, parameters);
}
/** /**
* Create message without parameters. * Create message without parameters.
* *

View file

@ -10,6 +10,7 @@ import org.keycloak.models.AuthenticationExecutionModel;
import org.keycloak.models.AuthenticatorConfigModel; import org.keycloak.models.AuthenticatorConfigModel;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel; import org.keycloak.models.RealmModel;
import org.keycloak.models.utils.FormMessage;
import org.keycloak.services.managers.BruteForceProtector; import org.keycloak.services.managers.BruteForceProtector;
/** /**
@ -79,11 +80,19 @@ public interface AbstractAuthenticationFlowContext {
AuthenticatorConfigModel getAuthenticatorConfig(); 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 * and we want to continue authentication locally. forwardedErrorMessage can then be displayed by
* whatever form is challenging. * 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 * Generates access code and updates clientsession timestamp

View file

@ -4,6 +4,8 @@ import org.keycloak.login.LoginFormsProvider;
import org.keycloak.models.ClientSessionModel; import org.keycloak.models.ClientSessionModel;
import org.keycloak.models.UserModel; import org.keycloak.models.UserModel;
import org.keycloak.models.UserSessionModel; import org.keycloak.models.UserSessionModel;
import org.keycloak.models.utils.FormMessage;
import java.net.URI; import java.net.URI;
/** /**
@ -70,9 +72,34 @@ public interface AuthenticationFlowContext extends AbstractAuthenticationFlowCon
void cancelLogin(); 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 * @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);
} }

View file

@ -17,7 +17,7 @@ public enum AuthenticationFlowError {
USER_TEMPORARILY_DISABLED, USER_TEMPORARILY_DISABLED,
INTERNAL_ERROR, INTERNAL_ERROR,
UNKNOWN_USER, UNKNOWN_USER,
RESET_TO_BROWSER_LOGIN, FORK_FLOW,
UNKNOWN_CLIENT, UNKNOWN_CLIENT,
CLIENT_NOT_FOUND, CLIENT_NOT_FOUND,
CLIENT_DISABLED, CLIENT_DISABLED,

View file

@ -19,6 +19,7 @@ import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel; import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel; import org.keycloak.models.UserModel;
import org.keycloak.models.UserSessionModel; import org.keycloak.models.UserSessionModel;
import org.keycloak.models.utils.FormMessage;
import org.keycloak.protocol.LoginProtocol; import org.keycloak.protocol.LoginProtocol;
import org.keycloak.protocol.oidc.TokenManager; import org.keycloak.protocol.oidc.TokenManager;
import org.keycloak.services.ErrorPage; import org.keycloak.services.ErrorPage;
@ -33,6 +34,7 @@ import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriInfo; import javax.ws.rs.core.UriInfo;
import java.net.URI; import java.net.URI;
import java.util.List; import java.util.List;
import java.util.Map;
/** /**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a> * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
@ -53,11 +55,14 @@ public class AuthenticationProcessor {
protected String flowId; protected String flowId;
protected String flowPath; protected String flowPath;
/** /**
* This could be an error message forwarded from brokering when the broker failed authentication * This could be an error message forwarded from another authenticator
* and we want to continue authentication locally. forwardedErrorMessage can then be displayed by
* whatever form is challenging.
*/ */
protected String forwardedErrorMessage; protected FormMessage forwardedErrorMessage;
/**
* This could be an success message forwarded from another authenticator
*/
protected FormMessage forwardedSuccessMessage;
protected boolean userSessionCreated; protected boolean userSessionCreated;
// Used for client authentication // Used for client authentication
@ -157,11 +162,16 @@ public class AuthenticationProcessor {
return this; return this;
} }
public AuthenticationProcessor setForwardedErrorMessage(String forwardedErrorMessage) { public AuthenticationProcessor setForwardedErrorMessage(FormMessage forwardedErrorMessage) {
this.forwardedErrorMessage = forwardedErrorMessage; this.forwardedErrorMessage = forwardedErrorMessage;
return this; return this;
} }
public AuthenticationProcessor setForwardedSuccessMessage(FormMessage forwardedSuccessMessage) {
this.forwardedSuccessMessage = forwardedSuccessMessage;
return this;
}
public String generateCode() { public String generateCode() {
ClientSessionCode accessCode = new ClientSessionCode(getRealm(), getClientSession()); ClientSessionCode accessCode = new ClientSessionCode(getRealm(), getClientSession());
clientSession.setTimestamp(Time.currentTime()); clientSession.setTimestamp(Time.currentTime());
@ -199,6 +209,8 @@ public class AuthenticationProcessor {
Response challenge; Response challenge;
AuthenticationFlowError error; AuthenticationFlowError error;
List<AuthenticationExecutionModel> currentExecutions; List<AuthenticationExecutionModel> currentExecutions;
FormMessage errorMessage;
FormMessage successMessage;
private Result(AuthenticationExecutionModel execution, Authenticator authenticator, List<AuthenticationExecutionModel> currentExecutions) { private Result(AuthenticationExecutionModel execution, Authenticator authenticator, List<AuthenticationExecutionModel> currentExecutions) {
this.execution = execution; this.execution = execution;
@ -370,7 +382,7 @@ public class AuthenticationProcessor {
} }
@Override @Override
public String getForwardedErrorMessage() { public FormMessage getForwardedErrorMessage() {
return AuthenticationProcessor.this.forwardedErrorMessage; return AuthenticationProcessor.this.forwardedErrorMessage;
} }
@ -398,7 +410,9 @@ public class AuthenticationProcessor {
.setActionUri(action) .setActionUri(action)
.setClientSessionCode(accessCode); .setClientSessionCode(accessCode);
if (getForwardedErrorMessage() != null) { if (getForwardedErrorMessage() != null) {
provider.setError(getForwardedErrorMessage()); provider.addError(getForwardedErrorMessage());
} else if (getForwardedSuccessMessage() != null) {
provider.addSuccess(getForwardedSuccessMessage());
} }
return provider; return provider;
} }
@ -429,8 +443,35 @@ public class AuthenticationProcessor {
} }
@Override @Override
public void resetBrowserLogin() { public void fork() {
this.status = FlowStatus.RESET_BROWSER_LOGIN; 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) { public Response handleBrowserException(Exception failure) {
if (failure instanceof AuthenticationFlowException) { if (failure instanceof AuthenticationFlowException) {
AuthenticationFlowException e = (AuthenticationFlowException) failure; AuthenticationFlowException e = (AuthenticationFlowException) failure;
logger.error("failed authentication: " + e.getError().toString(), e);
if (e.getError() == AuthenticationFlowError.INVALID_USER) { if (e.getError() == AuthenticationFlowError.INVALID_USER) {
logger.error("failed authentication: " + e.getError().toString(), e);
event.error(Errors.USER_NOT_FOUND); event.error(Errors.USER_NOT_FOUND);
return ErrorPage.error(session, Messages.INVALID_USER); return ErrorPage.error(session, Messages.INVALID_USER);
} else if (e.getError() == AuthenticationFlowError.USER_DISABLED) { } else if (e.getError() == AuthenticationFlowError.USER_DISABLED) {
logger.error("failed authentication: " + e.getError().toString(), e);
event.error(Errors.USER_DISABLED); event.error(Errors.USER_DISABLED);
return ErrorPage.error(session, Messages.ACCOUNT_DISABLED); return ErrorPage.error(session, Messages.ACCOUNT_DISABLED);
} else if (e.getError() == AuthenticationFlowError.USER_TEMPORARILY_DISABLED) { } else if (e.getError() == AuthenticationFlowError.USER_TEMPORARILY_DISABLED) {
logger.error("failed authentication: " + e.getError().toString(), e);
event.error(Errors.USER_TEMPORARILY_DISABLED); event.error(Errors.USER_TEMPORARILY_DISABLED);
return ErrorPage.error(session, Messages.ACCOUNT_TEMPORARILY_DISABLED); return ErrorPage.error(session, Messages.ACCOUNT_TEMPORARILY_DISABLED);
} else if (e.getError() == AuthenticationFlowError.INVALID_CLIENT_SESSION) { } else if (e.getError() == AuthenticationFlowError.INVALID_CLIENT_SESSION) {
logger.error("failed authentication: " + e.getError().toString(), e);
event.error(Errors.INVALID_CODE); event.error(Errors.INVALID_CODE);
return ErrorPage.error(session, Messages.INVALID_CODE); return ErrorPage.error(session, Messages.INVALID_CODE);
} else if (e.getError() == AuthenticationFlowError.EXPIRED_CODE) { } else if (e.getError() == AuthenticationFlowError.EXPIRED_CODE) {
logger.error("failed authentication: " + e.getError().toString(), e);
event.error(Errors.EXPIRED_CODE); event.error(Errors.EXPIRED_CODE);
return ErrorPage.error(session, Messages.EXPIRED_CODE); return ErrorPage.error(session, Messages.EXPIRED_CODE);
} else if (e.getError() == AuthenticationFlowError.RESET_TO_BROWSER_LOGIN) { } else if (e.getError() == AuthenticationFlowError.FORK_FLOW) {
resetFlow(getClientSession()); ForkFlowException reset = (ForkFlowException)e;
ClientSessionModel clone = clone(session, clientSession);
clone.setAction(ClientSessionModel.Action.AUTHENTICATE.name());
AuthenticationProcessor processor = new AuthenticationProcessor(); AuthenticationProcessor processor = new AuthenticationProcessor();
processor.setClientSession(clientSession) processor.setClientSession(clone)
.setFlowPath(LoginActionsService.AUTHENTICATE_PATH) .setFlowPath(LoginActionsService.AUTHENTICATE_PATH)
.setFlowId(realm.getBrowserFlow().getId()) .setFlowId(realm.getBrowserFlow().getId())
.setForwardedErrorMessage(reset.getErrorMessage())
.setForwardedSuccessMessage(reset.getSuccessMessage())
.setConnection(connection) .setConnection(connection)
.setEventBuilder(event) .setEventBuilder(event)
.setProtector(protector) .setProtector(protector)
@ -491,6 +540,7 @@ public class AuthenticationProcessor {
return processor.authenticate(); return processor.authenticate();
} else { } else {
logger.error("failed authentication: " + e.getError().toString(), e);
event.error(Errors.INVALID_USER_CREDENTIALS); event.error(Errors.INVALID_USER_CREDENTIALS);
return ErrorPage.error(session, Messages.INVALID_USER); return ErrorPage.error(session, Messages.INVALID_USER);
} }
@ -586,6 +636,20 @@ public class AuthenticationProcessor {
clientSession.removeNote(CURRENT_AUTHENTICATION_EXECUTION); 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<String, String> 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) { public Response authenticationAction(String execution) {
checkClientSession(); checkClientSession();
String current = clientSession.getNote(CURRENT_AUTHENTICATION_EXECUTION); String current = clientSession.getNote(CURRENT_AUTHENTICATION_EXECUTION);
@ -710,4 +774,6 @@ public class AuthenticationProcessor {
} }
} }

View file

@ -166,9 +166,10 @@ public class DefaultAuthenticationFlow implements AuthenticationFlow {
return sendChallenge(result, execution); return sendChallenge(result, execution);
} }
throw new AuthenticationFlowException(result.getError()); 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()); 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) { } else if (status == FlowStatus.FORCE_CHALLENGE) {
processor.getClientSession().setExecutionStatus(execution.getId(), ClientSessionModel.ExecutionStatus.CHALLENGED); processor.getClientSession().setExecutionStatus(execution.getId(), ClientSessionModel.ExecutionStatus.CHALLENGED);
return sendChallenge(result, execution); return sendChallenge(result, execution);

View file

@ -45,9 +45,9 @@ public enum FlowStatus {
ATTEMPTED, 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
} }

View file

@ -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 <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @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;
}
}

View file

@ -1,7 +1,6 @@
package org.keycloak.authentication.authenticators.resetcred; package org.keycloak.authentication.authenticators.resetcred;
import org.jboss.logging.Logger; import org.jboss.logging.Logger;
import org.keycloak.ClientConnection;
import org.keycloak.Config; import org.keycloak.Config;
import org.keycloak.authentication.AuthenticationFlowContext; import org.keycloak.authentication.AuthenticationFlowContext;
import org.keycloak.authentication.AuthenticationFlowError; import org.keycloak.authentication.AuthenticationFlowError;
@ -14,24 +13,16 @@ import org.keycloak.events.Details;
import org.keycloak.events.Errors; import org.keycloak.events.Errors;
import org.keycloak.events.EventBuilder; import org.keycloak.events.EventBuilder;
import org.keycloak.events.EventType; 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.AuthenticationExecutionModel;
import org.keycloak.models.ClientSessionModel;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory; import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.models.RealmModel; import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel; 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.models.utils.HmacOTP;
import org.keycloak.protocol.oidc.TokenManager;
import org.keycloak.provider.ProviderConfigProperty; import org.keycloak.provider.ProviderConfigProperty;
import org.keycloak.services.Urls;
import org.keycloak.services.messages.Messages; 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.Response;
import javax.ws.rs.core.UriBuilder; import javax.ws.rs.core.UriBuilder;
import java.util.List; import java.util.List;
@ -54,12 +45,9 @@ public class ResetCredentialEmail implements Authenticator, AuthenticatorFactory
String username = context.getClientSession().getNote(AbstractUsernameFormAuthenticator.ATTEMPTED_USERNAME); 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. // 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) { if (user == null) {
Response challenge = context.form() context.forkWithSuccessMessage(new FormMessage(Messages.EMAIL_SENT));
.setSuccess(Messages.EMAIL_SENT)
.createForm("validate-reset-email.ftl");
context.challenge(challenge);
return; return;
} }
@ -70,10 +58,8 @@ public class ResetCredentialEmail implements Authenticator, AuthenticatorFactory
event.user(user) event.user(user)
.detail(Details.USERNAME, username) .detail(Details.USERNAME, username)
.error(Errors.INVALID_EMAIL); .error(Errors.INVALID_EMAIL);
Response challenge = context.form()
.setSuccess(Messages.EMAIL_SENT) context.forkWithSuccessMessage(new FormMessage(Messages.EMAIL_SENT));
.createForm("validate-reset-email.ftl");
context.challenge(challenge);
return; return;
} }
@ -85,15 +71,12 @@ public class ResetCredentialEmail implements Authenticator, AuthenticatorFactory
long expiration = TimeUnit.SECONDS.toMinutes(context.getRealm().getAccessCodeLifespanUserAction()); long expiration = TimeUnit.SECONDS.toMinutes(context.getRealm().getAccessCodeLifespanUserAction());
try { 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) event.clone().event(EventType.SEND_RESET_PASSWORD)
.user(user) .user(user)
.detail(Details.USERNAME, username) .detail(Details.USERNAME, username)
.detail(Details.EMAIL, user.getEmail()).detail(Details.CODE_ID, context.getClientSession().getId()).success(); .detail(Details.EMAIL, user.getEmail()).detail(Details.CODE_ID, context.getClientSession().getId()).success();
Response challenge = context.form() context.forkWithSuccessMessage(new FormMessage(Messages.EMAIL_SENT));
.setSuccess(Messages.EMAIL_SENT)
.createForm("validate-reset-email.ftl");
context.challenge(challenge);
} catch (EmailException e) { } catch (EmailException e) {
event.clone().event(EventType.SEND_RESET_PASSWORD) event.clone().event(EventType.SEND_RESET_PASSWORD)
.detail(Details.USERNAME, username) .detail(Details.USERNAME, username)
@ -110,19 +93,7 @@ public class ResetCredentialEmail implements Authenticator, AuthenticatorFactory
@Override @Override
public void action(AuthenticationFlowContext context) { public void action(AuthenticationFlowContext context) {
String secret = context.getClientSession().getNote(RESET_CREDENTIAL_SECRET); String secret = context.getClientSession().getNote(RESET_CREDENTIAL_SECRET);
String key = null; String key = context.getUriInfo().getQueryParameters().getFirst(KEY);
if (context.getHttpRequest().getHttpMethod().equalsIgnoreCase("GET")) {
key =context.getUriInfo().getQueryParameters().getFirst(KEY);
} else if (context.getHttpRequest().getHttpMethod().equalsIgnoreCase("POST")) {
MultivaluedMap<String, String> formData = context.getHttpRequest().getDecodedFormParameters();
if (formData.containsKey("cancel")) {
context.resetBrowserLogin();
return;
}
key = formData.getFirst(KEY);
}
// Can only guess once! We remove the note so another guess can't happen // Can only guess once! We remove the note so another guess can't happen
context.getClientSession().removeNote(RESET_CREDENTIAL_SECRET); context.getClientSession().removeNote(RESET_CREDENTIAL_SECRET);

View file

@ -3,6 +3,7 @@ package org.keycloak.protocol.oidc;
import org.jboss.logging.Logger; import org.jboss.logging.Logger;
import org.keycloak.ClientConnection; import org.keycloak.ClientConnection;
import org.keycloak.OAuthErrorException; import org.keycloak.OAuthErrorException;
import org.keycloak.authentication.AuthenticationProcessor;
import org.keycloak.events.Details; import org.keycloak.events.Details;
import org.keycloak.events.EventBuilder; import org.keycloak.events.EventBuilder;
import org.keycloak.jose.jws.JWSBuilder; import org.keycloak.jose.jws.JWSBuilder;
@ -225,6 +226,7 @@ public class TokenManager {
} }
public static void dettachClientSession(UserSessionProvider sessions, RealmModel realm, ClientSessionModel clientSession) { public static void dettachClientSession(UserSessionProvider sessions, RealmModel realm, ClientSessionModel clientSession) {
UserSessionModel userSession = clientSession.getUserSession(); UserSessionModel userSession = clientSession.getUserSession();
if (userSession == null) { if (userSession == null) {

View file

@ -46,6 +46,7 @@ import org.keycloak.models.RoleModel;
import org.keycloak.models.UserModel; import org.keycloak.models.UserModel;
import org.keycloak.models.UserSessionModel; import org.keycloak.models.UserSessionModel;
import org.keycloak.models.utils.DefaultAuthenticationFlows; import org.keycloak.models.utils.DefaultAuthenticationFlows;
import org.keycloak.models.utils.FormMessage;
import org.keycloak.protocol.oidc.TokenManager; import org.keycloak.protocol.oidc.TokenManager;
import org.keycloak.provider.ProviderFactory; import org.keycloak.provider.ProviderFactory;
import org.keycloak.representations.AccessToken; import org.keycloak.representations.AccessToken;
@ -482,7 +483,7 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal
.setSession(session) .setSession(session)
.setUriInfo(uriInfo) .setUriInfo(uriInfo)
.setRequest(request); .setRequest(request);
if (errorMessage != null) processor.setForwardedErrorMessage(errorMessage); if (errorMessage != null) processor.setForwardedErrorMessage(new FormMessage(null, errorMessage));
try { try {
return processor.authenticate(); return processor.authenticate();

View file

@ -301,11 +301,11 @@ public class LoginActionsService {
.setConnection(clientConnection) .setConnection(clientConnection)
.setEventBuilder(event) .setEventBuilder(event)
.setProtector(authManager.getProtector()) .setProtector(authManager.getProtector())
.setForwardedErrorMessage(errorMessage)
.setRealm(realm) .setRealm(realm)
.setSession(session) .setSession(session)
.setUriInfo(uriInfo) .setUriInfo(uriInfo)
.setRequest(request); .setRequest(request);
if (errorMessage != null) processor.setForwardedErrorMessage(new FormMessage(null, errorMessage));
try { try {
if (execution != null) { if (execution != null) {

View file

@ -119,9 +119,6 @@ public class ResetPasswordTest {
@WebResource @WebResource
protected LoginPasswordResetPage resetPasswordPage; protected LoginPasswordResetPage resetPasswordPage;
@WebResource
protected ValidatePassworrdEmailResetPage validateResetPage;
@WebResource @WebResource
protected LoginPasswordUpdatePage updatePasswordPage; protected LoginPasswordUpdatePage updatePasswordPage;
@ -148,48 +145,6 @@ public class ResetPasswordTest {
resetPassword("login-test"); 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 @Test
public void resetPasswordCancelChangeUser() throws IOException, MessagingException { public void resetPasswordCancelChangeUser() throws IOException, MessagingException {
loginPage.open(); loginPage.open();
@ -199,15 +154,13 @@ public class ResetPasswordTest {
resetPasswordPage.changePassword("test-user@localhost"); 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") events.expectRequiredAction(EventType.SEND_RESET_PASSWORD).detail(Details.USERNAME, "test-user@localhost")
.session((String) null) .session((String) null)
.detail(Details.EMAIL, "test-user@localhost").assertEvent(); .detail(Details.EMAIL, "test-user@localhost").assertEvent();
validateResetPage.cancel();
assertTrue(loginPage.isCurrent());
loginPage.login("login@test.com", "password"); loginPage.login("login@test.com", "password");
@ -235,7 +188,8 @@ public class ResetPasswordTest {
resetPasswordPage.changePassword(username); 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) events.expectRequiredAction(EventType.SEND_RESET_PASSWORD)
.user(userId) .user(userId)
@ -244,8 +198,6 @@ public class ResetPasswordTest {
.session((String)null) .session((String)null)
.assertEvent(); .assertEvent();
assertEquals("You should receive an email shortly with further instructions.", resetPasswordPage.getSuccessMessage());
assertEquals(1, greenMail.getReceivedMessages().length); assertEquals(1, greenMail.getReceivedMessages().length);
MimeMessage message = greenMail.getReceivedMessages()[0]; MimeMessage message = greenMail.getReceivedMessages()[0];
@ -285,13 +237,12 @@ public class ResetPasswordTest {
resetPasswordPage.changePassword(username); 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) events.expectRequiredAction(EventType.SEND_RESET_PASSWORD).user(userId).session((String)null)
.detail(Details.USERNAME, username).detail(Details.EMAIL, "login@test.com").assertEvent(); .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]; MimeMessage message = greenMail.getReceivedMessages()[greenMail.getReceivedMessages().length - 1];
String changePasswordUrl = getPasswordResetEmailLink(message); String changePasswordUrl = getPasswordResetEmailLink(message);
@ -322,13 +273,12 @@ public class ResetPasswordTest {
resetPasswordPage.changePassword(username); 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) events.expectRequiredAction(EventType.SEND_RESET_PASSWORD).user(userId).session((String)null)
.detail(Details.USERNAME, username).detail(Details.EMAIL, "login@test.com").assertEvent(); .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]; MimeMessage message = greenMail.getReceivedMessages()[greenMail.getReceivedMessages().length - 1];
String changePasswordUrl = getPasswordResetEmailLink(message); String changePasswordUrl = getPasswordResetEmailLink(message);
@ -352,9 +302,8 @@ public class ResetPasswordTest {
resetPasswordPage.changePassword("invalid"); resetPasswordPage.changePassword("invalid");
validateResetPage.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.", resetPasswordPage.getSuccessMessage());
assertEquals(0, greenMail.getReceivedMessages().length); assertEquals(0, greenMail.getReceivedMessages().length);
@ -390,14 +339,13 @@ public class ResetPasswordTest {
resetPasswordPage.changePassword("login-test"); 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) events.expectRequiredAction(EventType.SEND_RESET_PASSWORD)
.session((String)null) .session((String)null)
.user(userId).detail(Details.USERNAME, "login-test").detail(Details.EMAIL, "login@test.com").assertEvent(); .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); assertEquals(1, greenMail.getReceivedMessages().length);
MimeMessage message = greenMail.getReceivedMessages()[0]; MimeMessage message = greenMail.getReceivedMessages()[0];
@ -435,9 +383,8 @@ public class ResetPasswordTest {
resetPasswordPage.changePassword("login-test"); resetPasswordPage.changePassword("login-test");
validateResetPage.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.", resetPasswordPage.getSuccessMessage());
assertEquals(0, greenMail.getReceivedMessages().length); assertEquals(0, greenMail.getReceivedMessages().length);
@ -473,9 +420,8 @@ public class ResetPasswordTest {
resetPasswordPage.changePassword("login-test"); resetPasswordPage.changePassword("login-test");
validateResetPage.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.", resetPasswordPage.getSuccessMessage());
assertEquals(0, greenMail.getReceivedMessages().length); assertEquals(0, greenMail.getReceivedMessages().length);
@ -544,9 +490,8 @@ public class ResetPasswordTest {
resetPasswordPage.changePassword("login-test"); resetPasswordPage.changePassword("login-test");
validateResetPage.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.", resetPasswordPage.getSuccessMessage());
assertEquals(1, greenMail.getReceivedMessages().length); 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 { private String getPasswordResetEmailLink(MimeMessage message) throws IOException, MessagingException {
Multipart multipart = (Multipart) message.getContent(); Multipart multipart = (Multipart) message.getContent();

View file

@ -68,6 +68,10 @@ public class LoginPage extends AbstractPage {
@FindBy(className = "feedback-warning") @FindBy(className = "feedback-warning")
private WebElement loginWarningMessage; private WebElement loginWarningMessage;
@FindBy(className = "feedback-success")
private WebElement emailSuccessMessage;
@FindBy(id = "kc-current-locale-link") @FindBy(id = "kc-current-locale-link")
private WebElement languageText; private WebElement languageText;
@ -116,6 +120,11 @@ public class LoginPage extends AbstractPage {
return loginErrorMessage != null ? loginErrorMessage.getText() : null; return loginErrorMessage != null ? loginErrorMessage.getText() : null;
} }
public String getSuccessMessage() {
return emailSuccessMessage != null ? emailSuccessMessage.getText() : null;
}
public boolean isCurrent() { public boolean isCurrent() {
return driver.getTitle().equals("Log in to test") || driver.getTitle().equals("Anmeldung bei test"); return driver.getTitle().equals("Log in to test") || driver.getTitle().equals("Anmeldung bei test");
} }