keycloak-memoizeit
Changes
forms/pom.xml 28(+23 -5)
forms/src/main/resources/META-INF/resources/forms/theme/default/css/img/customer-login-screen-bg.jpg 0(+0 -0)
forms/src/main/resources/META-INF/resources/forms/theme/default/css/img/customer-login-screen-bg.svg 0(+0 -0)
forms/src/main/resources/META-INF/resources/forms/theme/default/css/img/feedback-error-arrow-down.png 0(+0 -0)
forms/src/main/resources/META-INF/resources/forms/theme/default/css/img/feedback-error-arrow-down.svg 0(+0 -0)
forms/src/main/resources/META-INF/resources/forms/theme/default/css/img/feedback-error-sign.png 0(+0 -0)
forms/src/main/resources/META-INF/resources/forms/theme/default/css/img/feedback-error-sign.svg 0(+0 -0)
forms/src/main/resources/META-INF/resources/forms/theme/default/css/img/login-register-separator.svg 0(+0 -0)
forms/src/main/resources/META-INF/resources/forms/theme/default/css/img/login-register-separators.png 0(+0 -0)
forms/src/main/resources/META-INF/resources/forms/theme/default/css/img/login-register-social-separators.png 0(+0 -0)
forms/src/main/resources/META-INF/resources/forms/theme/default/css/img/login-register-social-separators.svg 0(+0 -0)
forms/src/main/resources/META-INF/resources/forms/theme/default/css/img/register-login-bg.png 0(+0 -0)
forms/src/main/resources/META-INF/resources/forms/theme/default/css/zocial/zocial-regular-webfont.eot 0(+0 -0)
forms/src/main/resources/META-INF/resources/forms/theme/default/css/zocial/zocial-regular-webfont.svg 0(+0 -0)
forms/src/main/resources/META-INF/resources/forms/theme/default/css/zocial/zocial-regular-webfont.ttf 0(+0 -0)
forms/src/main/resources/META-INF/resources/forms/theme/default/css/zocial/zocial-regular-webfont.woff 0(+0 -0)
forms/src/main/resources/META-INF/resources/forms/theme/default/img/customer-login-screen-bg2.jpg 0(+0 -0)
forms/src/main/resources/META-INF/resources/forms/theme/default/img/login-screen-background.jpg 0(+0 -0)
pom.xml 10(+10 -0)
Details
forms/pom.xml 28(+23 -5)
diff --git a/forms/pom.xml b/forms/pom.xml
index be51c9f..8338766 100755
--- a/forms/pom.xml
+++ b/forms/pom.xml
@@ -15,19 +15,29 @@
<dependencies>
<dependency>
<groupId>org.keycloak</groupId>
- <artifactId>keycloak-services</artifactId>
+ <artifactId>keycloak-core</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
- <artifactId>keycloak-social-core</artifactId>
+ <artifactId>keycloak-services</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
- <groupId>org.jboss.resteasy</groupId>
- <artifactId>jaxrs-api</artifactId>
- <scope>provided</scope>
+ <groupId>org.keycloak</groupId>
+ <artifactId>keycloak-social-core</artifactId>
+ <version>${project.version}</version>
</dependency>
+ <dependency>
+ <groupId>org.picketlink</groupId>
+ <artifactId>picketlink-common</artifactId>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.jboss.resteasy</groupId>
+ <artifactId>resteasy-jaxrs</artifactId>
+ <scope>provided</scope>
+ </dependency>
<dependency>
<groupId>org.jboss.spec.javax.servlet</groupId>
<artifactId>jboss-servlet-api_3.0_spec</artifactId>
@@ -38,6 +48,14 @@
<artifactId>jsf-api</artifactId>
<version>2.1</version>
</dependency>
+ <dependency>
+ <groupId>com.google.zxing</groupId>
+ <artifactId>core</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>com.google.zxing</groupId>
+ <artifactId>javase</artifactId>
+ </dependency>
</dependencies>
<build>
diff --git a/forms/src/main/java/org/keycloak/forms/FormsBean.java b/forms/src/main/java/org/keycloak/forms/FormsBean.java
index 5588658..6473dce 100755
--- a/forms/src/main/java/org/keycloak/forms/FormsBean.java
+++ b/forms/src/main/java/org/keycloak/forms/FormsBean.java
@@ -21,12 +21,15 @@
*/
package org.keycloak.forms;
+import java.io.UnsupportedEncodingException;
import java.net.URI;
+import java.net.URLEncoder;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
+import java.util.UUID;
import javax.annotation.PostConstruct;
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.UriBuilder;
+import org.keycloak.representations.idm.CredentialRepresentation;
import org.keycloak.services.models.RealmModel;
import org.keycloak.services.models.RequiredCredentialModel;
import org.keycloak.services.resources.flows.FormFlows;
import org.keycloak.services.resources.flows.Urls;
+import org.picketlink.common.util.Base32;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
@@ -81,6 +86,10 @@ public class FormsBean {
private Map<String, String> formData;
+ private String totpSecret;
+
+ private String formsUrl;
+
@PostConstruct
public void init() {
FacesContext ctx = FacesContext.getCurrentInstance();
@@ -99,7 +108,7 @@ public class FormsBean {
view = ctx.getViewRoot().getViewId();
view = view.substring(view.lastIndexOf('/') + 1, view.lastIndexOf('.'));
-
+
UriBuilder b = UriBuilder.fromUri(request.getRequestURI()).replaceQuery(request.getQueryString())
.replacePath(request.getContextPath()).path("rest");
URI baseURI = b.build();
@@ -125,9 +134,11 @@ public class FormsBean {
addSocialProviders();
addErrors(request);
+ formsUrl = FacesContext.getCurrentInstance().getExternalContext().getRequestContextPath() + "/forms";
+
// TODO Get theme name from realm
theme = "default";
- themeUrl = FacesContext.getCurrentInstance().getExternalContext().getRequestContextPath() + "/sdk/theme/" + theme;
+ themeUrl = formsUrl + "/theme/" + theme;
themeConfig = new HashMap<String, Object>();
@@ -220,10 +231,34 @@ public class FormsBean {
for (RequiredCredentialModel m : realm.getRequiredCredentials()) {
if (m.isInput()) {
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() {
// TODO Add providers configured for realm instead of all providers
providers = new LinkedList<SocialProvider>();
diff --git a/forms/src/main/java/org/keycloak/forms/QRServlet.java b/forms/src/main/java/org/keycloak/forms/QRServlet.java
new file mode 100644
index 0000000..56f0d05
--- /dev/null
+++ b/forms/src/main/java/org/keycloak/forms/QRServlet.java
@@ -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);
+ }
+ }
+
+}
diff --git a/forms/src/main/resources/org/keycloak/forms/messages.properties b/forms/src/main/resources/org/keycloak/forms/messages.properties
index 842091c..18c1ad5 100644
--- a/forms/src/main/resources/org/keycloak/forms/messages.properties
+++ b/forms/src/main/resources/org/keycloak/forms/messages.properties
@@ -26,7 +26,11 @@ missingName=Please specify full name
missingEmail=Please specify email
missingUsername=Please specify username
missingPassword=Please specify password
+missingTotp=Please specify authenticator code
invalidPasswordConfirm=Password confirmation doesn't match
+invalidTotp=Invalid authenticator code
-usernameExists=Username already exists
\ No newline at end of file
+usernameExists=Username already exists
+
+error=A system error has occured, contact admin
\ No newline at end of file
pom.xml 10(+10 -0)
diff --git a/pom.xml b/pom.xml
index b0e3b22..f937a7d 100755
--- a/pom.xml
+++ b/pom.xml
@@ -200,6 +200,16 @@
<artifactId>twitter4j-core</artifactId>
<version>3.0.3</version>
</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>
</dependencyManagement>
diff --git a/services/src/main/java/org/keycloak/services/messages/Messages.java b/services/src/main/java/org/keycloak/services/messages/Messages.java
index 0441cc2..60ea232 100644
--- a/services/src/main/java/org/keycloak/services/messages/Messages.java
+++ b/services/src/main/java/org/keycloak/services/messages/Messages.java
@@ -42,6 +42,12 @@ public class Messages {
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 ERROR = "error";
+
}
diff --git a/services/src/main/java/org/keycloak/services/resources/flows/Pages.java b/services/src/main/java/org/keycloak/services/resources/flows/Pages.java
index 1f06965..0fffe7f 100644
--- a/services/src/main/java/org/keycloak/services/resources/flows/Pages.java
+++ b/services/src/main/java/org/keycloak/services/resources/flows/Pages.java
@@ -26,11 +26,11 @@ package org.keycloak.services.resources.flows;
*/
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 REGISTER = "/sdk/register.xhtml";
+ public final static String REGISTER = "/forms/register.xhtml";
public final static String SECURITY_FAILURE = "/saas/securityFailure.jsp";
diff --git a/services/src/main/java/org/keycloak/services/resources/SaasService.java b/services/src/main/java/org/keycloak/services/resources/SaasService.java
index ea786cc..7b5a151 100755
--- a/services/src/main/java/org/keycloak/services/resources/SaasService.java
+++ b/services/src/main/java/org/keycloak/services/resources/SaasService.java
@@ -11,16 +11,20 @@ import org.keycloak.services.managers.AuthenticationManager;
import org.keycloak.services.managers.RealmManager;
import org.keycloak.services.messages.Messages;
import org.keycloak.services.models.RealmModel;
+import org.keycloak.services.models.RequiredCredentialModel;
import org.keycloak.services.models.RoleModel;
import org.keycloak.services.models.UserCredentialModel;
import org.keycloak.services.models.UserModel;
import org.keycloak.services.resources.admin.RealmsAdminResource;
import org.keycloak.services.resources.flows.Flows;
+import org.keycloak.services.validation.Validation;
import javax.ws.rs.*;
import javax.ws.rs.core.*;
import java.net.URI;
+import java.util.LinkedList;
+import java.util.List;
import java.util.StringTokenizer;
/**
@@ -308,7 +312,12 @@ public class SaasService {
RealmManager realmManager = new RealmManager(session);
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) {
return Flows.forms(defaultRealm, request).setError(error).setFormData(formData)
.forwardToRegistration();
@@ -341,7 +350,15 @@ public class SaasService {
newUser.setFirstName(first.toString());
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);
if (user == null) {
return Flows.forms(defaultRealm, request).setError(Messages.USERNAME_EXISTS)
@@ -384,32 +401,4 @@ public class SaasService {
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;
- }
-
}
diff --git a/services/src/main/java/org/keycloak/services/resources/TokenService.java b/services/src/main/java/org/keycloak/services/resources/TokenService.java
index 8c28310..a257bf3 100755
--- a/services/src/main/java/org/keycloak/services/resources/TokenService.java
+++ b/services/src/main/java/org/keycloak/services/resources/TokenService.java
@@ -19,11 +19,14 @@ import org.keycloak.services.managers.ResourceAdminManager;
import org.keycloak.services.managers.TokenManager;
import org.keycloak.services.messages.Messages;
import org.keycloak.services.models.RealmModel;
+import org.keycloak.services.models.RequiredCredentialModel;
import org.keycloak.services.models.RoleModel;
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.OAuthFlows;
+import org.keycloak.services.validation.Validation;
+import org.picketlink.idm.credential.util.TimeBasedOTP;
import javax.ws.rs.Consumes;
import javax.ws.rs.ForbiddenException;
@@ -46,6 +49,8 @@ import javax.ws.rs.ext.Providers;
import java.security.PrivateKey;
import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.List;
import java.util.Map;
import java.util.StringTokenizer;
@@ -250,7 +255,12 @@ public class TokenService {
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) {
return Flows.forms(realm, request).setError(error).setFormData(formData).forwardToRegistration();
}
@@ -291,10 +301,19 @@ public class TokenService {
user.setEmail(formData.getFirst("email"));
- UserCredentialModel credentials = new UserCredentialModel();
- credentials.setType(CredentialRepresentation.PASSWORD);
- credentials.setValue(formData.getFirst("password"));
- realm.updateCredential(user, credentials);
+ if (requiredCredentialTypes.contains(CredentialRepresentation.PASSWORD)) {
+ UserCredentialModel credentials = new UserCredentialModel();
+ credentials.setType(CredentialRepresentation.PASSWORD);
+ 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()) {
realm.grantRole(user, role);
@@ -577,32 +596,4 @@ public class TokenService {
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;
- }
-
}
diff --git a/services/src/main/java/org/keycloak/services/validation/Validation.java b/services/src/main/java/org/keycloak/services/validation/Validation.java
new file mode 100644
index 0000000..c54304c
--- /dev/null
+++ b/services/src/main/java/org/keycloak/services/validation/Validation.java
@@ -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;
+ }
+
+}