Started adding totp flow

This commit is contained in:
Stian Thorgersen 2013-08-20 17:15:10 +01:00
parent 17b61ed0c8
commit de7a185ee0
18 changed files with 310 additions and 47 deletions

View file

@ -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" ],

View file

@ -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;
}

View file

@ -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;
}
}

View file

@ -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;
}
}

View file

@ -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" />

View file

@ -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" />

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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

View file

@ -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());

View file

@ -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";

View file

@ -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();
}

View file

@ -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);
}

View file

@ -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;

View file

@ -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";

View file

@ -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;
}