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,
|
"registrationAllowed": true,
|
||||||
"privateKey": "MIICXAIBAAKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQABAoGAfmO8gVhyBxdqlxmIuglbz8bcjQbhXJLR2EoS8ngTXmN1bo2L90M0mUKSdc7qF10LgETBzqL8jYlQIbt+e6TH8fcEpKCjUlyq0Mf/vVbfZSNaVycY13nTzo27iPyWQHK5NLuJzn1xvxxrUeXI6A2WFpGEBLbHjwpx5WQG9A+2scECQQDvdn9NE75HPTVPxBqsEd2z10TKkl9CZxu10Qby3iQQmWLEJ9LNmy3acvKrE3gMiYNWb6xHPKiIqOR1as7L24aTAkEAtyvQOlCvr5kAjVqrEKXalj0Tzewjweuxc0pskvArTI2Oo070h65GpoIKLc9jf+UA69cRtquwP93aZKtW06U8dQJAF2Y44ks/mK5+eyDqik3koCI08qaC8HYq2wVl7G2QkJ6sbAaILtcvD92ToOvyGyeE0flvmDZxMYlvaZnaQ0lcSQJBAKZU6umJi3/xeEbkJqMfeLclD27XGEFoPeNrmdx0q10Azp4NfJAY+Z8KRyQCR2BEG+oNitBOZ+YXF9KCpH3cdmECQHEigJhYg+ykOvr1aiZUMFT72HU0jnmQe2FVekuG+LJUt2Tm7GtMjTFoGpf0JwrVuZN39fOYAlo+nTixgeW7X8Y=",
|
"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",
|
"publicKey": "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB",
|
||||||
"requiredCredentials": [ "password" ],
|
"requiredCredentials": [ "password", "totp" ],
|
||||||
"requiredApplicationCredentials": [ "password" ],
|
"requiredApplicationCredentials": [ "password" ],
|
||||||
"requiredOAuthClientCredentials": [ "password" ],
|
"requiredOAuthClientCredentials": [ "password" ],
|
||||||
"defaultRoles": [ "user" ],
|
"defaultRoles": [ "user" ],
|
||||||
|
|
|
@ -47,6 +47,8 @@ public class LoginBean {
|
||||||
|
|
||||||
private String username;
|
private String username;
|
||||||
|
|
||||||
|
private String password;
|
||||||
|
|
||||||
private List<RequiredCredential> requiredCredentials;
|
private List<RequiredCredential> requiredCredentials;
|
||||||
|
|
||||||
@PostConstruct
|
@PostConstruct
|
||||||
|
@ -58,6 +60,7 @@ public class LoginBean {
|
||||||
MultivaluedMap<String, String> formData = (MultivaluedMap<String, String>) request.getAttribute(FormFlows.DATA);
|
MultivaluedMap<String, String> formData = (MultivaluedMap<String, String>) request.getAttribute(FormFlows.DATA);
|
||||||
if (formData != null) {
|
if (formData != null) {
|
||||||
username = formData.getFirst("username");
|
username = formData.getFirst("username");
|
||||||
|
password = formData.getFirst("password");
|
||||||
}
|
}
|
||||||
|
|
||||||
requiredCredentials = new LinkedList<RequiredCredential>();
|
requiredCredentials = new LinkedList<RequiredCredential>();
|
||||||
|
@ -72,6 +75,10 @@ public class LoginBean {
|
||||||
return username;
|
return username;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getPassword() {
|
||||||
|
return password;
|
||||||
|
}
|
||||||
|
|
||||||
public List<RequiredCredential> getRequiredCredentials() {
|
public List<RequiredCredential> getRequiredCredentials() {
|
||||||
return requiredCredentials;
|
return requiredCredentials;
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,6 +28,7 @@ import java.util.Random;
|
||||||
import javax.annotation.PostConstruct;
|
import javax.annotation.PostConstruct;
|
||||||
import javax.faces.application.FacesMessage;
|
import javax.faces.application.FacesMessage;
|
||||||
import javax.faces.bean.ManagedBean;
|
import javax.faces.bean.ManagedBean;
|
||||||
|
import javax.faces.bean.ManagedProperty;
|
||||||
import javax.faces.bean.RequestScoped;
|
import javax.faces.bean.RequestScoped;
|
||||||
import javax.faces.context.FacesContext;
|
import javax.faces.context.FacesContext;
|
||||||
|
|
||||||
|
@ -40,12 +41,14 @@ import org.picketlink.common.util.Base32;
|
||||||
@RequestScoped
|
@RequestScoped
|
||||||
public class TotpBean {
|
public class TotpBean {
|
||||||
|
|
||||||
|
@ManagedProperty(value = "#{user}")
|
||||||
|
private UserBean user;
|
||||||
|
|
||||||
private String totpSecret;
|
private String totpSecret;
|
||||||
private String totpSecretEncoded;
|
private String totpSecretEncoded;
|
||||||
|
|
||||||
@PostConstruct
|
@PostConstruct
|
||||||
public void init() {
|
public void init() {
|
||||||
|
|
||||||
FacesContext facesContext = FacesContext.getCurrentInstance();
|
FacesContext facesContext = FacesContext.getCurrentInstance();
|
||||||
FacesMessage facesMessage = new FacesMessage("This is a message");
|
FacesMessage facesMessage = new FacesMessage("This is a message");
|
||||||
facesContext.addMessage(null, facesMessage);
|
facesContext.addMessage(null, facesMessage);
|
||||||
|
@ -65,6 +68,10 @@ public class TotpBean {
|
||||||
return sb.toString();
|
return sb.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean isEnabled() {
|
||||||
|
return "ENABLED".equals(user.getUser().getAttribute("KEYCLOAK_TOTP"));
|
||||||
|
}
|
||||||
|
|
||||||
public String getTotpSecret() {
|
public String getTotpSecret() {
|
||||||
return totpSecret;
|
return totpSecret;
|
||||||
}
|
}
|
||||||
|
@ -86,5 +93,13 @@ public class TotpBean {
|
||||||
return contextPath + "/forms/qrcode" + "?size=200x200&contents=" + contents;
|
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;
|
package org.keycloak.forms;
|
||||||
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Map.Entry;
|
|
||||||
|
|
||||||
import javax.annotation.PostConstruct;
|
import javax.annotation.PostConstruct;
|
||||||
import javax.faces.bean.ManagedBean;
|
import javax.faces.bean.ManagedBean;
|
||||||
import javax.faces.bean.RequestScoped;
|
import javax.faces.bean.RequestScoped;
|
||||||
import javax.faces.context.FacesContext;
|
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>
|
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
||||||
|
@ -36,17 +37,34 @@ import javax.faces.context.FacesContext;
|
||||||
@RequestScoped
|
@RequestScoped
|
||||||
public class UserBean {
|
public class UserBean {
|
||||||
|
|
||||||
|
private UserModel user;
|
||||||
|
|
||||||
@PostConstruct
|
@PostConstruct
|
||||||
public void init() {
|
public void init() {
|
||||||
FacesContext ctx = FacesContext.getCurrentInstance();
|
FacesContext ctx = FacesContext.getCurrentInstance();
|
||||||
Map<String, Object> map = ctx.getExternalContext().getRequestCookieMap();
|
HttpServletRequest request = (HttpServletRequest) ctx.getExternalContext().getRequest();
|
||||||
for (Entry<String, Object> c : map.entrySet()) {
|
|
||||||
System.out.println(c.getKey());
|
user = (UserModel) request.getAttribute(FormFlows.USER);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isLoggedIn() {
|
public String getFirstName() {
|
||||||
return false;
|
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="header">Edit Account</ui:define>
|
||||||
|
|
||||||
<ui:define name="content">
|
<ui:define name="content">
|
||||||
<form action="#" method="post">
|
<form action="#{url.accountUrl}" method="post">
|
||||||
<div>
|
<div>
|
||||||
<label for="name">#{messages.fullName}</label>
|
<label for="firstName">#{messages.firstName}</label>
|
||||||
<input type="text" id="name" name="name" value="#{forms.formData['name']}" />
|
<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>
|
||||||
<div>
|
<div>
|
||||||
<label for="email">#{messages.email}</label>
|
<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>
|
||||||
<div>
|
<div>
|
||||||
<label for="username">#{messages.username}</label>
|
<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>
|
</div>
|
||||||
|
|
||||||
<input type="button" value="Cancel" />
|
<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="header">Change Password</ui:define>
|
||||||
|
|
||||||
<ui:define name="content">
|
<ui:define name="content">
|
||||||
<form action="#" method="post">
|
<form action="#{url.passwordUrl}" method="post">
|
||||||
<div>
|
<div>
|
||||||
<label for="password">#{messages.password}</label>
|
<label for="password">#{messages.password}</label>
|
||||||
<input type="password" id="password" name="password" />
|
<input type="password" id="password" name="password" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label for="password">#{messages.passwordNew}</label>
|
<label for="password-new">#{messages.passwordNew}</label>
|
||||||
<input type="passwordNew" id="passwordNew" name="passwordNew" />
|
<input type="password" id="password-new" name="password-new" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label for="password-confirm">#{messages.passwordConfirm}</label>
|
<label for="password-confirm">#{messages.passwordConfirm}</label>
|
||||||
|
|
|
@ -33,6 +33,10 @@ body {
|
||||||
<ui:insert name="header" />
|
<ui:insert name="header" />
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
|
<h:panelGroup rendered="#{not empty error.summary}">
|
||||||
|
<div class="alert alert-danger">#{messages[error.summary]}</div>
|
||||||
|
</h:panelGroup>
|
||||||
|
|
||||||
<ui:insert name="content" />
|
<ui:insert name="content" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -6,7 +6,11 @@
|
||||||
|
|
||||||
<ui:define name="content">
|
<ui:define name="content">
|
||||||
<h:messages globalOnly="true" />
|
<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>
|
<h2>To setup Google Authenticator</h2>
|
||||||
|
|
||||||
<ol>
|
<ol>
|
||||||
|
@ -16,11 +20,11 @@
|
||||||
</li>
|
</li>
|
||||||
<li>Enter a one-time password provided by Google Authenticator and click Save to finish the setup
|
<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>
|
<div>
|
||||||
<label for="totp">#{messages.authenticatorCode}</label>
|
<label for="totp">#{messages.authenticatorCode}</label>
|
||||||
<input type="text" id="totp" name="totp" />
|
<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>
|
</div>
|
||||||
|
|
||||||
<input type="button" value="Cancel" />
|
<input type="button" value="Cancel" />
|
||||||
|
@ -28,5 +32,6 @@
|
||||||
</form>
|
</form>
|
||||||
</li>
|
</li>
|
||||||
</ol>
|
</ol>
|
||||||
|
</h:panelGroup>
|
||||||
</ui:define>
|
</ui:define>
|
||||||
</ui:composition>
|
</ui:composition>
|
|
@ -11,6 +11,8 @@ poweredByKeycloak=Powered by Keycloak
|
||||||
|
|
||||||
username=Username
|
username=Username
|
||||||
fullName=Full name
|
fullName=Full name
|
||||||
|
firstName=First name
|
||||||
|
lastName=Last name
|
||||||
email=Email
|
email=Email
|
||||||
password=Password
|
password=Password
|
||||||
passwordConfirm=Confirm Password
|
passwordConfirm=Confirm Password
|
||||||
|
@ -29,6 +31,7 @@ missingUsername=Please specify username
|
||||||
missingPassword=Please specify password
|
missingPassword=Please specify password
|
||||||
missingTotp=Please specify authenticator code
|
missingTotp=Please specify authenticator code
|
||||||
|
|
||||||
|
invalidPasswordExisting=Invalid existing password
|
||||||
invalidPasswordConfirm=Password confirmation doesn't match
|
invalidPasswordConfirm=Password confirmation doesn't match
|
||||||
invalidTotp=Invalid authenticator code
|
invalidTotp=Invalid authenticator code
|
||||||
|
|
||||||
|
|
|
@ -211,6 +211,10 @@ public class AuthenticationManager {
|
||||||
requiredCredentials = realm.getRequiredOAuthClientCredentials();
|
requiredCredentials = realm.getRequiredOAuthClientCredentials();
|
||||||
} else {
|
} else {
|
||||||
requiredCredentials = realm.getRequiredCredentials();
|
requiredCredentials = realm.getRequiredCredentials();
|
||||||
|
|
||||||
|
if (!types.contains(CredentialRepresentation.TOTP) && "ENABLED".equals(user.getAttribute("KEYCLOAK_TOTP"))) {
|
||||||
|
types.add(CredentialRepresentation.TOTP);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
for (RequiredCredentialModel credential : requiredCredentials) {
|
for (RequiredCredentialModel credential : requiredCredentials) {
|
||||||
types.add(credential.getType());
|
types.add(credential.getType());
|
||||||
|
|
|
@ -30,6 +30,8 @@ public class Messages {
|
||||||
|
|
||||||
public static final String INVALID_PASSWORD = "invalidPassword";
|
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_PASSWORD_CONFIRM = "invalidPasswordConfirm";
|
||||||
|
|
||||||
public static final String INVALID_USER = "invalidUser";
|
public static final String INVALID_USER = "invalidUser";
|
||||||
|
|
|
@ -21,14 +21,29 @@
|
||||||
*/
|
*/
|
||||||
package org.keycloak.services.resources;
|
package org.keycloak.services.resources;
|
||||||
|
|
||||||
|
import javax.ws.rs.Consumes;
|
||||||
import javax.ws.rs.GET;
|
import javax.ws.rs.GET;
|
||||||
|
import javax.ws.rs.POST;
|
||||||
import javax.ws.rs.Path;
|
import javax.ws.rs.Path;
|
||||||
import javax.ws.rs.core.Context;
|
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.Response;
|
||||||
|
import javax.ws.rs.core.UriInfo;
|
||||||
|
import javax.ws.rs.core.Response.Status;
|
||||||
|
|
||||||
import org.jboss.resteasy.spi.HttpRequest;
|
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.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.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>
|
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
||||||
|
@ -40,6 +55,14 @@ public class AccountService {
|
||||||
@Context
|
@Context
|
||||||
private HttpRequest request;
|
private HttpRequest request;
|
||||||
|
|
||||||
|
@Context
|
||||||
|
protected HttpHeaders headers;
|
||||||
|
|
||||||
|
@Context
|
||||||
|
private UriInfo uriInfo;
|
||||||
|
|
||||||
|
protected AuthenticationManager authManager = new AuthenticationManager();
|
||||||
|
|
||||||
public AccountService(RealmModel realm) {
|
public AccountService(RealmModel realm) {
|
||||||
this.realm = realm;
|
this.realm = realm;
|
||||||
}
|
}
|
||||||
|
@ -49,7 +72,122 @@ public class AccountService {
|
||||||
public Response accessPage() {
|
public Response accessPage() {
|
||||||
return new Transaction<Response>() {
|
return new Transaction<Response>() {
|
||||||
protected Response callImpl() {
|
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();
|
}.call();
|
||||||
}
|
}
|
||||||
|
@ -59,7 +197,12 @@ public class AccountService {
|
||||||
public Response accountPage() {
|
public Response accountPage() {
|
||||||
return new Transaction<Response>() {
|
return new Transaction<Response>() {
|
||||||
protected Response callImpl() {
|
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();
|
}.call();
|
||||||
}
|
}
|
||||||
|
@ -69,7 +212,12 @@ public class AccountService {
|
||||||
public Response socialPage() {
|
public Response socialPage() {
|
||||||
return new Transaction<Response>() {
|
return new Transaction<Response>() {
|
||||||
protected Response callImpl() {
|
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();
|
}.call();
|
||||||
}
|
}
|
||||||
|
@ -79,7 +227,12 @@ public class AccountService {
|
||||||
public Response totpPage() {
|
public Response totpPage() {
|
||||||
return new Transaction<Response>() {
|
return new Transaction<Response>() {
|
||||||
protected Response callImpl() {
|
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();
|
}.call();
|
||||||
}
|
}
|
||||||
|
@ -89,7 +242,12 @@ public class AccountService {
|
||||||
public Response passwordPage() {
|
public Response passwordPage() {
|
||||||
return new Transaction<Response>() {
|
return new Transaction<Response>() {
|
||||||
protected Response callImpl() {
|
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();
|
}.call();
|
||||||
}
|
}
|
||||||
|
|
|
@ -212,9 +212,21 @@ public class TokenService {
|
||||||
return Flows.forms(realm, request).setError(Messages.INVALID_USER).setFormData(formData)
|
return Flows.forms(realm, request).setError(Messages.INVALID_USER).setFormData(formData)
|
||||||
.forwardToLogin();
|
.forwardToLogin();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!user.isEnabled()) {
|
if (!user.isEnabled()) {
|
||||||
return oauth.forwardToSecurityFailure("Your account is not enabled.");
|
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);
|
boolean authenticated = authManager.authenticateForm(realm, user, formData);
|
||||||
if (!authenticated) {
|
if (!authenticated) {
|
||||||
logger.error("Authentication failed");
|
logger.error("Authentication failed");
|
||||||
|
@ -308,13 +320,6 @@ public class TokenService {
|
||||||
realm.updateCredential(user, credentials);
|
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()) {
|
for (RoleModel role : realm.getDefaultRoles()) {
|
||||||
realm.grantRole(user, role);
|
realm.grantRole(user, role);
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,6 +26,7 @@ import javax.ws.rs.core.Response;
|
||||||
|
|
||||||
import org.jboss.resteasy.spi.HttpRequest;
|
import org.jboss.resteasy.spi.HttpRequest;
|
||||||
import org.keycloak.services.models.RealmModel;
|
import org.keycloak.services.models.RealmModel;
|
||||||
|
import org.keycloak.services.models.UserModel;
|
||||||
import org.picketlink.idm.model.sample.Realm;
|
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 DATA = "KEYCLOAK_FORMS_DATA";
|
||||||
public static final String ERROR_MESSAGE = "KEYCLOAK_FORMS_ERROR_MESSAGE";
|
public static final String ERROR_MESSAGE = "KEYCLOAK_FORMS_ERROR_MESSAGE";
|
||||||
public static final String REALM = Realm.class.getName();
|
public static final String REALM = Realm.class.getName();
|
||||||
|
public static final String USER = UserModel.class.getName();
|
||||||
|
|
||||||
private String error;
|
private String error;
|
||||||
private MultivaluedMap<String, String> formData;
|
private MultivaluedMap<String, String> formData;
|
||||||
|
@ -43,6 +45,7 @@ public class FormFlows {
|
||||||
private RealmModel realm;
|
private RealmModel realm;
|
||||||
|
|
||||||
private HttpRequest request;
|
private HttpRequest request;
|
||||||
|
private UserModel userModel;
|
||||||
|
|
||||||
FormFlows(RealmModel realm, HttpRequest request) {
|
FormFlows(RealmModel realm, HttpRequest request) {
|
||||||
this.realm = realm;
|
this.realm = realm;
|
||||||
|
@ -68,6 +71,10 @@ public class FormFlows {
|
||||||
request.setAttribute(DATA, formData);
|
request.setAttribute(DATA, formData);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (userModel != null) {
|
||||||
|
request.setAttribute(USER, userModel);
|
||||||
|
}
|
||||||
|
|
||||||
request.forward(form);
|
request.forward(form);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
@ -76,6 +83,10 @@ public class FormFlows {
|
||||||
return forwardToForm(Pages.LOGIN);
|
return forwardToForm(Pages.LOGIN);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Response forwardToLoginTotp() {
|
||||||
|
return forwardToForm(Pages.LOGIN_TOTP);
|
||||||
|
}
|
||||||
|
|
||||||
public Response forwardToPassword() {
|
public Response forwardToPassword() {
|
||||||
return forwardToForm(Pages.PASSWORD);
|
return forwardToForm(Pages.PASSWORD);
|
||||||
}
|
}
|
||||||
|
@ -97,6 +108,11 @@ public class FormFlows {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public FormFlows setUser(UserModel userModel) {
|
||||||
|
this.userModel = userModel;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
public FormFlows setFormData(MultivaluedMap<String, String> formData) {
|
public FormFlows setFormData(MultivaluedMap<String, String> formData) {
|
||||||
this.formData = formData;
|
this.formData = formData;
|
||||||
return this;
|
return this;
|
||||||
|
|
|
@ -32,6 +32,8 @@ public class Pages {
|
||||||
|
|
||||||
public final static String LOGIN = "/forms/login.xhtml";
|
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 OAUTH_GRANT = "/saas/oauthGrantForm.jsp";
|
||||||
|
|
||||||
public final static String PASSWORD = "/forms/password.xhtml";
|
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.representations.idm.CredentialRepresentation;
|
||||||
import org.keycloak.services.messages.Messages;
|
import org.keycloak.services.messages.Messages;
|
||||||
import org.picketlink.idm.credential.util.TimeBasedOTP;
|
|
||||||
|
|
||||||
public class Validation {
|
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;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue