Started adding totp flow
This commit is contained in:
parent
17b61ed0c8
commit
de7a185ee0
18 changed files with 310 additions and 47 deletions
|
@ -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" ],
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
|
|
@ -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 boolean isLoggedIn() {
|
||||
return false;
|
||||
public String getFirstName() {
|
||||
return user.getFirstName();
|
||||
}
|
||||
|
||||
public String getLastName() {
|
||||
return user.getLastName();
|
||||
}
|
||||
|
||||
public String getUsername() {
|
||||
return user.getLoginName();
|
||||
}
|
||||
|
||||
public String getEmail() {
|
||||
return user.getEmail();
|
||||
}
|
||||
|
||||
UserModel getUser() {
|
||||
return user;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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" />
|
|
@ -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" />
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
||||
|
|
|
@ -33,6 +33,10 @@ body {
|
|||
<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>
|
||||
|
||||
|
|
|
@ -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>
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue