Details
diff --git a/examples/as7-eap-demo/server/src/main/resources/META-INF/testrealm.json b/examples/as7-eap-demo/server/src/main/resources/META-INF/testrealm.json
index f92e34c..910d488 100755
--- a/examples/as7-eap-demo/server/src/main/resources/META-INF/testrealm.json
+++ b/examples/as7-eap-demo/server/src/main/resources/META-INF/testrealm.json
@@ -8,7 +8,7 @@
"registrationAllowed": true,
"privateKey": "MIICXAIBAAKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQABAoGAfmO8gVhyBxdqlxmIuglbz8bcjQbhXJLR2EoS8ngTXmN1bo2L90M0mUKSdc7qF10LgETBzqL8jYlQIbt+e6TH8fcEpKCjUlyq0Mf/vVbfZSNaVycY13nTzo27iPyWQHK5NLuJzn1xvxxrUeXI6A2WFpGEBLbHjwpx5WQG9A+2scECQQDvdn9NE75HPTVPxBqsEd2z10TKkl9CZxu10Qby3iQQmWLEJ9LNmy3acvKrE3gMiYNWb6xHPKiIqOR1as7L24aTAkEAtyvQOlCvr5kAjVqrEKXalj0Tzewjweuxc0pskvArTI2Oo070h65GpoIKLc9jf+UA69cRtquwP93aZKtW06U8dQJAF2Y44ks/mK5+eyDqik3koCI08qaC8HYq2wVl7G2QkJ6sbAaILtcvD92ToOvyGyeE0flvmDZxMYlvaZnaQ0lcSQJBAKZU6umJi3/xeEbkJqMfeLclD27XGEFoPeNrmdx0q10Azp4NfJAY+Z8KRyQCR2BEG+oNitBOZ+YXF9KCpH3cdmECQHEigJhYg+ykOvr1aiZUMFT72HU0jnmQe2FVekuG+LJUt2Tm7GtMjTFoGpf0JwrVuZN39fOYAlo+nTixgeW7X8Y=",
"publicKey": "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB",
- "requiredCredentials": [ "password" ],
+ "requiredCredentials": [ "password", "totp" ],
"requiredApplicationCredentials": [ "password" ],
"requiredOAuthClientCredentials": [ "password" ],
"defaultRoles": [ "user" ],
diff --git a/forms/src/main/java/org/keycloak/forms/LoginBean.java b/forms/src/main/java/org/keycloak/forms/LoginBean.java
index aa02c92..a028886 100644
--- a/forms/src/main/java/org/keycloak/forms/LoginBean.java
+++ b/forms/src/main/java/org/keycloak/forms/LoginBean.java
@@ -47,6 +47,8 @@ public class LoginBean {
private String username;
+ private String password;
+
private List<RequiredCredential> requiredCredentials;
@PostConstruct
@@ -58,6 +60,7 @@ public class LoginBean {
MultivaluedMap<String, String> formData = (MultivaluedMap<String, String>) request.getAttribute(FormFlows.DATA);
if (formData != null) {
username = formData.getFirst("username");
+ password = formData.getFirst("password");
}
requiredCredentials = new LinkedList<RequiredCredential>();
@@ -72,6 +75,10 @@ public class LoginBean {
return username;
}
+ public String getPassword() {
+ return password;
+ }
+
public List<RequiredCredential> getRequiredCredentials() {
return requiredCredentials;
}
diff --git a/forms/src/main/java/org/keycloak/forms/TotpBean.java b/forms/src/main/java/org/keycloak/forms/TotpBean.java
index 3ca4ddb..b0a412b 100644
--- a/forms/src/main/java/org/keycloak/forms/TotpBean.java
+++ b/forms/src/main/java/org/keycloak/forms/TotpBean.java
@@ -28,6 +28,7 @@ import java.util.Random;
import javax.annotation.PostConstruct;
import javax.faces.application.FacesMessage;
import javax.faces.bean.ManagedBean;
+import javax.faces.bean.ManagedProperty;
import javax.faces.bean.RequestScoped;
import javax.faces.context.FacesContext;
@@ -40,12 +41,14 @@ import org.picketlink.common.util.Base32;
@RequestScoped
public class TotpBean {
+ @ManagedProperty(value = "#{user}")
+ private UserBean user;
+
private String totpSecret;
private String totpSecretEncoded;
@PostConstruct
public void init() {
-
FacesContext facesContext = FacesContext.getCurrentInstance();
FacesMessage facesMessage = new FacesMessage("This is a message");
facesContext.addMessage(null, facesMessage);
@@ -65,6 +68,10 @@ public class TotpBean {
return sb.toString();
}
+ public boolean isEnabled() {
+ return "ENABLED".equals(user.getUser().getAttribute("KEYCLOAK_TOTP"));
+ }
+
public String getTotpSecret() {
return totpSecret;
}
@@ -86,5 +93,13 @@ public class TotpBean {
return contextPath + "/forms/qrcode" + "?size=200x200&contents=" + contents;
}
+ public UserBean getUser() {
+ return user;
+ }
+
+ public void setUser(UserBean user) {
+ this.user = user;
+ }
+
}
diff --git a/forms/src/main/java/org/keycloak/forms/UserBean.java b/forms/src/main/java/org/keycloak/forms/UserBean.java
index b1b11bc..0be17b3 100644
--- a/forms/src/main/java/org/keycloak/forms/UserBean.java
+++ b/forms/src/main/java/org/keycloak/forms/UserBean.java
@@ -21,13 +21,14 @@
*/
package org.keycloak.forms;
-import java.util.Map;
-import java.util.Map.Entry;
-
import javax.annotation.PostConstruct;
import javax.faces.bean.ManagedBean;
import javax.faces.bean.RequestScoped;
import javax.faces.context.FacesContext;
+import javax.servlet.http.HttpServletRequest;
+
+import org.keycloak.services.models.UserModel;
+import org.keycloak.services.resources.flows.FormFlows;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
@@ -36,17 +37,34 @@ import javax.faces.context.FacesContext;
@RequestScoped
public class UserBean {
+ private UserModel user;
+
@PostConstruct
public void init() {
FacesContext ctx = FacesContext.getCurrentInstance();
- Map<String, Object> map = ctx.getExternalContext().getRequestCookieMap();
- for (Entry<String, Object> c : map.entrySet()) {
- System.out.println(c.getKey());
- }
+ HttpServletRequest request = (HttpServletRequest) ctx.getExternalContext().getRequest();
+
+ user = (UserModel) request.getAttribute(FormFlows.USER);
+ }
+
+ public String getFirstName() {
+ return user.getFirstName();
+ }
+
+ public String getLastName() {
+ return user.getLastName();
+ }
+
+ public String getUsername() {
+ return user.getLoginName();
+ }
+
+ public String getEmail() {
+ return user.getEmail();
}
- public boolean isLoggedIn() {
- return false;
+ UserModel getUser() {
+ return user;
}
}
diff --git a/forms/src/main/resources/META-INF/resources/forms/login-totp.xhtml b/forms/src/main/resources/META-INF/resources/forms/login-totp.xhtml
new file mode 100644
index 0000000..a757b27
--- /dev/null
+++ b/forms/src/main/resources/META-INF/resources/forms/login-totp.xhtml
@@ -0,0 +1,2 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<ui:include xmlns:ui="http://java.sun.com/jsf/facelets" src="theme/#{template.theme}/login-totp.xhtml" />
\ No newline at end of file
diff --git a/forms/src/main/resources/META-INF/resources/forms/theme/default/account.xhtml b/forms/src/main/resources/META-INF/resources/forms/theme/default/account.xhtml
index cbab6e8..07e0ca4 100755
--- a/forms/src/main/resources/META-INF/resources/forms/theme/default/account.xhtml
+++ b/forms/src/main/resources/META-INF/resources/forms/theme/default/account.xhtml
@@ -5,18 +5,22 @@
<ui:define name="header">Edit Account</ui:define>
<ui:define name="content">
- <form action="#" method="post">
+ <form action="#{url.accountUrl}" method="post">
<div>
- <label for="name">#{messages.fullName}</label>
- <input type="text" id="name" name="name" value="#{forms.formData['name']}" />
+ <label for="firstName">#{messages.firstName}</label>
+ <input type="text" id="firstName" name="firstName" value="#{user.firstName}" />
+ </div>
+ <div>
+ <label for="lastName">#{messages.lastName}</label>
+ <input type="text" id="lastName" name="lastName" value="#{user.lastName}" />
</div>
<div>
<label for="email">#{messages.email}</label>
- <input type="text" id="email" name="email" value="#{forms.formData['email']}" />
+ <input type="text" id="email" name="email" value="#{user.email}" />
</div>
<div>
<label for="username">#{messages.username}</label>
- <input type="text" id="username" name="username" value="#{forms.formData['username']}" />
+ <input type="text" id="username" name="username" value="#{user.username}" disabled="true" />
</div>
<input type="button" value="Cancel" />
diff --git a/forms/src/main/resources/META-INF/resources/forms/theme/default/login-totp.xhtml b/forms/src/main/resources/META-INF/resources/forms/theme/default/login-totp.xhtml
new file mode 100755
index 0000000..1deed99
--- /dev/null
+++ b/forms/src/main/resources/META-INF/resources/forms/theme/default/login-totp.xhtml
@@ -0,0 +1,31 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<ui:composition xmlns:h="http://java.sun.com/jsf/html" xmlns:f="http://java.sun.com/jsf/core" xmlns:ui="http://java.sun.com/jsf/facelets"
+ xmlns:c="http://java.sun.com/jstl/core" template="template-login.xhtml">
+
+ <ui:define name="header">Log in to <strong>#{realm.name}</strong></ui:define>
+
+ <ui:define name="form">
+ <form action="#{url.loginAction}" method="post">
+ <input id="username" name="username" value="#{login.username}" type="hidden" />
+ <input id="password" name="password" value="#{login.password}" type="hidden" />
+
+ <div>
+ <label for="totp">#{messages.authenticatorCode}</label>
+ <input id="totp" name="totp" type="text" />
+ </div>
+
+ <div class="aside-btn">
+ <!-- <input type="checkbox" id="remember" /><label for="remember">Remember Username</label> -->
+ <!-- <p>Forgot <a href="#">Username</a> or <a href="#">Password</a>?</p> -->
+ </div>
+
+ <input type="submit" value="Log In" />
+ </form>
+ </ui:define>
+
+ <ui:define name="info">
+ <h:panelGroup rendered="#{realm.registrationAllowed}">
+ <p>#{messages.noAccount} <a href="#{url.registrationUrl}">#{messages.register}</a>.</p>
+ </h:panelGroup>
+ </ui:define>
+</ui:composition>
\ No newline at end of file
diff --git a/forms/src/main/resources/META-INF/resources/forms/theme/default/password.xhtml b/forms/src/main/resources/META-INF/resources/forms/theme/default/password.xhtml
index f94a005..05384b1 100755
--- a/forms/src/main/resources/META-INF/resources/forms/theme/default/password.xhtml
+++ b/forms/src/main/resources/META-INF/resources/forms/theme/default/password.xhtml
@@ -5,14 +5,14 @@
<ui:define name="header">Change Password</ui:define>
<ui:define name="content">
- <form action="#" method="post">
+ <form action="#{url.passwordUrl}" method="post">
<div>
<label for="password">#{messages.password}</label>
<input type="password" id="password" name="password" />
</div>
<div>
- <label for="password">#{messages.passwordNew}</label>
- <input type="passwordNew" id="passwordNew" name="passwordNew" />
+ <label for="password-new">#{messages.passwordNew}</label>
+ <input type="password" id="password-new" name="password-new" />
</div>
<div>
<label for="password-confirm">#{messages.passwordConfirm}</label>
diff --git a/forms/src/main/resources/META-INF/resources/forms/theme/default/template-main.xhtml b/forms/src/main/resources/META-INF/resources/forms/theme/default/template-main.xhtml
index 09d546f..acbe73c 100644
--- a/forms/src/main/resources/META-INF/resources/forms/theme/default/template-main.xhtml
+++ b/forms/src/main/resources/META-INF/resources/forms/theme/default/template-main.xhtml
@@ -32,6 +32,10 @@ body {
<h1>
<ui:insert name="header" />
</h1>
+
+ <h:panelGroup rendered="#{not empty error.summary}">
+ <div class="alert alert-danger">#{messages[error.summary]}</div>
+ </h:panelGroup>
<ui:insert name="content" />
</div>
diff --git a/forms/src/main/resources/META-INF/resources/forms/theme/default/totp.xhtml b/forms/src/main/resources/META-INF/resources/forms/theme/default/totp.xhtml
index 790b173..c3b04d1 100755
--- a/forms/src/main/resources/META-INF/resources/forms/theme/default/totp.xhtml
+++ b/forms/src/main/resources/META-INF/resources/forms/theme/default/totp.xhtml
@@ -6,7 +6,11 @@
<ui:define name="content">
<h:messages globalOnly="true" />
-
+ <h:panelGroup rendered="#{totp.enabled}">
+ Google Authenticator enabled
+ </h:panelGroup>
+
+ <h:panelGroup rendered="#{not totp.enabled}">
<h2>To setup Google Authenticator</h2>
<ol>
@@ -16,11 +20,11 @@
</li>
<li>Enter a one-time password provided by Google Authenticator and click Save to finish the setup
- <form action="#" method="post">
+ <form action="#{url.totpUrl}" method="post">
<div>
<label for="totp">#{messages.authenticatorCode}</label>
<input type="text" id="totp" name="totp" />
- <input type="hidden" id="totpSecret" name="totpSecret" value="{forms.totpSecret}" />
+ <input type="hidden" id="totpSecret" name="totpSecret" value="#{totp.totpSecret}" />
</div>
<input type="button" value="Cancel" />
@@ -28,5 +32,6 @@
</form>
</li>
</ol>
+ </h:panelGroup>
</ui:define>
</ui:composition>
\ No newline at end of file
diff --git a/forms/src/main/resources/org/keycloak/forms/messages.properties b/forms/src/main/resources/org/keycloak/forms/messages.properties
index 7997683..50e21b6 100644
--- a/forms/src/main/resources/org/keycloak/forms/messages.properties
+++ b/forms/src/main/resources/org/keycloak/forms/messages.properties
@@ -11,6 +11,8 @@ poweredByKeycloak=Powered by Keycloak
username=Username
fullName=Full name
+firstName=First name
+lastName=Last name
email=Email
password=Password
passwordConfirm=Confirm Password
@@ -29,6 +31,7 @@ missingUsername=Please specify username
missingPassword=Please specify password
missingTotp=Please specify authenticator code
+invalidPasswordExisting=Invalid existing password
invalidPasswordConfirm=Password confirmation doesn't match
invalidTotp=Invalid authenticator code
diff --git a/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java b/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java
index 7ecc27a..728df77 100755
--- a/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java
+++ b/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java
@@ -211,6 +211,10 @@ public class AuthenticationManager {
requiredCredentials = realm.getRequiredOAuthClientCredentials();
} else {
requiredCredentials = realm.getRequiredCredentials();
+
+ if (!types.contains(CredentialRepresentation.TOTP) && "ENABLED".equals(user.getAttribute("KEYCLOAK_TOTP"))) {
+ types.add(CredentialRepresentation.TOTP);
+ }
}
for (RequiredCredentialModel credential : requiredCredentials) {
types.add(credential.getType());
diff --git a/services/src/main/java/org/keycloak/services/messages/Messages.java b/services/src/main/java/org/keycloak/services/messages/Messages.java
index 60ea232..c6165a9 100644
--- a/services/src/main/java/org/keycloak/services/messages/Messages.java
+++ b/services/src/main/java/org/keycloak/services/messages/Messages.java
@@ -30,6 +30,8 @@ public class Messages {
public static final String INVALID_PASSWORD = "invalidPassword";
+ public static final String INVALID_PASSWORD_EXISTING = "invalidPasswordExisting";
+
public static final String INVALID_PASSWORD_CONFIRM = "invalidPasswordConfirm";
public static final String INVALID_USER = "invalidUser";
diff --git a/services/src/main/java/org/keycloak/services/resources/AccountService.java b/services/src/main/java/org/keycloak/services/resources/AccountService.java
index 72862eb..30428af 100644
--- a/services/src/main/java/org/keycloak/services/resources/AccountService.java
+++ b/services/src/main/java/org/keycloak/services/resources/AccountService.java
@@ -21,14 +21,29 @@
*/
package org.keycloak.services.resources;
+import javax.ws.rs.Consumes;
import javax.ws.rs.GET;
+import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.core.Context;
+import javax.ws.rs.core.HttpHeaders;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response;
+import javax.ws.rs.core.UriInfo;
+import javax.ws.rs.core.Response.Status;
import org.jboss.resteasy.spi.HttpRequest;
+import org.keycloak.representations.idm.CredentialRepresentation;
+import org.keycloak.services.managers.AuthenticationManager;
+import org.keycloak.services.messages.Messages;
import org.keycloak.services.models.RealmModel;
+import org.keycloak.services.models.UserCredentialModel;
+import org.keycloak.services.models.UserModel;
import org.keycloak.services.resources.flows.Flows;
+import org.keycloak.services.resources.flows.FormFlows;
+import org.keycloak.services.validation.Validation;
+import org.picketlink.idm.credential.util.TimeBasedOTP;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
@@ -40,6 +55,14 @@ public class AccountService {
@Context
private HttpRequest request;
+ @Context
+ protected HttpHeaders headers;
+
+ @Context
+ private UriInfo uriInfo;
+
+ protected AuthenticationManager authManager = new AuthenticationManager();
+
public AccountService(RealmModel realm) {
this.realm = realm;
}
@@ -49,7 +72,122 @@ public class AccountService {
public Response accessPage() {
return new Transaction<Response>() {
protected Response callImpl() {
- return Flows.forms(realm, request).forwardToAccess();
+ UserModel user = authManager.authenticateIdentityCookie(realm, uriInfo, headers);
+ if (user != null) {
+ return Flows.forms(realm, request).setUser(user).forwardToAccess();
+ } else {
+ return Response.status(Status.FORBIDDEN).build();
+ }
+ }
+ }.call();
+ }
+
+ @Path("")
+ @POST
+ @Consumes(MediaType.APPLICATION_FORM_URLENCODED)
+ public Response processAccountUpdate(final MultivaluedMap<String, String> formData) {
+ return new Transaction<Response>() {
+ protected Response callImpl() {
+ UserModel user = authManager.authenticateIdentityCookie(realm, uriInfo, headers);
+ if (user != null) {
+ user.setFirstName(formData.getFirst("firstName"));
+ user.setLastName(formData.getFirst("lastName"));
+ user.setEmail(formData.getFirst("email"));
+
+ return Flows.forms(realm, request).setUser(user).forwardToAccount();
+ } else {
+ return Response.status(Status.FORBIDDEN).build();
+ }
+ }
+ }.call();
+ }
+
+ @Path("totp")
+ @POST
+ @Consumes(MediaType.APPLICATION_FORM_URLENCODED)
+ public Response processTotpUpdate(final MultivaluedMap<String, String> formData) {
+ return new Transaction<Response>() {
+ protected Response callImpl() {
+ UserModel user = authManager.authenticateIdentityCookie(realm, uriInfo, headers);
+ if (user != null) {
+ FormFlows forms = Flows.forms(realm, request);
+
+ String totp = formData.getFirst("totp");
+ String totpSecret = formData.getFirst("totpSecret");
+
+ String error = null;
+
+ if (Validation.isEmpty(totp)) {
+ error = Messages.MISSING_TOTP;
+ } else if (!new TimeBasedOTP().validate(totp, totpSecret.getBytes())) {
+ error = Messages.INVALID_TOTP;
+ }
+
+ if (error != null) {
+ return forms.setError(error).forwardToTotp();
+ }
+
+ UserCredentialModel credentials = new UserCredentialModel();
+ credentials.setType(CredentialRepresentation.TOTP);
+ credentials.setValue(formData.getFirst("totpSecret"));
+ realm.updateCredential(user, credentials);
+
+ if (!user.isEnabled() && "REQUIRED".equals(user.getAttribute("KEYCLOAK_TOTP"))) {
+ user.setEnabled(true);
+ }
+
+ user.setAttribute("KEYCLOAK_TOTP", "ENABLED");
+
+ return Flows.forms(realm, request).setUser(user).forwardToTotp();
+ } else {
+ return Response.status(Status.FORBIDDEN).build();
+ }
+ }
+ }.call();
+ }
+
+ @Path("password")
+ @POST
+ @Consumes(MediaType.APPLICATION_FORM_URLENCODED)
+ public Response processPasswordUpdate(final MultivaluedMap<String, String> formData) {
+ return new Transaction<Response>() {
+ protected Response callImpl() {
+ UserModel user = authManager.authenticateIdentityCookie(realm, uriInfo, headers);
+ if (user != null) {
+ FormFlows forms = Flows.forms(realm, request).setUser(user);
+
+ String password = formData.getFirst("password");
+ String passwordNew = formData.getFirst("password-new");
+ String passwordConfirm = formData.getFirst("password-confirm");
+
+ String error = null;
+
+ if (Validation.isEmpty(password)) {
+ error = Messages.MISSING_PASSWORD;
+ } else if (Validation.isEmpty(passwordNew)) {
+ error = Messages.MISSING_PASSWORD;
+ } else if (!passwordNew.equals(passwordConfirm)) {
+ error = Messages.INVALID_PASSWORD_CONFIRM;
+ } else if (!realm.validatePassword(user, password)) {
+ error = Messages.INVALID_PASSWORD_EXISTING;
+ }
+
+ if (error != null) {
+ return forms.setError(error).forwardToPassword();
+ }
+
+ UserCredentialModel credentials = new UserCredentialModel();
+ credentials.setType(CredentialRepresentation.PASSWORD);
+ credentials.setValue(passwordNew);
+
+ realm.updateCredential(user, credentials);
+
+ authManager.expireIdentityCookie(realm, uriInfo);
+
+ return Flows.forms(realm, request).setUser(user).forwardToPassword();
+ } else {
+ return Response.status(Status.FORBIDDEN).build();
+ }
}
}.call();
}
@@ -59,7 +197,12 @@ public class AccountService {
public Response accountPage() {
return new Transaction<Response>() {
protected Response callImpl() {
- return Flows.forms(realm, request).forwardToAccount();
+ UserModel user = authManager.authenticateIdentityCookie(realm, uriInfo, headers);
+ if (user != null) {
+ return Flows.forms(realm, request).setUser(user).forwardToAccount();
+ } else {
+ return Response.status(Status.FORBIDDEN).build();
+ }
}
}.call();
}
@@ -69,7 +212,12 @@ public class AccountService {
public Response socialPage() {
return new Transaction<Response>() {
protected Response callImpl() {
- return Flows.forms(realm, request).forwardToSocial();
+ UserModel user = authManager.authenticateIdentityCookie(realm, uriInfo, headers);
+ if (user != null) {
+ return Flows.forms(realm, request).setUser(user).forwardToSocial();
+ } else {
+ return Response.status(Status.FORBIDDEN).build();
+ }
}
}.call();
}
@@ -79,7 +227,12 @@ public class AccountService {
public Response totpPage() {
return new Transaction<Response>() {
protected Response callImpl() {
- return Flows.forms(realm, request).forwardToTotp();
+ UserModel user = authManager.authenticateIdentityCookie(realm, uriInfo, headers);
+ if (user != null) {
+ return Flows.forms(realm, request).setUser(user).forwardToTotp();
+ } else {
+ return Response.status(Status.FORBIDDEN).build();
+ }
}
}.call();
}
@@ -89,7 +242,12 @@ public class AccountService {
public Response passwordPage() {
return new Transaction<Response>() {
protected Response callImpl() {
- return Flows.forms(realm, request).forwardToPassword();
+ UserModel user = authManager.authenticateIdentityCookie(realm, uriInfo, headers);
+ if (user != null) {
+ return Flows.forms(realm, request).setUser(user).forwardToPassword();
+ } else {
+ return Response.status(Status.FORBIDDEN).build();
+ }
}
}.call();
}
diff --git a/services/src/main/java/org/keycloak/services/resources/flows/FormFlows.java b/services/src/main/java/org/keycloak/services/resources/flows/FormFlows.java
index 64817d8..3804b44 100644
--- a/services/src/main/java/org/keycloak/services/resources/flows/FormFlows.java
+++ b/services/src/main/java/org/keycloak/services/resources/flows/FormFlows.java
@@ -26,6 +26,7 @@ import javax.ws.rs.core.Response;
import org.jboss.resteasy.spi.HttpRequest;
import org.keycloak.services.models.RealmModel;
+import org.keycloak.services.models.UserModel;
import org.picketlink.idm.model.sample.Realm;
/**
@@ -36,6 +37,7 @@ public class FormFlows {
public static final String DATA = "KEYCLOAK_FORMS_DATA";
public static final String ERROR_MESSAGE = "KEYCLOAK_FORMS_ERROR_MESSAGE";
public static final String REALM = Realm.class.getName();
+ public static final String USER = UserModel.class.getName();
private String error;
private MultivaluedMap<String, String> formData;
@@ -43,6 +45,7 @@ public class FormFlows {
private RealmModel realm;
private HttpRequest request;
+ private UserModel userModel;
FormFlows(RealmModel realm, HttpRequest request) {
this.realm = realm;
@@ -68,6 +71,10 @@ public class FormFlows {
request.setAttribute(DATA, formData);
}
+ if (userModel != null) {
+ request.setAttribute(USER, userModel);
+ }
+
request.forward(form);
return null;
}
@@ -76,6 +83,10 @@ public class FormFlows {
return forwardToForm(Pages.LOGIN);
}
+ public Response forwardToLoginTotp() {
+ return forwardToForm(Pages.LOGIN_TOTP);
+ }
+
public Response forwardToPassword() {
return forwardToForm(Pages.PASSWORD);
}
@@ -97,6 +108,11 @@ public class FormFlows {
return this;
}
+ public FormFlows setUser(UserModel userModel) {
+ this.userModel = userModel;
+ return this;
+ }
+
public FormFlows setFormData(MultivaluedMap<String, String> formData) {
this.formData = formData;
return this;
diff --git a/services/src/main/java/org/keycloak/services/resources/flows/Pages.java b/services/src/main/java/org/keycloak/services/resources/flows/Pages.java
index e46a6f8..806ed63 100644
--- a/services/src/main/java/org/keycloak/services/resources/flows/Pages.java
+++ b/services/src/main/java/org/keycloak/services/resources/flows/Pages.java
@@ -32,6 +32,8 @@ public class Pages {
public final static String LOGIN = "/forms/login.xhtml";
+ public final static String LOGIN_TOTP = "/forms/login-totp.xhtml";
+
public final static String OAUTH_GRANT = "/saas/oauthGrantForm.jsp";
public final static String PASSWORD = "/forms/password.xhtml";
diff --git a/services/src/main/java/org/keycloak/services/resources/TokenService.java b/services/src/main/java/org/keycloak/services/resources/TokenService.java
index a257bf3..cb63744 100755
--- a/services/src/main/java/org/keycloak/services/resources/TokenService.java
+++ b/services/src/main/java/org/keycloak/services/resources/TokenService.java
@@ -212,9 +212,21 @@ public class TokenService {
return Flows.forms(realm, request).setError(Messages.INVALID_USER).setFormData(formData)
.forwardToLogin();
}
+
if (!user.isEnabled()) {
return oauth.forwardToSecurityFailure("Your account is not enabled.");
}
+
+ if ("ENABLED".equals(user.getAttribute("KEYCLOAK_TOTP")) && Validation.isEmpty(formData.getFirst("totp"))) {
+ return Flows.forms(realm, request).setFormData(formData).forwardToLoginTotp();
+ } else {
+ for (RequiredCredentialModel c : realm.getRequiredCredentials()) {
+ if (c.getType().equals(CredentialRepresentation.TOTP)) {
+ return Flows.forms(realm, request).forwardToTotp();
+ }
+ }
+ }
+
boolean authenticated = authManager.authenticateForm(realm, user, formData);
if (!authenticated) {
logger.error("Authentication failed");
@@ -308,13 +320,6 @@ public class TokenService {
realm.updateCredential(user, credentials);
}
- if (requiredCredentialTypes.contains(CredentialRepresentation.TOTP)) {
- UserCredentialModel credentials = new UserCredentialModel();
- credentials.setType(CredentialRepresentation.TOTP);
- credentials.setValue(formData.getFirst("totpSecret"));
- realm.updateCredential(user, credentials);
- }
-
for (RoleModel role : realm.getDefaultRoles()) {
realm.grantRole(user, role);
}
diff --git a/services/src/main/java/org/keycloak/services/validation/Validation.java b/services/src/main/java/org/keycloak/services/validation/Validation.java
index c54304c..f8d85bc 100644
--- a/services/src/main/java/org/keycloak/services/validation/Validation.java
+++ b/services/src/main/java/org/keycloak/services/validation/Validation.java
@@ -6,7 +6,6 @@ import javax.ws.rs.core.MultivaluedMap;
import org.keycloak.representations.idm.CredentialRepresentation;
import org.keycloak.services.messages.Messages;
-import org.picketlink.idm.credential.util.TimeBasedOTP;
public class Validation {
@@ -33,18 +32,6 @@ public class Validation {
}
}
- if (requiredCredentialTypes.contains(CredentialRepresentation.TOTP)) {
- if (isEmpty(formData.getFirst("totp"))) {
- return Messages.MISSING_TOTP;
- }
-
- boolean validTotp = new TimeBasedOTP().validate(formData.getFirst("totp"), formData.getFirst("totpSecret")
- .getBytes());
- if (!validTotp) {
- return Messages.INVALID_TOTP;
- }
- }
-
return null;
}