diff --git a/forms/common-themes/src/main/resources/theme/base/login/login.ftl b/forms/common-themes/src/main/resources/theme/base/login/login.ftl
index e3a3456693..e4ab6668db 100755
--- a/forms/common-themes/src/main/resources/theme/base/login/login.ftl
+++ b/forms/common-themes/src/main/resources/theme/base/login/login.ftl
@@ -42,7 +42,7 @@
#if>
diff --git a/forms/common-themes/src/main/resources/theme/base/login/messages/messages_de.properties b/forms/common-themes/src/main/resources/theme/base/login/messages/messages_de.properties
index d02ca8fe6e..3050953770 100755
--- a/forms/common-themes/src/main/resources/theme/base/login/messages/messages_de.properties
+++ b/forms/common-themes/src/main/resources/theme/base/login/messages/messages_de.properties
@@ -80,7 +80,9 @@ emailVerifyInstruction3=um ein neues E-Mail zu verschicken.
backToLogin=« Zur\u00FCck zur Anmeldung
backToApplication=« Zur\u00FCck zur Applikation
+temporaryEmailCode=Temporary Email Code
emailInstruction=Geben Sie ihren Benutzernamen oder E-Mail Adresse ein und klicken Sie auf Absenden. Danach werden wir ihnen ein E-Mail mit weiteren Instruktionen zusenden.
+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=Bitte kopieren sie den folgenden Code und f\u00FCgen ihn in die Applikation ein\:
diff --git a/forms/common-themes/src/main/resources/theme/base/login/messages/messages_en.properties b/forms/common-themes/src/main/resources/theme/base/login/messages/messages_en.properties
index bc10220b0d..1f223aa235 100755
--- a/forms/common-themes/src/main/resources/theme/base/login/messages/messages_en.properties
+++ b/forms/common-themes/src/main/resources/theme/base/login/messages/messages_en.properties
@@ -81,7 +81,9 @@ emailVerifyInstruction3=to re-send the email.
backToLogin=« Back to Login
+temporaryEmailCode=Temporary Email Code
emailInstruction=Enter your username or email address and we will send you instructions on how to create a new password.
+validateResetEmailInstruction=You have just been sent an email. Clicking on the URL in the email will allow you to reset credentials and log in. Alternatively, you can manually enter in the temporary code provided in the email in the textbox to the left and hit submit.
copyCodeInstruction=Please copy this code and paste it into your application:
diff --git a/forms/common-themes/src/main/resources/theme/base/login/messages/messages_it.properties b/forms/common-themes/src/main/resources/theme/base/login/messages/messages_it.properties
index 0860397bec..e57afcdd16 100755
--- a/forms/common-themes/src/main/resources/theme/base/login/messages/messages_it.properties
+++ b/forms/common-themes/src/main/resources/theme/base/login/messages/messages_it.properties
@@ -77,7 +77,9 @@ emailVerifyInstruction3=per reinviare la mail.
backToLogin=« Torna al Login
+temporaryEmailCode=Temporary Email Code
emailInstruction=Scrivi il tuo username o indirizzo email e noi ti invieremo le istruzioni per creare una nuova 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=Copiaquesto codice e incollalo nella tua applicazione:
diff --git a/forms/common-themes/src/main/resources/theme/base/login/messages/messages_pt_BR.properties b/forms/common-themes/src/main/resources/theme/base/login/messages/messages_pt_BR.properties
index 2b730c9246..46858cb551 100755
--- a/forms/common-themes/src/main/resources/theme/base/login/messages/messages_pt_BR.properties
+++ b/forms/common-themes/src/main/resources/theme/base/login/messages/messages_pt_BR.properties
@@ -77,7 +77,9 @@ emailVerifyInstruction3=para reenviar o e-mail.
backToLogin=« Voltar
+temporaryEmailCode=Temporary Email Code
emailInstruction=Digite seu nome de usu\u00E1rio ou endere\u00E7o de email e n\u00F3s lhe enviaremos instru\u00E7\u00F5es sobre como criar uma nova senha.
+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=Por favor, copie o c\u00F3digo e cole-o em sua aplica\u00E7\u00E3o:
diff --git a/forms/common-themes/src/main/resources/theme/base/login/validate-reset-email.ftl b/forms/common-themes/src/main/resources/theme/base/login/validate-reset-email.ftl
new file mode 100755
index 0000000000..fd5fa65c6d
--- /dev/null
+++ b/forms/common-themes/src/main/resources/theme/base/login/validate-reset-email.ftl
@@ -0,0 +1,33 @@
+<#import "template.ftl" as layout>
+<@layout.registrationLayout displayInfo=true; section>
+ <#if section = "title">
+ ${msg("emailForgotTitle")}
+ <#elseif section = "header">
+ ${msg("emailForgotTitle")}
+ <#elseif section = "form">
+
+ <#elseif section = "info" >
+ ${msg("validateResetEmailInstruction")}
+ #if>
+@layout.registrationLayout>
\ No newline at end of file
diff --git a/forms/common-themes/src/main/resources/theme/keycloak/email/html/changePassword.ftl b/forms/common-themes/src/main/resources/theme/keycloak/email/html/changePassword.ftl
new file mode 100755
index 0000000000..62037e01f8
--- /dev/null
+++ b/forms/common-themes/src/main/resources/theme/keycloak/email/html/changePassword.ftl
@@ -0,0 +1,5 @@
+
+
+${msg("changePasswordBodyHtml",link, linkExpiration, realmName)}
+
+
diff --git a/forms/common-themes/src/main/resources/theme/keycloak/email/html/password-reset.ftl b/forms/common-themes/src/main/resources/theme/keycloak/email/html/password-reset.ftl
old mode 100644
new mode 100755
index 846b45dafd..55c676f469
--- a/forms/common-themes/src/main/resources/theme/keycloak/email/html/password-reset.ftl
+++ b/forms/common-themes/src/main/resources/theme/keycloak/email/html/password-reset.ftl
@@ -1,5 +1,5 @@
-${msg("passwordResetBodyHtml",link, linkExpiration, realmName)}
+${msg("passwordResetBodyHtml",link, linkExpiration, realmName, code)}
\ No newline at end of file
diff --git a/forms/common-themes/src/main/resources/theme/keycloak/email/messages/messages_de.properties b/forms/common-themes/src/main/resources/theme/keycloak/email/messages/messages_de.properties
old mode 100644
new mode 100755
index 91337c4ee8..fdaa81c9b8
--- a/forms/common-themes/src/main/resources/theme/keycloak/email/messages/messages_de.properties
+++ b/forms/common-themes/src/main/resources/theme/keycloak/email/messages/messages_de.properties
@@ -1,7 +1,10 @@
emailVerificationSubject=E-Mail verifizieren
passwordResetSubject=Passwort zur\u00FCckzusetzen
-passwordResetBody=Jemand hat angefordert Ihr {2} Passwort zur\u00FCckzusetzen. Falls das Sie waren, dann klicken Sie auf den folgenden Link um das Passwort zur\u00FCckzusetzen.\n\n{0}\n\nDieser Link wird in {1} Minuten ablaufen.\n\nFalls Sie das Passwort nicht zur\u00FCcksetzen m\u00F6chten, dann k\u00F6nnen Sie diese E-Mail ignorieren.
-passwordResetBodyHtml=
Jemand hat angefordert Ihr {2} Passwort zur\u00FCckzusetzen. Falls das Sie waren, dann klicken Sie auf den folgenden Link um das Passwort zur\u00FCckzusetzen.
{0}
Dieser Link wird in {1} Minuten ablaufen.
Falls Sie das Passwort nicht zur\u00FCcksetzen m\u00F6chten, dann k\u00F6nnen Sie diese E-Mail ignorieren.
+passwordResetBody=Someone just requested to change your {2} account''s password. If this was you, click on the link below to set a new password or cut and paste the temporary code to the forgot password form.\n\n{0}\n\nTemporary Code: {3}\n\nThis link and code will expire within {1} minutes.\n\nIf you don''t want to reset your password, just ignore this message and nothing will be changed.
+passwordResetBodyHtml=
Someone just requested to change your {2} account''s password. If this was you, click on the link below to set a new password or cut and paste the temporary code to the forgot password form
{0}
Temporary code: {3}
This link will expire within {1} minutes.
If you don''t want to reset your password, just ignore this message and nothing will be changed.
+changePasswordSubject=Change password
+changePasswordBody=Your adminstrator has just requested that you change your {2} account''s password. Click on the link below to set a new password\n\n{0}\n\nThis link will expire within {1} minutes.\n\nIf you don''t want to reset your password, just ignore this message and nothing will be changed.
+changePasswordBodyHtml=
Your adminstrator has just requested that you change your {2} account''s password. Click on the link below to set a new password
{0}
This link will expire within {1} minutes.
If you don''t want to reset your password, just ignore this message and nothing will be changed.
emailVerificationBody=Jemand hat ein {2} Konto mit dieser E-Mail Adresse erstellt. Fall das Sie waren, dann klicken Sie auf den Link um die E-Mail Adresse zu verifizieren.\n\n{0}\n\nDieser Link wird in {1} Minuten ablaufen.\n\nFalls Sie dieses Konto nicht erstellt haben, dann k\u00F6nnen sie diese Nachricht ignorieren.
emailVerificationBodyHtml=
Jemand hat ein {2} Konto mit dieser E-Mail Adresse erstellt. Fall das Sie waren, dann klicken Sie auf den Link um die E-Mail Adresse zu verifizieren.
{0}
Dieser Link wird in {1} Minuten ablaufen.
Falls Sie dieses Konto nicht erstellt haben, dann k\u00F6nnen sie diese Nachricht ignorieren.
eventLoginErrorSubject=Fehlgeschlagene Anmeldung
diff --git a/forms/common-themes/src/main/resources/theme/keycloak/email/messages/messages_en.properties b/forms/common-themes/src/main/resources/theme/keycloak/email/messages/messages_en.properties
index 7a3ac65499..eb45780410 100755
--- a/forms/common-themes/src/main/resources/theme/keycloak/email/messages/messages_en.properties
+++ b/forms/common-themes/src/main/resources/theme/keycloak/email/messages/messages_en.properties
@@ -2,8 +2,11 @@ emailVerificationSubject=Verify email
emailVerificationBody=Someone has created a {2} account with this email address. If this was you, click the link below to verify your email address\n\n{0}\n\nThis link will expire within {1} minutes.\n\nIf you didn''t create this account, just ignore this message.
emailVerificationBodyHtml=
Someone has created a {2} account with this email address. If this was you, click the link below to verify your email address
{0}
This link will expire within {1} minutes.
If you didn''t create this account, just ignore this message.
passwordResetSubject=Reset password
-passwordResetBody=Someone just requested to change your {2} account''s password. If this was you, click on the link below to set a new password\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.
-passwordResetBodyHtml=
Someone just requested to change your {2} account''s password. If this was you, click on the link below to set a new password
{0}
This link will expire within {1} minutes.
If you don''t want to reset your password, just ignore this message and nothing will be changed.
+passwordResetBody=Someone just requested to change your {2} account''s password. If this was you, click on the link below to set a new password or cut and paste the temporary code to the forgot password form.\n\n{0}\n\nTemporary Code: {3}\n\nThis link and code will expire within {1} minutes.\n\nIf you don''t want to reset your password, just ignore this message and nothing will be changed.
+passwordResetBodyHtml=
Someone just requested to change your {2} account''s password. If this was you, click on the link below to set a new password or cut and paste the temporary code to the forgot password form
{0}
Temporary code: {3}
This link will expire within {1} minutes.
If you don''t want to reset your password, just ignore this message and nothing will be changed.
+changePasswordSubject=Change password
+changePasswordBody=Your adminstrator has just requested that you change your {2} account''s password. Click on the link below to set a new password\n\n{0}\n\nThis link will expire within {1} minutes.\n\nIf you don''t want to reset your password, just ignore this message and nothing will be changed.
+changePasswordBodyHtml=
Your adminstrator has just requested that you change your {2} account''s password. Click on the link below to set a new password
{0}
This link will expire within {1} minutes.
If you don''t want to reset your password, just ignore this message and nothing will be changed.
eventLoginErrorSubject=Login error
eventLoginErrorBody=A failed login attempt was detected to your account on {0} from {1}. If this was not you, please contact an admin.
eventLoginErrorBodyHtml=
A failed login attempt was detected to your account on {0} from {1}. If this was not you, please contact an admin.
diff --git a/forms/common-themes/src/main/resources/theme/keycloak/email/messages/messages_pt_BR.properties b/forms/common-themes/src/main/resources/theme/keycloak/email/messages/messages_pt_BR.properties
old mode 100644
new mode 100755
index fff7e10475..e913fa0470
--- a/forms/common-themes/src/main/resources/theme/keycloak/email/messages/messages_pt_BR.properties
+++ b/forms/common-themes/src/main/resources/theme/keycloak/email/messages/messages_pt_BR.properties
@@ -2,8 +2,11 @@ emailVerificationSubject=Verifica\u00E7\u00E3o de e-mail
emailVerificationBody=Algu\u00E9m criou uma conta {2} com este endere\u00E7o de e-mail. Se foi voc\u00EA, clique no link abaixo para verificar o seu endere\u00E7o de email\n\n{0}\n\nEste link ir\u00E1 expirar dentro de {1} minutos.\n\nSe n\u00E3o foi voc\u00EA que criou esta conta, basta ignorar esta mensagem.
emailVerificationBodyHtml=
Algu\u00E9m criou uma conta {2} com este endere\u00E7o de e-mail. Se foi voc\u00EA, clique no link abaixo para verificar o seu endere\u00E7o de email
{0}
Este link ir\u00E1 expirar dentro de {1} minutos.
Se n\u00E3o foi voc\u00EA que criou esta conta, basta ignorar esta mensagem.
passwordResetSubject=Redefini\u00E7\u00E3o de senha
-passwordResetBody=Algu\u00E9m pediu para mudar a senha de sua conta {2}. Se foi voc\u00EA, clique no link abaixo para definir uma nova senha\n\n{0}\n\nEste link ir\u00E1 expirar dentro de {1} minutos.\n\nSe voc\u00EA n\u00E3o deseja redefinir sua senha, basta ignorar esta mensagem e nada ser\u00E1 mudado.
-passwordResetBodyHtml=
Algu\u00E9m pediu para mudar a senha de sua conta {2}. Se foi voc\u00EA, clique no link abaixo para definir uma nova senha
{0}
Este link ir\u00E1 expirar dentro de {1} minutos.
Se voc\u00EA n\u00E3o deseja redefinir sua senha, basta ignorar esta mensagem e nada ser\u00E1 mudado.
+passwordResetBody=Someone just requested to change your {2} account''s password. If this was you, click on the link below to set a new password or cut and paste the temporary code to the forgot password form.\n\n{0}\n\nTemporary Code: {3}\n\nThis link and code will expire within {1} minutes.\n\nIf you don''t want to reset your password, just ignore this message and nothing will be changed.
+passwordResetBodyHtml=
Someone just requested to change your {2} account''s password. If this was you, click on the link below to set a new password or cut and paste the temporary code to the forgot password form
{0}
Temporary code: {3}
This link will expire within {1} minutes.
If you don''t want to reset your password, just ignore this message and nothing will be changed.
+changePasswordSubject=Change password
+changePasswordBody=Your adminstrator has just requested that you change your {2} account''s password. Click on the link below to set a new password\n\n{0}\n\nThis link will expire within {1} minutes.\n\nIf you don''t want to reset your password, just ignore this message and nothing will be changed.
+changePasswordBodyHtml=
Your adminstrator has just requested that you change your {2} account''s password. Click on the link below to set a new password
{0}
This link will expire within {1} minutes.
If you don''t want to reset your password, just ignore this message and nothing will be changed.
eventLoginErrorSubject=Erro de login
eventLoginErrorBody=Uma tentativa de login mal sucedida para a sua conta foi detectada em {0} de {1}. Se n\u00E3o foi voc\u00EA, por favor, entre em contato com um administrador.
eventLoginErrorBodyHtml=
Uma tentativa de login mal sucedida para a sua conta foi detectada em {0} de {1}. Se n\u00E3o foi voc\u00EA, por favor, entre em contato com um administrador.
diff --git a/forms/common-themes/src/main/resources/theme/keycloak/email/text/changePassword.ftl b/forms/common-themes/src/main/resources/theme/keycloak/email/text/changePassword.ftl
new file mode 100755
index 0000000000..6cad660005
--- /dev/null
+++ b/forms/common-themes/src/main/resources/theme/keycloak/email/text/changePassword.ftl
@@ -0,0 +1 @@
+${msg("changePasswordBody",link, linkExpiration, realmName)}
\ No newline at end of file
diff --git a/forms/common-themes/src/main/resources/theme/keycloak/email/text/password-reset.ftl b/forms/common-themes/src/main/resources/theme/keycloak/email/text/password-reset.ftl
old mode 100644
new mode 100755
index aba4fd1941..aa15723d97
--- a/forms/common-themes/src/main/resources/theme/keycloak/email/text/password-reset.ftl
+++ b/forms/common-themes/src/main/resources/theme/keycloak/email/text/password-reset.ftl
@@ -1 +1 @@
-${msg("passwordResetBody",link, linkExpiration, realmName)}
\ No newline at end of file
+${msg("passwordResetBody",link, linkExpiration, realmName, code)}
\ No newline at end of file
diff --git a/forms/email-api/src/main/java/org/keycloak/email/EmailProvider.java b/forms/email-api/src/main/java/org/keycloak/email/EmailProvider.java
old mode 100644
new mode 100755
index 1d7aff3275..bd62a85daa
--- a/forms/email-api/src/main/java/org/keycloak/email/EmailProvider.java
+++ b/forms/email-api/src/main/java/org/keycloak/email/EmailProvider.java
@@ -16,7 +16,24 @@ public interface EmailProvider extends Provider {
public void sendEvent(Event event) throws EmailException;
- public void sendPasswordReset(String link, long expirationInMinutes) throws EmailException;
+ /**
+ * Reset password sent from forgot password link on login
+ *
+ * @param code
+ * @param link
+ * @param expirationInMinutes
+ * @throws EmailException
+ */
+ public void sendPasswordReset(String code, String link, long expirationInMinutes) throws EmailException;
+
+ /**
+ * Change password email requested by admin
+ *
+ * @param link
+ * @param expirationInMinutes
+ * @throws EmailException
+ */
+ public void sendChangePassword(String link, long expirationInMinutes) throws EmailException;
public void sendVerifyEmail(String link, long expirationInMinutes) throws EmailException;
diff --git a/forms/email-freemarker/src/main/java/org/keycloak/email/freemarker/FreeMarkerEmailProvider.java b/forms/email-freemarker/src/main/java/org/keycloak/email/freemarker/FreeMarkerEmailProvider.java
index 79080de2c2..849fcae64e 100755
--- a/forms/email-freemarker/src/main/java/org/keycloak/email/freemarker/FreeMarkerEmailProvider.java
+++ b/forms/email-freemarker/src/main/java/org/keycloak/email/freemarker/FreeMarkerEmailProvider.java
@@ -70,7 +70,20 @@ public class FreeMarkerEmailProvider implements EmailProvider {
}
@Override
- public void sendPasswordReset(String link, long expirationInMinutes) throws EmailException {
+ public void sendPasswordReset(String code, String link, long expirationInMinutes) throws EmailException {
+ Map
attributes = new HashMap();
+ attributes.put("link", link);
+ attributes.put("linkExpiration", expirationInMinutes);
+ attributes.put("code", code);
+
+ String realmName = realm.getName().substring(0, 1).toUpperCase() + realm.getName().substring(1);
+ attributes.put("realmName", realmName);
+
+ send("passwordResetSubject", "password-reset.ftl", attributes);
+ }
+
+ @Override
+ public void sendChangePassword(String link, long expirationInMinutes) throws EmailException {
Map attributes = new HashMap();
attributes.put("link", link);
attributes.put("linkExpiration", expirationInMinutes);
@@ -78,7 +91,8 @@ public class FreeMarkerEmailProvider implements EmailProvider {
String realmName = realm.getName().substring(0, 1).toUpperCase() + realm.getName().substring(1);
attributes.put("realmName", realmName);
- send("passwordResetSubject", "password-reset.ftl", attributes);
+ send("changePasswordSubject", "changePassword.ftl", attributes);
+
}
@Override
diff --git a/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/model/UrlBean.java b/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/model/UrlBean.java
index 4fdc9fafb3..25ebc83d74 100755
--- a/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/model/UrlBean.java
+++ b/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/model/UrlBean.java
@@ -78,8 +78,8 @@ public class UrlBean {
return Urls.loginActionUpdateProfile(baseURI, realm).toString();
}
- public String getLoginPasswordResetUrl() {
- return Urls.loginPasswordReset(baseURI, realm).toString();
+ public String getLoginResetCredentialsUrl() {
+ return Urls.loginResetCredentials(baseURI, realm).toString();
}
public String getLoginUsernameReminderUrl() {
diff --git a/model/api/src/main/java/org/keycloak/migration/MigrationModelManager.java b/model/api/src/main/java/org/keycloak/migration/MigrationModelManager.java
index 09239afb7d..61b6244ff9 100755
--- a/model/api/src/main/java/org/keycloak/migration/MigrationModelManager.java
+++ b/model/api/src/main/java/org/keycloak/migration/MigrationModelManager.java
@@ -45,7 +45,7 @@ public class MigrationModelManager {
if (stored != null) {
logger.debug("Migrating older model to 1.5.0 updates");
}
- new MigrateTo1_4_0().migrate(session);
+ new MigrateTo1_5_0().migrate(session);
}
model.setStoredVersion(MigrationModel.LATEST_VERSION);
diff --git a/model/api/src/main/java/org/keycloak/migration/migrators/MigrateTo1_5_0.java b/model/api/src/main/java/org/keycloak/migration/migrators/MigrateTo1_5_0.java
index acec3b0660..a8c4dbdafa 100755
--- a/model/api/src/main/java/org/keycloak/migration/migrators/MigrateTo1_5_0.java
+++ b/model/api/src/main/java/org/keycloak/migration/migrators/MigrateTo1_5_0.java
@@ -24,10 +24,12 @@ public class MigrateTo1_5_0 {
public void migrate(KeycloakSession session) {
List realms = session.realms().getRealms();
for (RealmModel realm : realms) {
+ DefaultAuthenticationFlows.migrateFlows(realm); // add reset credentials flo
realm.setOTPPolicy(OTPPolicy.DEFAULT_POLICY);
realm.setBrowserFlow(realm.getFlowByAlias(DefaultAuthenticationFlows.BROWSER_FLOW));
realm.setRegistrationFlow(realm.getFlowByAlias(DefaultAuthenticationFlows.REGISTRATION_FLOW));
realm.setDirectGrantFlow(realm.getFlowByAlias(DefaultAuthenticationFlows.DIRECT_GRANT_FLOW));
+ realm.setResetCredentialsFlow(realm.getFlowByAlias(DefaultAuthenticationFlows.RESET_CREDENTIALS_FLOW));
}
}
diff --git a/model/api/src/main/java/org/keycloak/models/RealmModel.java b/model/api/src/main/java/org/keycloak/models/RealmModel.java
index 9f393778b9..58c198a147 100755
--- a/model/api/src/main/java/org/keycloak/models/RealmModel.java
+++ b/model/api/src/main/java/org/keycloak/models/RealmModel.java
@@ -191,6 +191,9 @@ public interface RealmModel extends RoleContainerModel {
AuthenticationFlowModel getDirectGrantFlow();
void setDirectGrantFlow(AuthenticationFlowModel flow);
+ AuthenticationFlowModel getResetCredentialsFlow();
+ void setResetCredentialsFlow(AuthenticationFlowModel flow);
+
List getAuthenticationFlows();
AuthenticationFlowModel getFlowByAlias(String alias);
AuthenticationFlowModel addAuthenticationFlow(AuthenticationFlowModel model);
diff --git a/model/api/src/main/java/org/keycloak/models/entities/RealmEntity.java b/model/api/src/main/java/org/keycloak/models/entities/RealmEntity.java
index 1d4859d4f5..6c64168608 100755
--- a/model/api/src/main/java/org/keycloak/models/entities/RealmEntity.java
+++ b/model/api/src/main/java/org/keycloak/models/entities/RealmEntity.java
@@ -89,6 +89,7 @@ public class RealmEntity extends AbstractIdentifiableEntity {
private String browserFlow;
private String registrationFlow;
private String directGrantFlow;
+ private String resetCredentialsFlow;
public String getName() {
@@ -593,6 +594,14 @@ public class RealmEntity extends AbstractIdentifiableEntity {
public void setDirectGrantFlow(String directGrantFlow) {
this.directGrantFlow = directGrantFlow;
}
+
+ public String getResetCredentialsFlow() {
+ return resetCredentialsFlow;
+ }
+
+ public void setResetCredentialsFlow(String resetCredentialsFlow) {
+ this.resetCredentialsFlow = resetCredentialsFlow;
+ }
}
diff --git a/model/api/src/main/java/org/keycloak/models/utils/DefaultAuthenticationFlows.java b/model/api/src/main/java/org/keycloak/models/utils/DefaultAuthenticationFlows.java
index 2c0b8e6048..608f57bde5 100755
--- a/model/api/src/main/java/org/keycloak/models/utils/DefaultAuthenticationFlows.java
+++ b/model/api/src/main/java/org/keycloak/models/utils/DefaultAuthenticationFlows.java
@@ -15,22 +15,22 @@ public class DefaultAuthenticationFlows {
public static final String REGISTRATION_FORM_FLOW = "registration form";
public static final String BROWSER_FLOW = "browser";
public static final String DIRECT_GRANT_FLOW = "direct grant";
+ public static final String RESET_CREDENTIALS_FLOW = "reset credentials";
public static final String LOGIN_FORMS_FLOW = "forms";
public static void addFlows(RealmModel realm) {
if (realm.getFlowByAlias(BROWSER_FLOW) == null) browserFlow(realm);
if (realm.getFlowByAlias(DIRECT_GRANT_FLOW) == null) directGrantFlow(realm, false);
if (realm.getFlowByAlias(REGISTRATION_FLOW) == null) registrationFlow(realm);
+ if (realm.getFlowByAlias(RESET_CREDENTIALS_FLOW) == null) resetCredentialsFlow(realm);
}
public static void migrateFlows(RealmModel realm) {
- browserFlow(realm, true);
- directGrantFlow(realm, true);
+ if (realm.getFlowByAlias(BROWSER_FLOW) == null) browserFlow(realm, true);
+ if (realm.getFlowByAlias(DIRECT_GRANT_FLOW) == null) directGrantFlow(realm, true);
if (realm.getFlowByAlias(REGISTRATION_FLOW) == null) registrationFlow(realm);
+ if (realm.getFlowByAlias(RESET_CREDENTIALS_FLOW) == null) resetCredentialsFlow(realm);
}
-
-
-
public static void registrationFlow(RealmModel realm) {
AuthenticationFlowModel registrationFlow = new AuthenticationFlowModel();
registrationFlow.setAlias(REGISTRATION_FLOW);
@@ -118,6 +118,53 @@ public class DefaultAuthenticationFlows {
return false;
}
+ public static void resetCredentialsFlow(RealmModel realm) {
+ AuthenticationFlowModel grant = new AuthenticationFlowModel();
+ grant.setAlias(RESET_CREDENTIALS_FLOW);
+ grant.setDescription("Reset credentials for a user if they forgot their password or something");
+ grant.setProviderId("basic-flow");
+ grant.setTopLevel(true);
+ grant.setBuiltIn(true);
+ grant = realm.addAuthenticationFlow(grant);
+ realm.setResetCredentialsFlow(grant);
+
+ // username
+ AuthenticationExecutionModel execution = new AuthenticationExecutionModel();
+ execution.setParentFlow(grant.getId());
+ execution.setRequirement(AuthenticationExecutionModel.Requirement.REQUIRED);
+ execution.setAuthenticator("reset-credentials-choose-user");
+ execution.setPriority(10);
+ execution.setAuthenticatorFlow(false);
+ realm.addAuthenticatorExecution(execution);
+
+ // send email
+ execution = new AuthenticationExecutionModel();
+ execution.setParentFlow(grant.getId());
+ execution.setRequirement(AuthenticationExecutionModel.Requirement.REQUIRED);
+ execution.setAuthenticator("reset-credential-email");
+ execution.setPriority(20);
+ execution.setAuthenticatorFlow(false);
+ realm.addAuthenticatorExecution(execution);
+
+ // password
+ execution = new AuthenticationExecutionModel();
+ execution.setParentFlow(grant.getId());
+ execution.setRequirement(AuthenticationExecutionModel.Requirement.REQUIRED);
+ execution.setAuthenticator("reset-password");
+ execution.setPriority(30);
+ execution.setAuthenticatorFlow(false);
+ realm.addAuthenticatorExecution(execution);
+
+ // otp
+ execution = new AuthenticationExecutionModel();
+ execution.setParentFlow(grant.getId());
+ execution.setRequirement(AuthenticationExecutionModel.Requirement.OPTIONAL);
+ execution.setAuthenticator("reset-otp");
+ execution.setPriority(40);
+ execution.setAuthenticatorFlow(false);
+ realm.addAuthenticatorExecution(execution);
+ }
+
public static void directGrantFlow(RealmModel realm, boolean migrate) {
AuthenticationFlowModel grant = new AuthenticationFlowModel();
grant.setAlias(DIRECT_GRANT_FLOW);
@@ -160,9 +207,6 @@ public class DefaultAuthenticationFlows {
execution.setPriority(30);
execution.setAuthenticatorFlow(false);
realm.addAuthenticatorExecution(execution);
-
-
-
}
public static void browserFlow(RealmModel realm, boolean migrate) {
diff --git a/model/file/src/main/java/org/keycloak/models/file/adapter/RealmAdapter.java b/model/file/src/main/java/org/keycloak/models/file/adapter/RealmAdapter.java
index 535328f7d2..888728cd37 100755
--- a/model/file/src/main/java/org/keycloak/models/file/adapter/RealmAdapter.java
+++ b/model/file/src/main/java/org/keycloak/models/file/adapter/RealmAdapter.java
@@ -1269,6 +1269,19 @@ public class RealmAdapter implements RealmModel {
}
+ @Override
+ public AuthenticationFlowModel getResetCredentialsFlow() {
+ String flowId = realm.getResetCredentialsFlow();
+ if (flowId == null) return null;
+ return getAuthenticationFlowById(flowId);
+ }
+
+ @Override
+ public void setResetCredentialsFlow(AuthenticationFlowModel flow) {
+ realm.setResetCredentialsFlow(flow.getId());
+ }
+
+
@Override
public List getAuthenticationFlows() {
diff --git a/model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmAdapter.java b/model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmAdapter.java
index cbffaa08b9..5a0e7b8e7c 100755
--- a/model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmAdapter.java
+++ b/model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmAdapter.java
@@ -1051,6 +1051,18 @@ public class RealmAdapter implements RealmModel {
getDelegateForUpdate();
updated.setDirectGrantFlow(flow);
+ }
+ @Override
+ public AuthenticationFlowModel getResetCredentialsFlow() {
+ if (updated != null) return updated.getResetCredentialsFlow();
+ return cached.getResetCredentialsFlow();
+ }
+
+ @Override
+ public void setResetCredentialsFlow(AuthenticationFlowModel flow) {
+ getDelegateForUpdate();
+ updated.setResetCredentialsFlow(flow);
+
}
@Override
diff --git a/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/entities/CachedRealm.java b/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/entities/CachedRealm.java
index a406d9db96..c708d40fcd 100755
--- a/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/entities/CachedRealm.java
+++ b/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/entities/CachedRealm.java
@@ -94,6 +94,7 @@ public class CachedRealm implements Serializable {
private AuthenticationFlowModel browserFlow;
private AuthenticationFlowModel registrationFlow;
private AuthenticationFlowModel directGrantFlow;
+ private AuthenticationFlowModel resetCredentialsFlow;
private boolean eventsEnabled;
private long eventsExpiration;
@@ -221,6 +222,7 @@ public class CachedRealm implements Serializable {
browserFlow = model.getBrowserFlow();
registrationFlow = model.getRegistrationFlow();
directGrantFlow = model.getDirectGrantFlow();
+ resetCredentialsFlow = model.getResetCredentialsFlow();
}
@@ -483,4 +485,8 @@ public class CachedRealm implements Serializable {
public AuthenticationFlowModel getDirectGrantFlow() {
return directGrantFlow;
}
+
+ public AuthenticationFlowModel getResetCredentialsFlow() {
+ return resetCredentialsFlow;
+ }
}
diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java b/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java
index 15ca36f231..33034b6a9d 100755
--- a/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java
+++ b/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java
@@ -1580,6 +1580,18 @@ public class RealmAdapter implements RealmModel {
}
+ @Override
+ public AuthenticationFlowModel getResetCredentialsFlow() {
+ String flowId = realm.getResetCredentialsFlow();
+ if (flowId == null) return null;
+ return getAuthenticationFlowById(flowId);
+ }
+
+ @Override
+ public void setResetCredentialsFlow(AuthenticationFlowModel flow) {
+ realm.setResetCredentialsFlow(flow.getId());
+ }
+
@Override
public List getAuthenticationFlows() {
TypedQuery query = em.createNamedQuery("getAuthenticationFlowsByRealm", AuthenticationFlowEntity.class);
diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/RealmEntity.java b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/RealmEntity.java
index ddb6e34de7..65c44cfe66 100755
--- a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/RealmEntity.java
+++ b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/RealmEntity.java
@@ -188,6 +188,8 @@ public class RealmEntity {
@Column(name="DIRECT_GRANT_FLOW")
protected String directGrantFlow;
+ @Column(name="RESET_CREDENTIALS_FLOW")
+ protected String resetCredentialsFlow;
@@ -678,5 +680,13 @@ public class RealmEntity {
public void setDirectGrantFlow(String directGrantFlow) {
this.directGrantFlow = directGrantFlow;
}
+
+ public String getResetCredentialsFlow() {
+ return resetCredentialsFlow;
+ }
+
+ public void setResetCredentialsFlow(String resetCredentialsFlow) {
+ this.resetCredentialsFlow = resetCredentialsFlow;
+ }
}
diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/RealmAdapter.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/RealmAdapter.java
index 295ba6d8b6..cfe05bc14e 100755
--- a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/RealmAdapter.java
+++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/RealmAdapter.java
@@ -1351,6 +1351,20 @@ public class RealmAdapter extends AbstractMongoAdapter impleme
}
+ @Override
+ public AuthenticationFlowModel getResetCredentialsFlow() {
+ String flowId = realm.getResetCredentialsFlow();
+ if (flowId == null) return null;
+ return getAuthenticationFlowById(flowId);
+ }
+
+ @Override
+ public void setResetCredentialsFlow(AuthenticationFlowModel flow) {
+ realm.setResetCredentialsFlow(flow.getId());
+ updateRealm();
+ }
+
+
@Override
public List getAuthenticationFlows() {
diff --git a/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlService.java b/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlService.java
index 8c0481fe84..38304e6b30 100755
--- a/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlService.java
+++ b/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlService.java
@@ -36,6 +36,7 @@ import org.keycloak.services.Urls;
import org.keycloak.services.managers.AuthenticationManager;
import org.keycloak.services.managers.ClientSessionCode;
import org.keycloak.services.messages.Messages;
+import org.keycloak.services.resources.LoginActionsService;
import org.keycloak.services.resources.RealmsResource;
import org.keycloak.util.StreamUtil;
@@ -505,6 +506,7 @@ public class SamlService {
String flowId = flow.getId();
AuthenticationProcessor processor = new AuthenticationProcessor();
processor.setClientSession(clientSession)
+ .setFlowPath(LoginActionsService.AUTHENTICATE_PATH)
.setFlowId(flowId)
.setConnection(clientConnection)
.setEventBuilder(event)
diff --git a/services/src/main/java/org/keycloak/authentication/AuthenticationProcessor.java b/services/src/main/java/org/keycloak/authentication/AuthenticationProcessor.java
index c7c80f4c1f..9f3eab9437 100755
--- a/services/src/main/java/org/keycloak/authentication/AuthenticationProcessor.java
+++ b/services/src/main/java/org/keycloak/authentication/AuthenticationProcessor.java
@@ -49,6 +49,7 @@ public class AuthenticationProcessor {
protected EventBuilder event;
protected HttpRequest request;
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
@@ -131,6 +132,16 @@ public class AuthenticationProcessor {
return this;
}
+ /**
+ * This is the path segment to append when generating an action URL.
+ *
+ * @param flowPath
+ */
+ public AuthenticationProcessor setFlowPath(String flowPath) {
+ this.flowPath = flowPath;
+ return this;
+ }
+
public AuthenticationProcessor setForwardedErrorMessage(String forwardedErrorMessage) {
this.forwardedErrorMessage = forwardedErrorMessage;
return this;
@@ -358,7 +369,8 @@ public class AuthenticationProcessor {
@Override
public URI getActionUrl(String code) {
- return LoginActionsService.authenticationFormProcessor(getUriInfo())
+ return LoginActionsService.loginActionsBaseUrl(getUriInfo())
+ .path(AuthenticationProcessor.this.flowPath)
.queryParam(OAuth2Constants.CODE, code)
.queryParam("execution", getExecution().getId())
.build(getRealm().getName());
@@ -490,6 +502,8 @@ public class AuthenticationProcessor {
resetFlow(clientSession);
return authenticate();
}
+ UserModel authUser = clientSession.getAuthenticatedUser();
+ validateUser(authUser);
AuthenticationExecutionModel model = realm.getAuthenticationExecutionById(execution);
if (model == null) {
logger.debug("Cannot find execution, reseting flow");
diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/resetcred/AbstractSetRequiredActionAuthenticator.java b/services/src/main/java/org/keycloak/authentication/authenticators/resetcred/AbstractSetRequiredActionAuthenticator.java
new file mode 100755
index 0000000000..50e9936a11
--- /dev/null
+++ b/services/src/main/java/org/keycloak/authentication/authenticators/resetcred/AbstractSetRequiredActionAuthenticator.java
@@ -0,0 +1,92 @@
+package org.keycloak.authentication.authenticators.resetcred;
+
+import org.keycloak.Config;
+import org.keycloak.authentication.AuthenticationFlowContext;
+import org.keycloak.authentication.Authenticator;
+import org.keycloak.authentication.AuthenticatorFactory;
+import org.keycloak.models.AuthenticationExecutionModel;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.KeycloakSessionFactory;
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.UserModel;
+import org.keycloak.provider.ProviderConfigProperty;
+
+import java.util.List;
+
+/**
+ * @author Bill Burke
+ * @version $Revision: 1 $
+ */
+public abstract class AbstractSetRequiredActionAuthenticator implements Authenticator, AuthenticatorFactory {
+ public static final AuthenticationExecutionModel.Requirement[] REQUIREMENT_CHOICES = {
+ AuthenticationExecutionModel.Requirement.REQUIRED,
+ AuthenticationExecutionModel.Requirement.OPTIONAL,
+ AuthenticationExecutionModel.Requirement.DISABLED
+
+ };
+
+ @Override
+ public void action(AuthenticationFlowContext context) {
+
+ }
+
+ @Override
+ public boolean requiresUser() {
+ return true;
+ }
+
+ @Override
+ public void setRequiredActions(KeycloakSession session, RealmModel realm, UserModel user) {
+
+ }
+
+ @Override
+ public String getReferenceCategory() {
+ return null;
+ }
+
+ @Override
+ public boolean isConfigurable() {
+ return false;
+ }
+
+ @Override
+ public AuthenticationExecutionModel.Requirement[] getRequirementChoices() {
+ return REQUIREMENT_CHOICES;
+ }
+
+ @Override
+ public boolean isUserSetupAllowed() {
+ return false;
+ }
+
+ @Override
+ public List getConfigProperties() {
+ return null;
+ }
+
+ @Override
+ public void close() {
+
+ }
+
+ @Override
+ public Authenticator create(KeycloakSession session) {
+ return this;
+ }
+
+ @Override
+ public void init(Config.Scope config) {
+
+ }
+
+ @Override
+ public void postInit(KeycloakSessionFactory factory) {
+
+ }
+
+ @Override
+ public boolean configuredFor(KeycloakSession session, RealmModel realm, UserModel user) {
+ return true;
+ }
+}
diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/resetcred/ResetCredentialChooseUser.java b/services/src/main/java/org/keycloak/authentication/authenticators/resetcred/ResetCredentialChooseUser.java
new file mode 100755
index 0000000000..1edd53586f
--- /dev/null
+++ b/services/src/main/java/org/keycloak/authentication/authenticators/resetcred/ResetCredentialChooseUser.java
@@ -0,0 +1,174 @@
+package org.keycloak.authentication.authenticators.resetcred;
+
+import org.keycloak.Config;
+import org.keycloak.authentication.AuthenticationFlowContext;
+import org.keycloak.authentication.AuthenticationFlowError;
+import org.keycloak.authentication.Authenticator;
+import org.keycloak.authentication.AuthenticatorFactory;
+import org.keycloak.email.EmailException;
+import org.keycloak.email.EmailProvider;
+import org.keycloak.events.Details;
+import org.keycloak.events.Errors;
+import org.keycloak.events.EventBuilder;
+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.protocol.oidc.TokenManager;
+import org.keycloak.provider.ProviderConfigProperty;
+import org.keycloak.services.Urls;
+import org.keycloak.services.messages.Messages;
+
+import javax.ws.rs.core.MultivaluedMap;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.UriBuilder;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * @author Bill Burke
+ * @version $Revision: 1 $
+ */
+public class ResetCredentialChooseUser implements Authenticator, AuthenticatorFactory {
+
+ public static final String PROVIDER_ID = "reset-credentials-choose-user";
+
+ @Override
+ public void authenticate(AuthenticationFlowContext context) {
+ Response challenge = context.form().createPasswordReset();
+ context.challenge(challenge);
+ }
+
+ @Override
+ public void action(AuthenticationFlowContext context) {
+ EventBuilder event = context.getEvent();
+ MultivaluedMap formData = context.getHttpRequest().getDecodedFormParameters();
+ String username = formData.getFirst("username");
+ if (username == null || username.isEmpty()) {
+ event.error(Errors.USERNAME_MISSING);
+ Response challenge = context.form()
+ .setError(Messages.MISSING_USERNAME)
+ .createPasswordReset();
+ context.failureChallenge(AuthenticationFlowError.INVALID_USER, challenge);
+ return;
+ }
+
+ UserModel user = context.getSession().users().getUserByUsername(username, context.getRealm());
+ if (user == null && username.contains("@")) {
+ user = context.getSession().users().getUserByEmail(username, context.getRealm());
+ }
+
+ if (user == null) {
+ event.error(Errors.INVALID_USER_CREDENTIALS);
+ Response challenge = context.form()
+ .setError(Messages.INVALID_USER)
+ .createPasswordReset();
+ context.failureChallenge(AuthenticationFlowError.INVALID_USER, challenge);
+ return;
+ }
+
+ if (!user.isEnabled()) {
+ event.user(user).error(Errors.USER_DISABLED);
+ Response challenge = context.form()
+ .setError(Messages.ACCOUNT_DISABLED)
+ .createPasswordReset();
+ context.failureChallenge(AuthenticationFlowError.INVALID_USER, challenge);
+ return;
+ }
+
+ if (user.getEmail() == null || user.getEmail().trim().length() == 0) {
+ event.user(user).error(Errors.INVALID_EMAIL);
+ Response challenge = context.form()
+ .setError(Messages.INVALID_EMAIL)
+ .createPasswordReset();
+ context.failureChallenge(AuthenticationFlowError.INVALID_USER, challenge);
+ return;
+ }
+
+ context.setUser(user);
+ context.success();
+ }
+
+ @Override
+ public boolean requiresUser() {
+ return false;
+ }
+
+ @Override
+ public boolean configuredFor(KeycloakSession session, RealmModel realm, UserModel user) {
+ return true;
+ }
+
+ @Override
+ public void setRequiredActions(KeycloakSession session, RealmModel realm, UserModel user) {
+
+ }
+
+ @Override
+ public String getDisplayType() {
+ return "Choose User";
+ }
+
+ @Override
+ public String getReferenceCategory() {
+ return null;
+ }
+
+ @Override
+ public boolean isConfigurable() {
+ return false;
+ }
+
+ public static final AuthenticationExecutionModel.Requirement[] REQUIREMENT_CHOICES = {
+ AuthenticationExecutionModel.Requirement.REQUIRED
+ };
+
+ @Override
+ public AuthenticationExecutionModel.Requirement[] getRequirementChoices() {
+ return REQUIREMENT_CHOICES;
+ }
+
+ @Override
+ public boolean isUserSetupAllowed() {
+ return false;
+ }
+
+ @Override
+ public String getHelpText() {
+ return "Choose a user to reset credentials for";
+ }
+
+ @Override
+ public List getConfigProperties() {
+ return null;
+ }
+
+ @Override
+ public void close() {
+
+ }
+
+ @Override
+ public Authenticator create(KeycloakSession session) {
+ return this;
+ }
+
+ @Override
+ public void init(Config.Scope config) {
+
+ }
+
+ @Override
+ public void postInit(KeycloakSessionFactory factory) {
+
+ }
+
+ @Override
+ public String getId() {
+ return PROVIDER_ID;
+ }
+}
diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/resetcred/ResetCredentialEmail.java b/services/src/main/java/org/keycloak/authentication/authenticators/resetcred/ResetCredentialEmail.java
new file mode 100755
index 0000000000..5fb3c00145
--- /dev/null
+++ b/services/src/main/java/org/keycloak/authentication/authenticators/resetcred/ResetCredentialEmail.java
@@ -0,0 +1,189 @@
+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;
+import org.keycloak.authentication.Authenticator;
+import org.keycloak.authentication.AuthenticatorFactory;
+import org.keycloak.email.EmailException;
+import org.keycloak.email.EmailProvider;
+import org.keycloak.events.Details;
+import org.keycloak.events.Errors;
+import org.keycloak.events.EventBuilder;
+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.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;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * @author Bill Burke
+ * @version $Revision: 1 $
+ */
+public class ResetCredentialEmail implements Authenticator, AuthenticatorFactory {
+ public static final String RESET_CREDENTIAL_SECRET = "RESET_CREDENTIAL_SECRET";
+ public static final String KEY = "key";
+ protected static Logger logger = Logger.getLogger(ResetCredentialEmail.class);
+
+ public static final String PROVIDER_ID = "reset-credential-email";
+
+ @Override
+ public void authenticate(AuthenticationFlowContext context) {
+ UserModel user = context.getUser();
+ EventBuilder event = context.getEvent();
+ if (user.getEmail() == null || user.getEmail().trim().length() == 0) {
+ event.user(user).error(Errors.INVALID_EMAIL);
+ Response challenge = context.form()
+ .setError(Messages.INVALID_EMAIL)
+ .createPasswordReset();
+ context.failureChallenge(AuthenticationFlowError.INVALID_USER, challenge);
+ return;
+ }
+
+ // We send the secret in the email in a link as a query param. We don't need to sign it or anything because
+ // it can only be guessed once, and it must match watch is stored in the client session.
+ String secret = HmacOTP.generateSecret(10);
+ context.getClientSession().setNote(RESET_CREDENTIAL_SECRET, secret);
+ String link = UriBuilder.fromUri(context.getActionUrl()).queryParam(KEY, secret).build().toString();
+ long expiration = TimeUnit.SECONDS.toMinutes(context.getRealm().getAccessCodeLifespanUserAction());
+ try {
+
+ context.getSession().getProvider(EmailProvider.class).setRealm(context.getRealm()).setUser(user).sendPasswordReset(secret, link, expiration);
+
+ event.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);
+ } catch (EmailException e) {
+ event.error(Errors.EMAIL_SEND_FAILED);
+ logger.error("Failed to send password reset email", e);
+ Response challenge = context.form()
+ .setError(Messages.EMAIL_SENT_ERROR)
+ .createErrorPage();
+ context.failure(AuthenticationFlowError.INTERNAL_ERROR, challenge);
+ }
+ }
+
+ @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")) {
+ key = context.getHttpRequest().getDecodedFormParameters().getFirst(KEY);
+ }
+
+ // Can only guess once! We remove the note so another guess can't happen
+ context.getClientSession().removeNote(RESET_CREDENTIAL_SECRET);
+ if (secret == null || key == null || !secret.equals(key)) {
+ context.getEvent().error(Errors.INVALID_USER_CREDENTIALS);
+ Response challenge = context.form()
+ .setError(Messages.INVALID_ACCESS_CODE)
+ .createErrorPage();
+ context.failure(AuthenticationFlowError.INTERNAL_ERROR, challenge);
+ return;
+ }
+ context.success();
+ }
+
+ @Override
+ public boolean requiresUser() {
+ return true;
+ }
+
+ @Override
+ public boolean configuredFor(KeycloakSession session, RealmModel realm, UserModel user) {
+ return true;
+ }
+
+ @Override
+ public void setRequiredActions(KeycloakSession session, RealmModel realm, UserModel user) {
+
+ }
+
+ @Override
+ public String getDisplayType() {
+ return "Reset Via Email";
+ }
+
+ @Override
+ public String getReferenceCategory() {
+ return null;
+ }
+
+ @Override
+ public boolean isConfigurable() {
+ return false;
+ }
+
+ public static final AuthenticationExecutionModel.Requirement[] REQUIREMENT_CHOICES = {
+ AuthenticationExecutionModel.Requirement.REQUIRED
+ };
+
+ @Override
+ public AuthenticationExecutionModel.Requirement[] getRequirementChoices() {
+ return REQUIREMENT_CHOICES;
+ }
+
+ @Override
+ public boolean isUserSetupAllowed() {
+ return false;
+ }
+
+ @Override
+ public String getHelpText() {
+ return "Send email to user and wait for response.";
+ }
+
+ @Override
+ public List getConfigProperties() {
+ return null;
+ }
+
+ @Override
+ public void close() {
+
+ }
+
+ @Override
+ public Authenticator create(KeycloakSession session) {
+ return this;
+ }
+
+ @Override
+ public void init(Config.Scope config) {
+
+ }
+
+ @Override
+ public void postInit(KeycloakSessionFactory factory) {
+
+ }
+
+ @Override
+ public String getId() {
+ return PROVIDER_ID;
+ }
+}
diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/resetcred/ResetOTP.java b/services/src/main/java/org/keycloak/authentication/authenticators/resetcred/ResetOTP.java
new file mode 100755
index 0000000000..d99dd9fb27
--- /dev/null
+++ b/services/src/main/java/org/keycloak/authentication/authenticators/resetcred/ResetOTP.java
@@ -0,0 +1,45 @@
+package org.keycloak.authentication.authenticators.resetcred;
+
+import org.keycloak.authentication.AuthenticationFlowContext;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.UserCredentialModel;
+import org.keycloak.models.UserModel;
+
+/**
+ * @author Bill Burke
+ * @version $Revision: 1 $
+ */
+public class ResetOTP extends AbstractSetRequiredActionAuthenticator {
+
+ public static final String PROVIDER_ID = "reset-otp";
+
+ @Override
+ public void authenticate(AuthenticationFlowContext context) {
+ if (context.getExecution().isRequired() ||
+ (context.getExecution().isOptional() &&
+ configuredFor(context))) {
+ context.getUser().addRequiredAction(UserModel.RequiredAction.CONFIGURE_TOTP);
+ }
+ context.success();
+ }
+
+ protected boolean configuredFor(AuthenticationFlowContext context) {
+ return context.getSession().users().configuredForCredentialType(context.getRealm().getOTPPolicy().getType(), context.getRealm(), context.getUser());
+ }
+
+ @Override
+ public String getDisplayType() {
+ return "Reset OTP";
+ }
+
+ @Override
+ public String getHelpText() {
+ return "Sets the Configure OTP required action if execution is REQUIRED. Will also set it if execution is OPTIONAL and the OTP is currently configured for it.";
+ }
+
+ @Override
+ public String getId() {
+ return PROVIDER_ID;
+ }
+}
diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/resetcred/ResetPassword.java b/services/src/main/java/org/keycloak/authentication/authenticators/resetcred/ResetPassword.java
new file mode 100755
index 0000000000..f41e5bd949
--- /dev/null
+++ b/services/src/main/java/org/keycloak/authentication/authenticators/resetcred/ResetPassword.java
@@ -0,0 +1,53 @@
+package org.keycloak.authentication.authenticators.resetcred;
+
+import org.keycloak.Config;
+import org.keycloak.authentication.AuthenticationFlowContext;
+import org.keycloak.authentication.Authenticator;
+import org.keycloak.authentication.AuthenticatorFactory;
+import org.keycloak.models.AuthenticationExecutionModel;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.KeycloakSessionFactory;
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.UserCredentialModel;
+import org.keycloak.models.UserModel;
+import org.keycloak.provider.ProviderConfigProperty;
+
+import java.util.List;
+
+/**
+ * @author Bill Burke
+ * @version $Revision: 1 $
+ */
+public class ResetPassword extends AbstractSetRequiredActionAuthenticator {
+
+ public static final String PROVIDER_ID = "reset-password";
+
+ @Override
+ public void authenticate(AuthenticationFlowContext context) {
+ if (context.getExecution().isRequired() ||
+ (context.getExecution().isOptional() &&
+ configuredFor(context))) {
+ context.getUser().addRequiredAction(UserModel.RequiredAction.UPDATE_PASSWORD);
+ }
+ context.success();
+ }
+
+ protected boolean configuredFor(AuthenticationFlowContext context) {
+ return context.getSession().users().configuredForCredentialType(UserCredentialModel.PASSWORD, context.getRealm(), context.getUser());
+ }
+
+ @Override
+ public String getDisplayType() {
+ return "Reset Password";
+ }
+
+ @Override
+ public String getHelpText() {
+ return "Sets the Update Password required action if execution is REQUIRED. Will also set it if execution is OPTIONAL and the password is currently configured for it.";
+ }
+
+ @Override
+ public String getId() {
+ return PROVIDER_ID;
+ }
+}
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/AuthorizationEndpoint.java b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/AuthorizationEndpoint.java
index e368029104..fda09ee9a9 100755
--- a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/AuthorizationEndpoint.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/AuthorizationEndpoint.java
@@ -27,6 +27,7 @@ import org.keycloak.services.managers.AuthenticationManager;
import org.keycloak.services.managers.ClientSessionCode;
import org.keycloak.services.messages.Messages;
import org.keycloak.services.Urls;
+import org.keycloak.services.resources.LoginActionsService;
import javax.ws.rs.GET;
import javax.ws.rs.core.Context;
@@ -267,6 +268,7 @@ public class AuthorizationEndpoint {
String flowId = flow.getId();
AuthenticationProcessor processor = new AuthenticationProcessor();
processor.setClientSession(clientSession)
+ .setFlowPath(LoginActionsService.AUTHENTICATE_PATH)
.setFlowId(flowId)
.setConnection(clientConnection)
.setEventBuilder(event)
diff --git a/services/src/main/java/org/keycloak/services/Urls.java b/services/src/main/java/org/keycloak/services/Urls.java
index 5254993e0c..57991b39c2 100755
--- a/services/src/main/java/org/keycloak/services/Urls.java
+++ b/services/src/main/java/org/keycloak/services/Urls.java
@@ -152,12 +152,16 @@ public class Urls {
return loginActionsBase(baseUri).path(LoginActionsService.class, "emailVerification");
}
- public static URI loginPasswordReset(URI baseUri, String realmId) {
- return loginPasswordResetBuilder(baseUri).build(realmId);
+ public static URI loginResetCredentials(URI baseUri, String realmId) {
+ return loginResetCredentialsBuilder(baseUri).build(realmId);
}
- public static UriBuilder loginPasswordResetBuilder(URI baseUri) {
- return loginActionsBase(baseUri).path(LoginActionsService.class, "passwordReset");
+ public static UriBuilder recoverPasswordBuilder(URI baseUri) {
+ return loginActionsBase(baseUri).path(LoginActionsService.class, "recoverPassword");
+ }
+
+ public static UriBuilder loginResetCredentialsBuilder(URI baseUri) {
+ return loginActionsBase(baseUri).path(LoginActionsService.RESET_CREDENTIALS_PATH);
}
public static URI loginUsernameReminder(URI baseUri, String realmId) {
diff --git a/services/src/main/java/org/keycloak/services/resources/IdentityBrokerService.java b/services/src/main/java/org/keycloak/services/resources/IdentityBrokerService.java
index 3ac8af5e1a..c5a7f8f0ac 100755
--- a/services/src/main/java/org/keycloak/services/resources/IdentityBrokerService.java
+++ b/services/src/main/java/org/keycloak/services/resources/IdentityBrokerService.java
@@ -473,6 +473,7 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal
String flowId = flow.getId();
AuthenticationProcessor processor = new AuthenticationProcessor();
processor.setClientSession(clientSession)
+ .setFlowPath(LoginActionsService.AUTHENTICATE_PATH)
.setFlowId(flowId)
.setConnection(clientConnection)
.setEventBuilder(event)
diff --git a/services/src/main/java/org/keycloak/services/resources/LoginActionsService.java b/services/src/main/java/org/keycloak/services/resources/LoginActionsService.java
index 0abca72f41..f34bed652e 100755
--- a/services/src/main/java/org/keycloak/services/resources/LoginActionsService.java
+++ b/services/src/main/java/org/keycloak/services/resources/LoginActionsService.java
@@ -50,13 +50,10 @@ import org.keycloak.models.UserModel;
import org.keycloak.models.UserModel.RequiredAction;
import org.keycloak.models.UserSessionModel;
import org.keycloak.models.utils.CredentialValidation;
-import org.keycloak.models.utils.DefaultAuthenticationFlows;
import org.keycloak.models.utils.FormMessage;
-import org.keycloak.models.utils.TimeBasedOTP;
import org.keycloak.protocol.LoginProtocol;
import org.keycloak.protocol.RestartLoginCookie;
import org.keycloak.protocol.oidc.TokenManager;
-import org.keycloak.representations.idm.CredentialRepresentation;
import org.keycloak.services.ErrorPage;
import org.keycloak.services.Urls;
import org.keycloak.services.managers.AuthenticationManager;
@@ -70,7 +67,6 @@ import javax.ws.rs.Consumes;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
-import javax.ws.rs.PathParam;
import javax.ws.rs.QueryParam;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.Context;
@@ -93,6 +89,9 @@ public class LoginActionsService {
protected static final Logger logger = Logger.getLogger(LoginActionsService.class);
public static final String ACTION_COOKIE = "KEYCLOAK_ACTION";
+ public static final String AUTHENTICATE_PATH = "authenticate";
+ public static final String REGISTRATION_PATH = "registration";
+ public static final String RESET_CREDENTIALS_PATH = "reset-credentials";
private RealmModel realm;
@@ -226,7 +225,7 @@ public class LoginActionsService {
ClientSessionModel clientSession = RestartLoginCookie.restartSession(session, realm, code);
if (clientSession != null) {
event.clone().detail(Details.RESTART_AFTER_TIMEOUT, "true").error(Errors.EXPIRED_CODE);
- response = processFlow(null, clientSession, realm.getBrowserFlow(), Messages.LOGIN_TIMEOUT);
+ response = processFlow(null, clientSession, AUTHENTICATE_PATH, realm.getBrowserFlow(), Messages.LOGIN_TIMEOUT);
return false;
}
} catch (Exception e) {
@@ -268,7 +267,7 @@ public class LoginActionsService {
* @param code
* @return
*/
- @Path("authenticate")
+ @Path(AUTHENTICATE_PATH)
@GET
public Response authenticate(@QueryParam("code") String code,
@QueryParam("execution") String execution) {
@@ -290,12 +289,13 @@ public class LoginActionsService {
}
protected Response processAuthentication(String execution, ClientSessionModel clientSession, String errorMessage) {
- return processFlow(execution, clientSession, realm.getBrowserFlow(), errorMessage);
+ return processFlow(execution, clientSession, AUTHENTICATE_PATH, realm.getBrowserFlow(), errorMessage);
}
- protected Response processFlow(String execution, ClientSessionModel clientSession, AuthenticationFlowModel flow, String errorMessage) {
+ protected Response processFlow(String execution, ClientSessionModel clientSession, String flowPath, AuthenticationFlowModel flow, String errorMessage) {
AuthenticationProcessor processor = new AuthenticationProcessor();
processor.setClientSession(clientSession)
+ .setFlowPath(flowPath)
.setFlowId(flow.getId())
.setConnection(clientConnection)
.setEventBuilder(event)
@@ -323,7 +323,7 @@ public class LoginActionsService {
* @param code
* @return
*/
- @Path("authenticate")
+ @Path(AUTHENTICATE_PATH)
@POST
public Response authenticateForm(@QueryParam("code") String code,
@QueryParam("execution") String execution) {
@@ -338,8 +338,39 @@ public class LoginActionsService {
return processAuthentication(execution, clientSession, null);
}
+ @Path(RESET_CREDENTIALS_PATH)
+ @POST
+ public Response resetCredentialsPOST(@QueryParam("code") String code,
+ @QueryParam("execution") String execution) {
+ return resetCredentials(code, execution);
+ }
+
+ @Path(RESET_CREDENTIALS_PATH)
+ @GET
+ public Response resetCredentialsGET(@QueryParam("code") String code,
+ @QueryParam("execution") String execution) {
+ return resetCredentials(code, execution);
+ }
+
+ protected Response resetCredentials(String code, String execution) {
+ event.event(EventType.RESET_PASSWORD);
+ Checks checks = new Checks();
+ if (!checks.verifyCode(code, ClientSessionModel.Action.AUTHENTICATE.name())) {
+ return checks.response;
+ }
+ final ClientSessionCode clientCode = checks.clientCode;
+ final ClientSessionModel clientSession = clientCode.getClientSession();
+
+ return processResetCredentials(execution, clientSession, null);
+ }
+
+ protected Response processResetCredentials(String execution, ClientSessionModel clientSession, String errorMessage) {
+ return processFlow(execution, clientSession, RESET_CREDENTIALS_PATH, realm.getResetCredentialsFlow(), errorMessage);
+ }
+
+
protected Response processRegistration(String execution, ClientSessionModel clientSession, String errorMessage) {
- return processFlow(execution, clientSession, realm.getRegistrationFlow(), errorMessage);
+ return processFlow(execution, clientSession, REGISTRATION_PATH, realm.getRegistrationFlow(), errorMessage);
}
@@ -349,7 +380,7 @@ public class LoginActionsService {
* @param code
* @return
*/
- @Path("registration")
+ @Path(REGISTRATION_PATH)
@GET
public Response registerPage(@QueryParam("code") String code,
@QueryParam("execution") String execution) {
@@ -380,7 +411,7 @@ public class LoginActionsService {
* @param code
* @return
*/
- @Path("registration")
+ @Path(REGISTRATION_PATH)
@POST
public Response processRegister(@QueryParam("code") String code,
@QueryParam("execution") String execution) {
@@ -393,10 +424,6 @@ public class LoginActionsService {
if (!checks.verifyCode(code, ClientSessionModel.Action.AUTHENTICATE.name())) {
return checks.response;
}
- if (!realm.isRegistrationAllowed()) {
- event.error(Errors.REGISTRATION_DISABLED);
- return ErrorPage.error(session, Messages.REGISTRATION_NOT_ALLOWED);
- }
ClientSessionCode clientCode = checks.clientCode;
ClientSessionModel clientSession = clientCode.getClientSession();
@@ -647,13 +674,10 @@ public class LoginActionsService {
event.event(EventType.UPDATE_PASSWORD).success();
if (clientSession.getAction().equals(ClientSessionModel.Action.RECOVER_PASSWORD.name())) {
- String actionCookieValue = getActionCookie();
- if (actionCookieValue == null || !actionCookieValue.equals(userSession.getId())) {
- session.sessions().removeClientSession(realm, clientSession);
- return session.getProvider(LoginFormsProvider.class)
- .setSuccess(Messages.ACCOUNT_PASSWORD_UPDATED)
- .createInfoPage();
- }
+ session.sessions().removeClientSession(realm, clientSession);
+ return session.getProvider(LoginFormsProvider.class)
+ .setSuccess(Messages.ACCOUNT_PASSWORD_UPDATED)
+ .createInfoPage();
}
event = event.clone().event(EventType.LOGIN);
@@ -712,34 +736,33 @@ public class LoginActionsService {
}
}
- @Path("password-reset")
+ /**
+ * Initiated by admin, not the user on login
+ *
+ * @param key
+ * @return
+ */
+ @Path("recover-password")
@GET
- public Response passwordReset(@QueryParam("code") String code, @QueryParam("key") String key) {
+ public Response recoverPassword(@QueryParam("key") String key) {
event.event(EventType.RESET_PASSWORD);
- if (!realm.isResetPasswordAllowed()) {
- event.error(Errors.RESET_CREDENTIAL_DISABLED);
- return ErrorPage.error(session, Messages.RESET_CREDENTIAL_NOT_ALLOWED);
- }
if (key != null) {
Checks checks = new Checks();
if (!checks.verifyCode(key, ClientSessionModel.Action.RECOVER_PASSWORD.name())) {
- return checks.response;
+ event.error(Errors.RESET_CREDENTIAL_DISABLED);
+ return ErrorPage.error(session, Messages.INVALID_CODE);
}
ClientSessionCode accessCode = checks.clientCode;
return session.getProvider(LoginFormsProvider.class)
.setClientSessionCode(accessCode.getCode())
.createResponse(RequiredAction.UPDATE_PASSWORD);
} else {
- return session.getProvider(LoginFormsProvider.class)
- .setClientSessionCode(code)
- .createPasswordReset();
+ event.error(Errors.RESET_CREDENTIAL_DISABLED);
+ return ErrorPage.error(session, Messages.INVALID_CODE);
}
}
- @Path("password-reset")
- @POST
- @Consumes(MediaType.APPLICATION_FORM_URLENCODED)
- public Response sendPasswordReset(@QueryParam("code") String code,
+ private Response sendPasswordReset(@QueryParam("code") String code,
final MultivaluedMap formData) {
event.event(EventType.SEND_RESET_PASSWORD);
if (!realm.isResetPasswordAllowed()) {
@@ -791,13 +814,13 @@ public class LoginActionsService {
accessCode.setAction(ClientSessionModel.Action.RECOVER_PASSWORD.name());
try {
- UriBuilder builder = Urls.loginPasswordResetBuilder(uriInfo.getBaseUri());
+ UriBuilder builder = Urls.loginResetCredentialsBuilder(uriInfo.getBaseUri());
builder.queryParam("key", accessCode.getCode());
String link = builder.build(realm.getName()).toString();
long expiration = TimeUnit.SECONDS.toMinutes(realm.getAccessCodeLifespanUserAction());
- this.session.getProvider(EmailProvider.class).setRealm(realm).setUser(user).sendPasswordReset(link, expiration);
+ this.session.getProvider(EmailProvider.class).setRealm(realm).setUser(user).sendChangePassword(link, expiration);
event.detail(Details.EMAIL, user.getEmail()).detail(Details.CODE_ID, clientSession.getId()).success();
} catch (EmailException e) {
diff --git a/services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java b/services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java
index 245a3d332b..066ce30817 100755
--- a/services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java
+++ b/services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java
@@ -854,13 +854,13 @@ public class UsersResource {
accessCode.setAction(ClientSessionModel.Action.RECOVER_PASSWORD.name());
try {
- UriBuilder builder = Urls.loginPasswordResetBuilder(uriInfo.getBaseUri());
+ UriBuilder builder = Urls.recoverPasswordBuilder(uriInfo.getBaseUri());
builder.queryParam("key", accessCode.getCode());
String link = builder.build(realm.getName()).toString();
long expiration = TimeUnit.SECONDS.toMinutes(realm.getAccessCodeLifespanUserAction());
- this.session.getProvider(EmailProvider.class).setRealm(realm).setUser(user).sendPasswordReset(link, expiration);
+ this.session.getProvider(EmailProvider.class).setRealm(realm).setUser(user).sendChangePassword(link, expiration);
//audit.user(user).detail(Details.EMAIL, user.getEmail()).detail(Details.CODE_ID, accessCode.getCodeId()).success();
diff --git a/services/src/main/resources/META-INF/services/org.keycloak.authentication.AuthenticatorFactory b/services/src/main/resources/META-INF/services/org.keycloak.authentication.AuthenticatorFactory
index 14fa990bdd..d6cada5df6 100755
--- a/services/src/main/resources/META-INF/services/org.keycloak.authentication.AuthenticatorFactory
+++ b/services/src/main/resources/META-INF/services/org.keycloak.authentication.AuthenticatorFactory
@@ -4,4 +4,8 @@ org.keycloak.authentication.authenticators.browser.OTPFormAuthenticatorFactory
org.keycloak.authentication.authenticators.browser.SpnegoAuthenticatorFactory
org.keycloak.authentication.authenticators.directgrant.ValidateOTP
org.keycloak.authentication.authenticators.directgrant.ValidatePassword
-org.keycloak.authentication.authenticators.directgrant.ValidateUsername
\ No newline at end of file
+org.keycloak.authentication.authenticators.directgrant.ValidateUsername
+org.keycloak.authentication.authenticators.resetcred.ResetCredentialChooseUser
+org.keycloak.authentication.authenticators.resetcred.ResetCredentialEmail
+org.keycloak.authentication.authenticators.resetcred.ResetOTP
+org.keycloak.authentication.authenticators.resetcred.ResetPassword
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/account/AccountTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/account/AccountTest.java
index 95d644eb82..bcaa98e257 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/account/AccountTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/account/AccountTest.java
@@ -167,7 +167,7 @@ public class AccountTest {
});
}
- //@Test
+ @Test
public void ideTesting() throws Exception {
Thread.sleep(100000000);
}