keycloak-memoizeit

Changes

forms/pom.xml 28(+23 -5)

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