Merge pull request #1552 from patriot1burke/master
change reset password
This commit is contained in:
commit
b9d0219f04
24 changed files with 231 additions and 222 deletions
|
@ -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:
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<html>
|
<html>
|
||||||
<body>
|
<body>
|
||||||
${msg("passwordResetBodyHtml",link, linkExpiration, realmName, code)}
|
${msg("passwordResetBodyHtml",link, linkExpiration, realmName)}
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
${msg("passwordResetBody",link, linkExpiration, realmName, code)}
|
${msg("passwordResetBody",link, linkExpiration, realmName)}
|
|
@ -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
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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);
|
||||||
|
|
4
model/api/src/main/java/org/keycloak/models/utils/FormMessage.java
Normal file → Executable file
4
model/api/src/main/java/org/keycloak/models/utils/FormMessage.java
Normal file → Executable file
|
@ -34,6 +34,10 @@ public class FormMessage {
|
||||||
this(field, message);
|
this(field, message);
|
||||||
this.parameters = parameters;
|
this.parameters = parameters;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public FormMessage(String message, Object...parameters) {
|
||||||
|
this(null, message, parameters);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create message without parameters.
|
* Create message without parameters.
|
||||||
|
|
13
services/src/main/java/org/keycloak/authentication/AbstractAuthenticationFlowContext.java
Normal file → Executable file
13
services/src/main/java/org/keycloak/authentication/AbstractAuthenticationFlowContext.java
Normal file → Executable 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
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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 {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
29
services/src/main/java/org/keycloak/authentication/ForkFlowException.java
Executable file
29
services/src/main/java/org/keycloak/authentication/ForkFlowException.java
Executable 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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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();
|
||||||
|
|
||||||
|
|
|
@ -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");
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue