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
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:

View file

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

View file

@ -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=<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>
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.</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
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>

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.
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
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=<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>
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.</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
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>

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.
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
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=<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>
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.</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
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>

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
* @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

View file

@ -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<String, Object> attributes = new HashMap<String, Object>();
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);

View file

@ -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);

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

View file

@ -35,6 +35,10 @@ public class FormMessage {
this.parameters = parameters;
}
public FormMessage(String message, Object...parameters) {
this(null, message, 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.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

View file

@ -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);
}

View file

@ -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,

View file

@ -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 <a href="mailto:bill@burkecentral.com">Bill Burke</a>
@ -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<AuthenticationExecutionModel> currentExecutions;
FormMessage errorMessage;
FormMessage successMessage;
private Result(AuthenticationExecutionModel execution, Authenticator authenticator, List<AuthenticationExecutionModel> 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<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) {
checkClientSession();
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);
}
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);

View file

@ -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
}

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;
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<String, String> 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);

View file

@ -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) {

View file

@ -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();

View file

@ -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) {

View file

@ -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();

View file

@ -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");
}