KEYCLOAK-44
Add configuration of TOTP to registration
|
@ -13,6 +13,11 @@
|
||||||
<description />
|
<description />
|
||||||
|
|
||||||
<dependencies>
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.keycloak</groupId>
|
||||||
|
<artifactId>keycloak-core</artifactId>
|
||||||
|
<version>${project.version}</version>
|
||||||
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.keycloak</groupId>
|
<groupId>org.keycloak</groupId>
|
||||||
<artifactId>keycloak-services</artifactId>
|
<artifactId>keycloak-services</artifactId>
|
||||||
|
@ -23,11 +28,16 @@
|
||||||
<artifactId>keycloak-social-core</artifactId>
|
<artifactId>keycloak-social-core</artifactId>
|
||||||
<version>${project.version}</version>
|
<version>${project.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.jboss.resteasy</groupId>
|
<groupId>org.picketlink</groupId>
|
||||||
<artifactId>jaxrs-api</artifactId>
|
<artifactId>picketlink-common</artifactId>
|
||||||
<scope>provided</scope>
|
<scope>provided</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.jboss.resteasy</groupId>
|
||||||
|
<artifactId>resteasy-jaxrs</artifactId>
|
||||||
|
<scope>provided</scope>
|
||||||
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.jboss.spec.javax.servlet</groupId>
|
<groupId>org.jboss.spec.javax.servlet</groupId>
|
||||||
<artifactId>jboss-servlet-api_3.0_spec</artifactId>
|
<artifactId>jboss-servlet-api_3.0_spec</artifactId>
|
||||||
|
@ -38,6 +48,14 @@
|
||||||
<artifactId>jsf-api</artifactId>
|
<artifactId>jsf-api</artifactId>
|
||||||
<version>2.1</version>
|
<version>2.1</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.google.zxing</groupId>
|
||||||
|
<artifactId>core</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.google.zxing</groupId>
|
||||||
|
<artifactId>javase</artifactId>
|
||||||
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
<build>
|
<build>
|
||||||
|
|
|
@ -21,12 +21,15 @@
|
||||||
*/
|
*/
|
||||||
package org.keycloak.forms;
|
package org.keycloak.forms;
|
||||||
|
|
||||||
|
import java.io.UnsupportedEncodingException;
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
|
import java.net.URLEncoder;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Iterator;
|
import java.util.Iterator;
|
||||||
import java.util.LinkedList;
|
import java.util.LinkedList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
import javax.annotation.PostConstruct;
|
import javax.annotation.PostConstruct;
|
||||||
import javax.faces.bean.ManagedBean;
|
import javax.faces.bean.ManagedBean;
|
||||||
|
@ -37,10 +40,12 @@ import javax.servlet.http.HttpServletRequest;
|
||||||
import javax.ws.rs.core.MultivaluedMap;
|
import javax.ws.rs.core.MultivaluedMap;
|
||||||
import javax.ws.rs.core.UriBuilder;
|
import javax.ws.rs.core.UriBuilder;
|
||||||
|
|
||||||
|
import org.keycloak.representations.idm.CredentialRepresentation;
|
||||||
import org.keycloak.services.models.RealmModel;
|
import org.keycloak.services.models.RealmModel;
|
||||||
import org.keycloak.services.models.RequiredCredentialModel;
|
import org.keycloak.services.models.RequiredCredentialModel;
|
||||||
import org.keycloak.services.resources.flows.FormFlows;
|
import org.keycloak.services.resources.flows.FormFlows;
|
||||||
import org.keycloak.services.resources.flows.Urls;
|
import org.keycloak.services.resources.flows.Urls;
|
||||||
|
import org.picketlink.common.util.Base32;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
||||||
|
@ -81,6 +86,10 @@ public class FormsBean {
|
||||||
|
|
||||||
private Map<String, String> formData;
|
private Map<String, String> formData;
|
||||||
|
|
||||||
|
private String totpSecret;
|
||||||
|
|
||||||
|
private String formsUrl;
|
||||||
|
|
||||||
@PostConstruct
|
@PostConstruct
|
||||||
public void init() {
|
public void init() {
|
||||||
FacesContext ctx = FacesContext.getCurrentInstance();
|
FacesContext ctx = FacesContext.getCurrentInstance();
|
||||||
|
@ -99,7 +108,7 @@ public class FormsBean {
|
||||||
|
|
||||||
view = ctx.getViewRoot().getViewId();
|
view = ctx.getViewRoot().getViewId();
|
||||||
view = view.substring(view.lastIndexOf('/') + 1, view.lastIndexOf('.'));
|
view = view.substring(view.lastIndexOf('/') + 1, view.lastIndexOf('.'));
|
||||||
|
|
||||||
UriBuilder b = UriBuilder.fromUri(request.getRequestURI()).replaceQuery(request.getQueryString())
|
UriBuilder b = UriBuilder.fromUri(request.getRequestURI()).replaceQuery(request.getQueryString())
|
||||||
.replacePath(request.getContextPath()).path("rest");
|
.replacePath(request.getContextPath()).path("rest");
|
||||||
URI baseURI = b.build();
|
URI baseURI = b.build();
|
||||||
|
@ -125,9 +134,11 @@ public class FormsBean {
|
||||||
addSocialProviders();
|
addSocialProviders();
|
||||||
addErrors(request);
|
addErrors(request);
|
||||||
|
|
||||||
|
formsUrl = FacesContext.getCurrentInstance().getExternalContext().getRequestContextPath() + "/forms";
|
||||||
|
|
||||||
// TODO Get theme name from realm
|
// TODO Get theme name from realm
|
||||||
theme = "default";
|
theme = "default";
|
||||||
themeUrl = FacesContext.getCurrentInstance().getExternalContext().getRequestContextPath() + "/sdk/theme/" + theme;
|
themeUrl = formsUrl + "/theme/" + theme;
|
||||||
|
|
||||||
themeConfig = new HashMap<String, Object>();
|
themeConfig = new HashMap<String, Object>();
|
||||||
|
|
||||||
|
@ -220,10 +231,34 @@ public class FormsBean {
|
||||||
for (RequiredCredentialModel m : realm.getRequiredCredentials()) {
|
for (RequiredCredentialModel m : realm.getRequiredCredentials()) {
|
||||||
if (m.isInput()) {
|
if (m.isInput()) {
|
||||||
requiredCredentials.add(new RequiredCredential(m.getType(), m.isSecret(), m.getFormLabel()));
|
requiredCredentials.add(new RequiredCredential(m.getType(), m.isSecret(), m.getFormLabel()));
|
||||||
|
|
||||||
|
if (m.getType().equals(CredentialRepresentation.TOTP)) {
|
||||||
|
if (formData != null) {
|
||||||
|
totpSecret = formData.get("totpSecret");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (totpSecret == null) {
|
||||||
|
totpSecret = UUID.randomUUID().toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean isTotp() {
|
||||||
|
return totpSecret != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getTotpSecret() {
|
||||||
|
return totpSecret;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getTotpSecretQrCodeUrl() throws UnsupportedEncodingException {
|
||||||
|
String totpSecretEncoded = Base32.encode(getTotpSecret().getBytes());
|
||||||
|
String contents = URLEncoder.encode("otpauth://totp/keycloak?secret=" + totpSecretEncoded, "utf-8");
|
||||||
|
return formsUrl + "/qrcode" + "?size=200x200&contents=" + contents;
|
||||||
|
}
|
||||||
|
|
||||||
private void addSocialProviders() {
|
private void addSocialProviders() {
|
||||||
// TODO Add providers configured for realm instead of all providers
|
// TODO Add providers configured for realm instead of all providers
|
||||||
providers = new LinkedList<SocialProvider>();
|
providers = new LinkedList<SocialProvider>();
|
||||||
|
|
46
forms/src/main/java/org/keycloak/forms/QRServlet.java
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
package org.keycloak.forms;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
import javax.servlet.ServletException;
|
||||||
|
import javax.servlet.annotation.WebServlet;
|
||||||
|
import javax.servlet.http.HttpServlet;
|
||||||
|
import javax.servlet.http.HttpServletRequest;
|
||||||
|
import javax.servlet.http.HttpServletResponse;
|
||||||
|
|
||||||
|
import org.jboss.resteasy.logging.Logger;
|
||||||
|
|
||||||
|
import com.google.zxing.BarcodeFormat;
|
||||||
|
import com.google.zxing.client.j2se.MatrixToImageWriter;
|
||||||
|
import com.google.zxing.common.BitMatrix;
|
||||||
|
import com.google.zxing.qrcode.QRCodeWriter;
|
||||||
|
|
||||||
|
@WebServlet(urlPatterns = "/forms/qrcode")
|
||||||
|
public class QRServlet extends HttpServlet {
|
||||||
|
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
private static final Logger log = Logger.getLogger(QRServlet.class);
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
|
||||||
|
String[] size = req.getParameter("size").split("x");
|
||||||
|
int width = Integer.parseInt(size[0]);
|
||||||
|
int height = Integer.parseInt(size[1]);
|
||||||
|
|
||||||
|
String contents = req.getParameter("contents");
|
||||||
|
|
||||||
|
try {
|
||||||
|
QRCodeWriter writer = new QRCodeWriter();
|
||||||
|
|
||||||
|
BitMatrix bitMatrix = writer.encode(contents, BarcodeFormat.QR_CODE, width, height);
|
||||||
|
|
||||||
|
MatrixToImageWriter.writeToStream(bitMatrix, "png", resp.getOutputStream());
|
||||||
|
resp.setContentType("image/png");
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("Failed to generate qr code", e);
|
||||||
|
resp.sendError(500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
Before Width: | Height: | Size: 722 B After Width: | Height: | Size: 722 B |
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 46 KiB |
Before Width: | Height: | Size: 9.6 KiB After Width: | Height: | Size: 9.6 KiB |
Before Width: | Height: | Size: 513 B After Width: | Height: | Size: 513 B |
Before Width: | Height: | Size: 793 B After Width: | Height: | Size: 793 B |
Before Width: | Height: | Size: 343 B After Width: | Height: | Size: 343 B |
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
Before Width: | Height: | Size: 551 B After Width: | Height: | Size: 551 B |
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 1.9 KiB |
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
Before Width: | Height: | Size: 77 KiB After Width: | Height: | Size: 77 KiB |
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 37 KiB |
Before Width: | Height: | Size: 118 KiB After Width: | Height: | Size: 118 KiB |
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 2.9 KiB |
|
@ -29,6 +29,14 @@
|
||||||
<label for="password-confirm">#{messages.passwordConfirm}</label>
|
<label for="password-confirm">#{messages.passwordConfirm}</label>
|
||||||
<input type="password" id="password-confirm" name="password-confirm" />
|
<input type="password" id="password-confirm" name="password-confirm" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<h:panelGroup rendered="#{forms.totp}">
|
||||||
|
<div>
|
||||||
|
<label for="totp">#{messages.authenticatorCode}</label>
|
||||||
|
<input type="text" id="totp" name="totp" />
|
||||||
|
<input type="hidden" id="totpSecret" name="totpSecret" value="#{forms.totpSecret}" />
|
||||||
|
</div>
|
||||||
|
</h:panelGroup>
|
||||||
|
|
||||||
<div class="aside-btn">
|
<div class="aside-btn">
|
||||||
<p>By registering you agree to the <a href="#">Terms of Service</a> and the <a href="#">Privacy Policy</a>.</p>
|
<p>By registering you agree to the <a href="#">Terms of Service</a> and the <a href="#">Privacy Policy</a>.</p>
|
||||||
|
@ -40,5 +48,11 @@
|
||||||
|
|
||||||
<ui:define name="info">
|
<ui:define name="info">
|
||||||
<p>#{messages.alreadyHaveAccount} <a href="#{forms.loginUrl}">#{messages.logIn}</a>.</p>
|
<p>#{messages.alreadyHaveAccount} <a href="#{forms.loginUrl}">#{messages.logIn}</a>.</p>
|
||||||
|
|
||||||
|
<h:panelGroup rendered="#{not empty forms.totpSecret}">
|
||||||
|
<p>Google Authenticator setup<br/>
|
||||||
|
<img src="#{forms.totpSecretQrCodeUrl}" />
|
||||||
|
</p>
|
||||||
|
</h:panelGroup>
|
||||||
</ui:define>
|
</ui:define>
|
||||||
</ui:composition>
|
</ui:composition>
|
|
@ -29,7 +29,7 @@
|
||||||
<div class="feedback error bottom-left show">
|
<div class="feedback error bottom-left show">
|
||||||
<p>
|
<p>
|
||||||
<strong>#{forms.error}</strong><br/>
|
<strong>#{forms.error}</strong><br/>
|
||||||
#{forms.errorDetails}
|
#{messages[forms.errorDetails]}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</h:panelGroup>
|
</h:panelGroup>
|
|
@ -26,7 +26,11 @@ missingName=Please specify full name
|
||||||
missingEmail=Please specify email
|
missingEmail=Please specify email
|
||||||
missingUsername=Please specify username
|
missingUsername=Please specify username
|
||||||
missingPassword=Please specify password
|
missingPassword=Please specify password
|
||||||
|
missingTotp=Please specify authenticator code
|
||||||
|
|
||||||
invalidPasswordConfirm=Password confirmation doesn't match
|
invalidPasswordConfirm=Password confirmation doesn't match
|
||||||
|
invalidTotp=Invalid authenticator code
|
||||||
|
|
||||||
usernameExists=Username already exists
|
usernameExists=Username already exists
|
||||||
|
|
||||||
|
error=A system error has occured, contact admin
|
10
pom.xml
|
@ -200,6 +200,16 @@
|
||||||
<artifactId>twitter4j-core</artifactId>
|
<artifactId>twitter4j-core</artifactId>
|
||||||
<version>3.0.3</version>
|
<version>3.0.3</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.google.zxing</groupId>
|
||||||
|
<artifactId>core</artifactId>
|
||||||
|
<version>2.2</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.google.zxing</groupId>
|
||||||
|
<artifactId>javase</artifactId>
|
||||||
|
<version>2.2</version>
|
||||||
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
</dependencyManagement>
|
</dependencyManagement>
|
||||||
|
|
||||||
|
|
|
@ -42,6 +42,12 @@ public class Messages {
|
||||||
|
|
||||||
public static final String MISSING_USERNAME = "missingUsername";
|
public static final String MISSING_USERNAME = "missingUsername";
|
||||||
|
|
||||||
|
public static final String MISSING_TOTP = "missingTotp";
|
||||||
|
|
||||||
|
public static final String INVALID_TOTP = "invalidTotp";
|
||||||
|
|
||||||
public static final String USERNAME_EXISTS = "usernameExists";
|
public static final String USERNAME_EXISTS = "usernameExists";
|
||||||
|
|
||||||
|
public static final String ERROR = "error";
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,16 +11,20 @@ import org.keycloak.services.managers.AuthenticationManager;
|
||||||
import org.keycloak.services.managers.RealmManager;
|
import org.keycloak.services.managers.RealmManager;
|
||||||
import org.keycloak.services.messages.Messages;
|
import org.keycloak.services.messages.Messages;
|
||||||
import org.keycloak.services.models.RealmModel;
|
import org.keycloak.services.models.RealmModel;
|
||||||
|
import org.keycloak.services.models.RequiredCredentialModel;
|
||||||
import org.keycloak.services.models.RoleModel;
|
import org.keycloak.services.models.RoleModel;
|
||||||
import org.keycloak.services.models.UserCredentialModel;
|
import org.keycloak.services.models.UserCredentialModel;
|
||||||
import org.keycloak.services.models.UserModel;
|
import org.keycloak.services.models.UserModel;
|
||||||
import org.keycloak.services.resources.admin.RealmsAdminResource;
|
import org.keycloak.services.resources.admin.RealmsAdminResource;
|
||||||
import org.keycloak.services.resources.flows.Flows;
|
import org.keycloak.services.resources.flows.Flows;
|
||||||
|
import org.keycloak.services.validation.Validation;
|
||||||
|
|
||||||
import javax.ws.rs.*;
|
import javax.ws.rs.*;
|
||||||
import javax.ws.rs.core.*;
|
import javax.ws.rs.core.*;
|
||||||
|
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
|
import java.util.LinkedList;
|
||||||
|
import java.util.List;
|
||||||
import java.util.StringTokenizer;
|
import java.util.StringTokenizer;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -308,7 +312,12 @@ public class SaasService {
|
||||||
RealmManager realmManager = new RealmManager(session);
|
RealmManager realmManager = new RealmManager(session);
|
||||||
RealmModel defaultRealm = realmManager.defaultRealm();
|
RealmModel defaultRealm = realmManager.defaultRealm();
|
||||||
|
|
||||||
String error = validateRegistrationForm(formData);
|
List<String> requiredCredentialTypes = new LinkedList<String>();
|
||||||
|
for (RequiredCredentialModel m : defaultRealm.getRequiredCredentials()) {
|
||||||
|
requiredCredentialTypes.add(m.getType());
|
||||||
|
}
|
||||||
|
|
||||||
|
String error = Validation.validateRegistrationForm(formData, requiredCredentialTypes);
|
||||||
if (error != null) {
|
if (error != null) {
|
||||||
return Flows.forms(defaultRealm, request).setError(error).setFormData(formData)
|
return Flows.forms(defaultRealm, request).setError(error).setFormData(formData)
|
||||||
.forwardToRegistration();
|
.forwardToRegistration();
|
||||||
|
@ -341,7 +350,15 @@ public class SaasService {
|
||||||
newUser.setFirstName(first.toString());
|
newUser.setFirstName(first.toString());
|
||||||
newUser.setLastName(last);
|
newUser.setLastName(last);
|
||||||
}
|
}
|
||||||
newUser.credential(CredentialRepresentation.PASSWORD, formData.getFirst("password"));
|
|
||||||
|
if (requiredCredentialTypes.contains(CredentialRepresentation.PASSWORD)) {
|
||||||
|
newUser.credential(CredentialRepresentation.PASSWORD, formData.getFirst("password"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (requiredCredentialTypes.contains(CredentialRepresentation.TOTP)) {
|
||||||
|
newUser.credential(CredentialRepresentation.TOTP, formData.getFirst("password"));
|
||||||
|
}
|
||||||
|
|
||||||
UserModel user = registerMe(defaultRealm, newUser);
|
UserModel user = registerMe(defaultRealm, newUser);
|
||||||
if (user == null) {
|
if (user == null) {
|
||||||
return Flows.forms(defaultRealm, request).setError(Messages.USERNAME_EXISTS)
|
return Flows.forms(defaultRealm, request).setError(Messages.USERNAME_EXISTS)
|
||||||
|
@ -384,32 +401,4 @@ public class SaasService {
|
||||||
return user;
|
return user;
|
||||||
}
|
}
|
||||||
|
|
||||||
private String validateRegistrationForm(MultivaluedMap<String, String> formData) {
|
|
||||||
if (isEmpty(formData.getFirst("name"))) {
|
|
||||||
return Messages.MISSING_NAME;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isEmpty(formData.getFirst("email"))) {
|
|
||||||
return Messages.MISSING_EMAIL;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isEmpty(formData.getFirst("username"))) {
|
|
||||||
return Messages.MISSING_USERNAME;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isEmpty(formData.getFirst("password"))) {
|
|
||||||
return Messages.MISSING_PASSWORD;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!formData.getFirst("password").equals(formData.getFirst("password-confirm"))) {
|
|
||||||
return Messages.INVALID_PASSWORD_CONFIRM;
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isEmpty(String s) {
|
|
||||||
return s == null || s.length() == 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,11 +19,14 @@ import org.keycloak.services.managers.ResourceAdminManager;
|
||||||
import org.keycloak.services.managers.TokenManager;
|
import org.keycloak.services.managers.TokenManager;
|
||||||
import org.keycloak.services.messages.Messages;
|
import org.keycloak.services.messages.Messages;
|
||||||
import org.keycloak.services.models.RealmModel;
|
import org.keycloak.services.models.RealmModel;
|
||||||
|
import org.keycloak.services.models.RequiredCredentialModel;
|
||||||
import org.keycloak.services.models.RoleModel;
|
import org.keycloak.services.models.RoleModel;
|
||||||
import org.keycloak.services.models.UserCredentialModel;
|
import org.keycloak.services.models.UserCredentialModel;
|
||||||
import org.keycloak.services.models.UserModel;
|
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.OAuthFlows;
|
import org.keycloak.services.resources.flows.OAuthFlows;
|
||||||
|
import org.keycloak.services.validation.Validation;
|
||||||
|
import org.picketlink.idm.credential.util.TimeBasedOTP;
|
||||||
|
|
||||||
import javax.ws.rs.Consumes;
|
import javax.ws.rs.Consumes;
|
||||||
import javax.ws.rs.ForbiddenException;
|
import javax.ws.rs.ForbiddenException;
|
||||||
|
@ -46,6 +49,8 @@ import javax.ws.rs.ext.Providers;
|
||||||
|
|
||||||
import java.security.PrivateKey;
|
import java.security.PrivateKey;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
|
import java.util.LinkedList;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.StringTokenizer;
|
import java.util.StringTokenizer;
|
||||||
|
|
||||||
|
@ -250,7 +255,12 @@ public class TokenService {
|
||||||
return oauth.forwardToSecurityFailure("Registration not allowed");
|
return oauth.forwardToSecurityFailure("Registration not allowed");
|
||||||
}
|
}
|
||||||
|
|
||||||
String error = validateRegistrationForm(formData);
|
List<String> requiredCredentialTypes = new LinkedList<String>();
|
||||||
|
for (RequiredCredentialModel m : realm.getRequiredCredentials()) {
|
||||||
|
requiredCredentialTypes.add(m.getType());
|
||||||
|
}
|
||||||
|
|
||||||
|
String error = Validation.validateRegistrationForm(formData, requiredCredentialTypes);
|
||||||
if (error != null) {
|
if (error != null) {
|
||||||
return Flows.forms(realm, request).setError(error).setFormData(formData).forwardToRegistration();
|
return Flows.forms(realm, request).setError(error).setFormData(formData).forwardToRegistration();
|
||||||
}
|
}
|
||||||
|
@ -291,10 +301,19 @@ public class TokenService {
|
||||||
|
|
||||||
user.setEmail(formData.getFirst("email"));
|
user.setEmail(formData.getFirst("email"));
|
||||||
|
|
||||||
UserCredentialModel credentials = new UserCredentialModel();
|
if (requiredCredentialTypes.contains(CredentialRepresentation.PASSWORD)) {
|
||||||
credentials.setType(CredentialRepresentation.PASSWORD);
|
UserCredentialModel credentials = new UserCredentialModel();
|
||||||
credentials.setValue(formData.getFirst("password"));
|
credentials.setType(CredentialRepresentation.PASSWORD);
|
||||||
realm.updateCredential(user, credentials);
|
credentials.setValue(formData.getFirst("password"));
|
||||||
|
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);
|
||||||
|
@ -577,32 +596,4 @@ public class TokenService {
|
||||||
return location.build();
|
return location.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
private String validateRegistrationForm(MultivaluedMap<String, String> formData) {
|
|
||||||
if (isEmpty(formData.getFirst("name"))) {
|
|
||||||
return Messages.MISSING_NAME;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isEmpty(formData.getFirst("email"))) {
|
|
||||||
return Messages.MISSING_EMAIL;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isEmpty(formData.getFirst("username"))) {
|
|
||||||
return Messages.MISSING_USERNAME;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isEmpty(formData.getFirst("password"))) {
|
|
||||||
return Messages.MISSING_PASSWORD;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!formData.getFirst("password").equals(formData.getFirst("password-confirm"))) {
|
|
||||||
return Messages.INVALID_PASSWORD_CONFIRM;
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isEmpty(String s) {
|
|
||||||
return s == null || s.length() == 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,11 +26,11 @@ package org.keycloak.services.resources.flows;
|
||||||
*/
|
*/
|
||||||
public class Pages {
|
public class Pages {
|
||||||
|
|
||||||
public final static String LOGIN = "/sdk/login.xhtml";
|
public final static String LOGIN = "/forms/login.xhtml";
|
||||||
|
|
||||||
public final static String OAUTH_GRANT = "/saas/oauthGrantForm.jsp";
|
public final static String OAUTH_GRANT = "/saas/oauthGrantForm.jsp";
|
||||||
|
|
||||||
public final static String REGISTER = "/sdk/register.xhtml";
|
public final static String REGISTER = "/forms/register.xhtml";
|
||||||
|
|
||||||
public final static String SECURITY_FAILURE = "/saas/securityFailure.jsp";
|
public final static String SECURITY_FAILURE = "/saas/securityFailure.jsp";
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,55 @@
|
||||||
|
package org.keycloak.services.validation;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
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 {
|
||||||
|
|
||||||
|
public static String validateRegistrationForm(MultivaluedMap<String, String> formData, List<String> requiredCredentialTypes) {
|
||||||
|
if (isEmpty(formData.getFirst("name"))) {
|
||||||
|
return Messages.MISSING_NAME;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isEmpty(formData.getFirst("email"))) {
|
||||||
|
return Messages.MISSING_EMAIL;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isEmpty(formData.getFirst("username"))) {
|
||||||
|
return Messages.MISSING_USERNAME;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (requiredCredentialTypes.contains(CredentialRepresentation.PASSWORD)) {
|
||||||
|
if (isEmpty(formData.getFirst(CredentialRepresentation.PASSWORD))) {
|
||||||
|
return Messages.MISSING_PASSWORD;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!formData.getFirst("password").equals(formData.getFirst("password-confirm"))) {
|
||||||
|
return Messages.INVALID_PASSWORD_CONFIRM;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean isEmpty(String s) {
|
||||||
|
return s == null || s.length() == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|