You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@syncope.apache.org by sk...@apache.org on 2018/09/10 13:13:51 UTC

[4/4] syncope git commit: [SYNCOPE-1019] Added dynamic templating feature to Enduser app

[SYNCOPE-1019] Added dynamic templating feature to Enduser app


Project: http://git-wip-us.apache.org/repos/asf/syncope/repo
Commit: http://git-wip-us.apache.org/repos/asf/syncope/commit/19d9e93e
Tree: http://git-wip-us.apache.org/repos/asf/syncope/tree/19d9e93e
Diff: http://git-wip-us.apache.org/repos/asf/syncope/diff/19d9e93e

Branch: refs/heads/2_0_X
Commit: 19d9e93e0d9102aa8b138b8eca88050dc81667ef
Parents: f89b408
Author: skylark17 <ma...@tirasa.net>
Authored: Tue Aug 28 10:11:47 2018 +0200
Committer: skylark17 <ma...@tirasa.net>
Committed: Mon Sep 10 15:12:54 2018 +0200

----------------------------------------------------------------------
 .gitignore                                      |   1 +
 archetype/pom.xml                               |   7 +-
 .../archetype-resources/enduser/pom.xml         |   6 +-
 .../syncope/client/console/pages/BasePage.html  |   2 +-
 .../syncope/client/console/pages/Login.html     |   2 +-
 .../console/pages/MustChangePassword.html       |   2 +-
 client/enduser/pom.xml                          |   4 +-
 .../enduser/SyncopeEnduserApplication.java      | 139 +++++--
 .../enduser/adapters/PlatformInfoAdapter.java   |   4 +-
 .../enduser/model/CustomAttributesInfo.java     |  15 -
 .../client/enduser/model/CustomTemplate.java    |  77 ++++
 .../enduser/model/CustomTemplateInfo.java       |  72 ++++
 .../client/enduser/model/CustomTemplateUrl.java |  37 ++
 .../enduser/model/CustomTemplateWizard.java     |  49 +++
 .../enduser/model/PlatformInfoRequest.java      |  10 +-
 .../resources/DynamicTemplateResource.java      |  79 ++++
 .../resources/ExternalResourceResource.java     |  14 +-
 .../client/enduser/resources/GroupResource.java |  72 ++--
 .../client/enduser/resources/InfoResource.java  |   8 +-
 .../enduser/resources/SchemaResource.java       |  44 ++-
 .../resources/UserSelfCreateResource.java       |   3 +-
 .../enduser/resources/UserSelfReadResource.java |  16 +-
 .../resources/UserSelfUpdateResource.java       |   7 +-
 .../enduser/util/UserRequestValidator.java      |  29 +-
 .../META-INF/resources/app/css/app.css          | 107 +++---
 .../resources/app/css/customSpinner.css         |  49 +++
 .../META-INF/resources/app/css/editUser.css     | 369 +++++++++++--------
 .../META-INF/resources/app/css/login.css        |  41 +--
 .../META-INF/resources/app/css/notification.css |  28 ++
 .../resources/app/css/passwordReset.css         |  37 ++
 .../app/css/templates/dark/editUser.css         |  95 +++++
 .../resources/app/css/templates/dark/login.css  |  78 ++++
 .../resources/META-INF/resources/app/index.html |  27 +-
 .../resources/META-INF/resources/app/js/app.js  | 160 +++++---
 .../app/js/controllers/LoginController.js       |  11 +-
 .../app/js/controllers/OIDCClientController.js  |   4 +-
 .../app/js/controllers/SAML2SPController.js     |   4 +-
 .../app/js/controllers/UserController.js        |  42 ++-
 .../app/js/directives/dynamicPlainAttribute.js  |  10 +-
 .../app/js/directives/dynamicTemplateItem.js    |  79 ++++
 .../js/directives/dynamicVirtualAttribute.js    |  22 +-
 .../js/directives/dynamicVirtualAttributes.js   |   3 +-
 .../resources/app/js/directives/fileInput.js    |   4 +-
 .../app/js/directives/navigationButtons.js      |  71 ----
 .../js/directives/navigationButtonsPartial.js   |  71 ++++
 .../app/js/services/dynamicTemplateService.js   |  67 ++++
 .../resources/app/js/util/assetsManager.js      | 105 ++++++
 .../META-INF/resources/app/views/captcha.html   |  14 +-
 .../app/views/confirmpasswordreset.html         |  28 +-
 .../app/views/dynamicDerivedAttributes.html     |  19 +-
 .../app/views/dynamicPlainAttribute.html        | 139 ++++---
 .../app/views/dynamicPlainAttributes.html       |  14 +-
 .../app/views/dynamicVirtualAttributes.html     |  18 +-
 .../META-INF/resources/app/views/editUser.html  |  31 +-
 .../resources/app/views/mustchangepassword.html |  22 +-
 .../resources/app/views/navigationButtons.html  |  26 --
 .../app/views/navigationButtonsPartial.html     |  29 ++
 .../resources/app/views/passwordreset.html      |  55 +--
 .../META-INF/resources/app/views/self.html      |  60 +--
 .../app/views/templates/editUserTemplate.html   |  57 +++
 .../onlyPlainAttrsDetails/editUserTemplate.html |  57 +++
 .../views/templates/passwordresetTemplate.html  |  79 ++++
 .../app/views/templates/selfTemplate.html       |  77 ++++
 .../resources/app/views/user-credentials.html   |  62 ++--
 .../app/views/user-derived-schemas.html         |  33 +-
 .../resources/app/views/user-form-finish.html   |  29 +-
 .../resources/app/views/user-groups.html        |  47 ++-
 .../resources/app/views/user-plain-schemas.html |  35 +-
 .../resources/app/views/user-resources.html     |  37 +-
 .../app/views/user-virtual-schemas.html         |  33 +-
 .../enduser/src/main/resources/customForm.json  |   1 -
 .../main/resources/customFormAttributes.json    |   1 +
 .../src/main/resources/customTemplate.json      |  65 ++++
 .../enduser/util/UserRequestValidatorTest.java  |  39 +-
 .../enduser/src/test/resources/customForm.json  |  50 ---
 .../test/resources/customFormAttributes.json    |  44 +++
 .../src/test/resources/customTemplate.json      |  56 +++
 deb/enduser/pom.xml                             |   3 +-
 deb/enduser/src/deb/control/conffiles           |   3 +-
 fit/enduser-reference/pom.xml                   |  23 --
 .../src/main/resources/customForm.json          |   1 -
 .../main/resources/customFormAttributes.json    |   1 +
 .../src/main/resources/customTemplate.json      |  65 ++++
 .../src/main/resources/package.json             |   1 +
 .../src/test/resources/customForm.json          |   1 -
 .../test/resources/customFormAttributes.json    |   1 +
 .../src/test/resources/customTemplate.json      |  65 ++++
 .../src/test/resources/protractor-conf.js       |   8 +-
 .../src/test/resources/tests/abstract.js        |  11 +-
 .../src/test/resources/tests/edit.js            |   5 +
 .../src/test/resources/tests/passwordreset.js   |   1 +
 pom.xml                                         |  46 +--
 .../workingwithapachesyncope/customization.adoc | 210 ++++++++++-
 93 files changed, 2799 insertions(+), 1086 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/syncope/blob/19d9e93e/.gitignore
----------------------------------------------------------------------
diff --git a/.gitignore b/.gitignore
index 3388d02..d479649 100644
--- a/.gitignore
+++ b/.gitignore
@@ -14,3 +14,4 @@ ide/eclipse/bundles/org.apache.syncope.ide.eclipse.plugin/lib/
 ide/eclipse/bundles/org.apache.syncope.ide.eclipse.tests/bin/
 ide/eclipse/bundles/org.apache.syncope.ide.eclipse.tests/screenshots/
 *nb-configuration.xml
+node_modules

http://git-wip-us.apache.org/repos/asf/syncope/blob/19d9e93e/archetype/pom.xml
----------------------------------------------------------------------
diff --git a/archetype/pom.xml b/archetype/pom.xml
index 67dc7bd..94efe7e 100644
--- a/archetype/pom.xml
+++ b/archetype/pom.xml
@@ -274,7 +274,8 @@ under the License.
         <targetPath>${project.build.outputDirectory}/archetype-resources/enduser/src/main/resources</targetPath>
         <includes>
           <include>enduser.properties</include>
-          <include>customForm.json</include>
+          <include>customFormAttributes.json</include>
+          <include>customTemplate.json</include>
         </includes>
       </resource>
       <resource>
@@ -308,7 +309,7 @@ under the License.
         <includes>
           <include>enduser.properties</include>
           <include>saml2sp-agent.properties</include>
-          <include>customForm.json</include>
+          <include>customFormAttributes.json</include>
         </includes>
       </resource>
       <resource>
@@ -317,7 +318,7 @@ under the License.
         <includes>
           <include>enduser.properties</include>
           <include>oidcclient-agent.properties</include>
-          <include>customForm.json</include>
+          <include>customFormAttributes.json</include>
         </includes>
       </resource>
       <resource>

http://git-wip-us.apache.org/repos/asf/syncope/blob/19d9e93e/archetype/src/main/resources/archetype-resources/enduser/pom.xml
----------------------------------------------------------------------
diff --git a/archetype/src/main/resources/archetype-resources/enduser/pom.xml b/archetype/src/main/resources/archetype-resources/enduser/pom.xml
index 4e8826c..190dd89 100644
--- a/archetype/src/main/resources/archetype-resources/enduser/pom.xml
+++ b/archetype/src/main/resources/archetype-resources/enduser/pom.xml
@@ -247,7 +247,11 @@ under the License.
                           todir="${project.build.directory}/${project.build.finalName}/WEB-INF/classes" 
                           overwrite="true"/>
                     
-                    <copy file="${project.build.directory}/test-classes/customForm.json" 
+                    <copy file="${project.build.directory}/test-classes/customFormAttributes.json" 
+                          todir="${project.build.directory}/${project.build.finalName}/WEB-INF/classes" 
+                          overwrite="true"/>
+                    
+                    <copy file="${project.build.directory}/test-classes/customTemplate.json" 
                           todir="${project.build.directory}/${project.build.finalName}/WEB-INF/classes" 
                           overwrite="true"/>
                   </target>

http://git-wip-us.apache.org/repos/asf/syncope/blob/19d9e93e/client/console/src/main/resources/org/apache/syncope/client/console/pages/BasePage.html
----------------------------------------------------------------------
diff --git a/client/console/src/main/resources/org/apache/syncope/client/console/pages/BasePage.html b/client/console/src/main/resources/org/apache/syncope/client/console/pages/BasePage.html
index 46afc74..ebfcfeb 100644
--- a/client/console/src/main/resources/org/apache/syncope/client/console/pages/BasePage.html
+++ b/client/console/src/main/resources/org/apache/syncope/client/console/pages/BasePage.html
@@ -26,7 +26,7 @@ under the License.
 
     <link rel="shortcut icon" href="img/favicon.png" type="image/png"/>
 
-    <link href="webjars/font-awesome/${font-awesome.version}/css/font-awesome.min.css" rel="stylesheet" type="text/css"/>
+    <link href="webjars/font-awesome/${font-awesome.version}/css/${font-awesome.filename}" rel="stylesheet" type="text/css"/>
     <link href="webjars/ionicons/${ionicons.version}/css/ionicons.min.css" rel="stylesheet" type="text/css"/>
     <link href="css/fonts.css" rel="stylesheet" type="text/css"/>
 

http://git-wip-us.apache.org/repos/asf/syncope/blob/19d9e93e/client/console/src/main/resources/org/apache/syncope/client/console/pages/Login.html
----------------------------------------------------------------------
diff --git a/client/console/src/main/resources/org/apache/syncope/client/console/pages/Login.html b/client/console/src/main/resources/org/apache/syncope/client/console/pages/Login.html
index bb5cd3a..0340b83 100644
--- a/client/console/src/main/resources/org/apache/syncope/client/console/pages/Login.html
+++ b/client/console/src/main/resources/org/apache/syncope/client/console/pages/Login.html
@@ -27,7 +27,7 @@ under the License.
 
     <link rel="shortcut icon" href="img/favicon.png" type="image/png"/>
 
-    <link href="webjars/font-awesome/${font-awesome.version}/css/font-awesome.min.css" rel="stylesheet" type="text/css" />
+    <link href="webjars/font-awesome/${font-awesome.version}/css/${font-awesome.filename}" rel="stylesheet" type="text/css" />
     <link href="webjars/ionicons/${ionicons.version}/css/ionicons.min.css" rel="stylesheet" type="text/css" />
     <link href="css/fonts.css" rel="stylesheet" type="text/css"/>
 

http://git-wip-us.apache.org/repos/asf/syncope/blob/19d9e93e/client/console/src/main/resources/org/apache/syncope/client/console/pages/MustChangePassword.html
----------------------------------------------------------------------
diff --git a/client/console/src/main/resources/org/apache/syncope/client/console/pages/MustChangePassword.html b/client/console/src/main/resources/org/apache/syncope/client/console/pages/MustChangePassword.html
index 4aef153..037ea71 100644
--- a/client/console/src/main/resources/org/apache/syncope/client/console/pages/MustChangePassword.html
+++ b/client/console/src/main/resources/org/apache/syncope/client/console/pages/MustChangePassword.html
@@ -27,7 +27,7 @@ under the License.
 
     <link rel="shortcut icon" href="img/favicon.png" type="image/png"/>
 
-    <link href="webjars/font-awesome/${font-awesome.version}/css/font-awesome.min.css" rel="stylesheet" type="text/css" />
+    <link href="webjars/font-awesome/${font-awesome.version}/css/${font-awesome.filename}" rel="stylesheet" type="text/css" />
     <link href="webjars/ionicons/${ionicons.version}/css/ionicons.min.css" rel="stylesheet" type="text/css" />
     <link href="css/fonts.css" rel="stylesheet" type="text/css"/>
 

http://git-wip-us.apache.org/repos/asf/syncope/blob/19d9e93e/client/enduser/pom.xml
----------------------------------------------------------------------
diff --git a/client/enduser/pom.xml b/client/enduser/pom.xml
index 8da6bdd..78be32c 100644
--- a/client/enduser/pom.xml
+++ b/client/enduser/pom.xml
@@ -137,8 +137,8 @@ under the License.
       <artifactId>ionicons</artifactId>
     </dependency>
     <dependency>
-      <groupId>org.webjars</groupId>
-      <artifactId>angular-ui-bootstrap</artifactId>
+      <groupId>org.webjars.npm</groupId>
+      <artifactId>ui-bootstrap4</artifactId>
     </dependency>
     <dependency>
       <groupId>org.webjars</groupId>

http://git-wip-us.apache.org/repos/asf/syncope/blob/19d9e93e/client/enduser/src/main/java/org/apache/syncope/client/enduser/SyncopeEnduserApplication.java
----------------------------------------------------------------------
diff --git a/client/enduser/src/main/java/org/apache/syncope/client/enduser/SyncopeEnduserApplication.java b/client/enduser/src/main/java/org/apache/syncope/client/enduser/SyncopeEnduserApplication.java
index 21e9e5f..6a45204 100644
--- a/client/enduser/src/main/java/org/apache/syncope/client/enduser/SyncopeEnduserApplication.java
+++ b/client/enduser/src/main/java/org/apache/syncope/client/enduser/SyncopeEnduserApplication.java
@@ -40,6 +40,7 @@ import org.apache.syncope.client.enduser.annotations.Resource;
 import org.apache.syncope.client.enduser.init.ClassPathScanImplementationLookup;
 import org.apache.syncope.client.enduser.init.EnduserInitializer;
 import org.apache.syncope.client.enduser.model.CustomAttributesInfo;
+import org.apache.syncope.client.enduser.model.CustomTemplateInfo;
 import org.apache.syncope.client.enduser.resources.CaptchaResource;
 import org.apache.syncope.client.lib.SyncopeClientFactoryBean;
 import org.apache.syncope.common.lib.PropertyUtils;
@@ -65,7 +66,9 @@ public class SyncopeEnduserApplication extends WebApplication implements Seriali
 
     private static final String ENDUSER_PROPERTIES = "enduser.properties";
 
-    private static final String CUSTOM_FORM_FILE = "customForm.json";
+    private static final String CUSTOM_FORM_ATTRIBUTES_FILE = "customFormAttributes.json";
+
+    private static final String CUSTOM_TEMPLATE_FILE = "customTemplate.json";
 
     public static SyncopeEnduserApplication get() {
         return (SyncopeEnduserApplication) WebApplication.get();
@@ -87,7 +90,9 @@ public class SyncopeEnduserApplication extends WebApplication implements Seriali
 
     private SyncopeClientFactoryBean clientFactory;
 
-    private Map<String, CustomAttributesInfo> customForm;
+    private Map<String, CustomAttributesInfo> customFormAttributes;
+
+    private CustomTemplateInfo customTemplate;
 
     private static final ObjectMapper MAPPER = new ObjectMapper();
 
@@ -131,17 +136,20 @@ public class SyncopeEnduserApplication extends WebApplication implements Seriali
                 setContentType(SyncopeClientFactoryBean.ContentType.JSON).
                 setUseCompression(BooleanUtils.toBoolean(useGZIPCompression));
 
-        // read customForm.json
-        try (InputStream is = getClass().getResourceAsStream("/" + CUSTOM_FORM_FILE)) {
-            customForm = MAPPER.readValue(is,
+        // read customFormAttributes.json
+        File enduserDir;
+        try (InputStream is = getClass().getResourceAsStream("/" + CUSTOM_FORM_ATTRIBUTES_FILE)) {
+            customFormAttributes = MAPPER.readValue(is,
                     new TypeReference<HashMap<String, CustomAttributesInfo>>() {
             });
-            File enduserDir = new File(props.getProperty("enduser.directory"));
+            enduserDir = new File(props.getProperty("enduser.directory"));
             boolean existsEnduserDir = enduserDir.exists() && enduserDir.canRead() && enduserDir.isDirectory();
             if (existsEnduserDir) {
-                File customFormFile = FileUtils.getFile(enduserDir, CUSTOM_FORM_FILE);
-                if (customFormFile.exists() && customFormFile.canRead() && customFormFile.isFile()) {
-                    customForm = MAPPER.readValue(FileUtils.openInputStream(customFormFile),
+                File customFormAttributesFile = FileUtils.getFile(enduserDir, CUSTOM_FORM_ATTRIBUTES_FILE);
+                if (customFormAttributesFile.exists()
+                        && customFormAttributesFile.canRead()
+                        && customFormAttributesFile.isFile()) {
+                    customFormAttributes = MAPPER.readValue(FileUtils.openInputStream(customFormAttributesFile),
                             new TypeReference<HashMap<String, CustomAttributesInfo>>() {
                     });
                 }
@@ -151,15 +159,15 @@ public class SyncopeEnduserApplication extends WebApplication implements Seriali
 
                         @Override
                         public boolean accept(final File pathname) {
-                            return StringUtils.contains(pathname.getPath(), CUSTOM_FORM_FILE);
+                            return StringUtils.contains(pathname.getPath(), CUSTOM_FORM_ATTRIBUTES_FILE);
                         }
                     })
-                    : new FileAlterationObserver(getClass().getResource("/" + CUSTOM_FORM_FILE).getFile(),
+                    : new FileAlterationObserver(getClass().getResource("/" + CUSTOM_FORM_ATTRIBUTES_FILE).getFile(),
                             new FileFilter() {
 
                         @Override
                         public boolean accept(final File pathname) {
-                            return StringUtils.contains(pathname.getPath(), CUSTOM_FORM_FILE);
+                            return StringUtils.contains(pathname.getPath(), CUSTOM_FORM_ATTRIBUTES_FILE);
                         }
                     });
 
@@ -170,8 +178,9 @@ public class SyncopeEnduserApplication extends WebApplication implements Seriali
                 @Override
                 public void onFileChange(final File file) {
                     try {
-                        LOG.trace("{} has changed. Reloading form customization configuration.", CUSTOM_FORM_FILE);
-                        customForm = MAPPER.readValue(FileUtils.openInputStream(file),
+                        LOG.trace("{} has changed. Reloading form attributes customization configuration.",
+                                CUSTOM_FORM_ATTRIBUTES_FILE);
+                        customFormAttributes = MAPPER.readValue(FileUtils.openInputStream(file),
                                 new TypeReference<HashMap<String, CustomAttributesInfo>>() {
                         });
                     } catch (IOException e) {
@@ -182,8 +191,9 @@ public class SyncopeEnduserApplication extends WebApplication implements Seriali
                 @Override
                 public void onFileCreate(final File file) {
                     try {
-                        LOG.trace("{} has been created. Loading form customization configuration.", CUSTOM_FORM_FILE);
-                        customForm = MAPPER.readValue(FileUtils.openInputStream(file),
+                        LOG.trace("{} has been created. Loading form attributes customization configuration.",
+                                CUSTOM_FORM_ATTRIBUTES_FILE);
+                        customFormAttributes = MAPPER.readValue(FileUtils.openInputStream(file),
                                 new TypeReference<HashMap<String, CustomAttributesInfo>>() {
                         });
                     } catch (IOException e) {
@@ -193,8 +203,81 @@ public class SyncopeEnduserApplication extends WebApplication implements Seriali
 
                 @Override
                 public void onFileDelete(final File file) {
-                    LOG.trace("{} has been deleted. Resetting form customization configuration.", CUSTOM_FORM_FILE);
-                    customForm = null;
+                    LOG.trace("{} has been deleted. Resetting form attributes customization configuration.",
+                            CUSTOM_FORM_ATTRIBUTES_FILE);
+                    customFormAttributes = null;
+                }
+            };
+
+            observer.addListener(listener);
+            monitor.addObserver(observer);
+            monitor.start();
+        } catch (Exception e) {
+            throw new WicketRuntimeException("Could not read " + CUSTOM_FORM_ATTRIBUTES_FILE, e);
+        }
+
+        // read customTemplate.json
+        try (InputStream is = getClass().getResourceAsStream("/" + CUSTOM_TEMPLATE_FILE)) {
+            customTemplate = MAPPER.readValue(is, CustomTemplateInfo.class);
+            if (enduserDir == null) {
+                enduserDir = new File(props.getProperty("enduser.directory"));
+            }
+            boolean existsEnduserDir = enduserDir.exists() && enduserDir.canRead() && enduserDir.isDirectory();
+            if (existsEnduserDir) {
+                File customTemplateFile = FileUtils.getFile(enduserDir, CUSTOM_TEMPLATE_FILE);
+                if (customTemplateFile.exists()
+                        && customTemplateFile.canRead()
+                        && customTemplateFile.isFile()) {
+                    customTemplate = MAPPER.readValue(FileUtils.openInputStream(customTemplateFile),
+                            CustomTemplateInfo.class);
+                }
+            }
+            FileAlterationObserver observer = existsEnduserDir
+                    ? new FileAlterationObserver(enduserDir, new FileFilter() {
+
+                        @Override
+                        public boolean accept(final File pathname) {
+                            return StringUtils.contains(pathname.getPath(), CUSTOM_TEMPLATE_FILE);
+                        }
+                    })
+                    : new FileAlterationObserver(getClass().getResource("/" + CUSTOM_TEMPLATE_FILE).getFile(),
+                            new FileFilter() {
+
+                        @Override
+                        public boolean accept(final File pathname) {
+                            return StringUtils.contains(pathname.getPath(), CUSTOM_TEMPLATE_FILE);
+                        }
+                    });
+
+            FileAlterationMonitor monitor = new FileAlterationMonitor(5000);
+
+            FileAlterationListener listener = new FileAlterationListenerAdaptor() {
+
+                @Override
+                public void onFileChange(final File file) {
+                    try {
+                        LOG.trace("{} has changed. Reloading app customization configuration.", CUSTOM_TEMPLATE_FILE);
+                        customTemplate = MAPPER.readValue(FileUtils.openInputStream(file), CustomTemplateInfo.class);
+                    } catch (IOException e) {
+                        e.printStackTrace(System.err);
+                    }
+                }
+
+                @Override
+                public void onFileCreate(final File file) {
+                    try {
+                        LOG.trace("{} has been created. Loading app customization configuration.",
+                                CUSTOM_TEMPLATE_FILE);
+                        customTemplate = MAPPER.readValue(FileUtils.openInputStream(file), CustomTemplateInfo.class);
+                    } catch (IOException e) {
+                        e.printStackTrace(System.err);
+                    }
+                }
+
+                @Override
+                public void onFileDelete(final File file) {
+                    LOG.trace("{} has been deleted. Resetting app customization configuration.", CUSTOM_TEMPLATE_FILE);
+                    customTemplate = null;
                 }
             };
 
@@ -202,7 +285,7 @@ public class SyncopeEnduserApplication extends WebApplication implements Seriali
             monitor.addObserver(observer);
             monitor.start();
         } catch (Exception e) {
-            throw new WicketRuntimeException("Could not read " + CUSTOM_FORM_FILE, e);
+            throw new WicketRuntimeException("Could not read " + CUSTOM_TEMPLATE_FILE, e);
         }
 
         // mount resources
@@ -287,13 +370,21 @@ public class SyncopeEnduserApplication extends WebApplication implements Seriali
         return maxUploadFileSizeMB;
     }
 
-    public Map<String, CustomAttributesInfo> getCustomForm() {
-        return customForm;
+    public Map<String, CustomAttributesInfo> getCustomFormAttributes() {
+        return customFormAttributes;
+    }
+
+    public void setCustomFormAttributes(final Map<String, CustomAttributesInfo> customFormAttributes) {
+        this.customFormAttributes.clear();
+        this.customFormAttributes.putAll(customFormAttributes);
+    }
+
+    public void setCustomTemplate(final CustomTemplateInfo customTemplate) {
+        this.customTemplate = customTemplate;
     }
 
-    public void setCustomForm(final Map<String, CustomAttributesInfo> customForm) {
-        this.customForm.clear();
-        this.customForm.putAll(customForm);
+    public CustomTemplateInfo getCustomTemplate() {
+        return customTemplate;
     }
 
 }

http://git-wip-us.apache.org/repos/asf/syncope/blob/19d9e93e/client/enduser/src/main/java/org/apache/syncope/client/enduser/adapters/PlatformInfoAdapter.java
----------------------------------------------------------------------
diff --git a/client/enduser/src/main/java/org/apache/syncope/client/enduser/adapters/PlatformInfoAdapter.java b/client/enduser/src/main/java/org/apache/syncope/client/enduser/adapters/PlatformInfoAdapter.java
index a9fcf3c..e194d6b 100644
--- a/client/enduser/src/main/java/org/apache/syncope/client/enduser/adapters/PlatformInfoAdapter.java
+++ b/client/enduser/src/main/java/org/apache/syncope/client/enduser/adapters/PlatformInfoAdapter.java
@@ -27,7 +27,7 @@ import org.apache.syncope.common.lib.info.PlatformInfo;
 public final class PlatformInfoAdapter {
 
     public static PlatformInfoRequest toPlatformInfoRequest(final PlatformInfo platformInfo,
-            final Map<String, CustomAttributesInfo> customForm) {
+            final Map<String, CustomAttributesInfo> customFormAttributes) {
         PlatformInfoRequest request = new PlatformInfoRequest();
         request.setPwdResetAllowed(platformInfo.isPwdResetAllowed());
         request.setSelfRegAllowed(platformInfo.isSelfRegAllowed());
@@ -37,7 +37,7 @@ public final class PlatformInfoAdapter {
         if (SyncopeEnduserApplication.get().getMaxUploadFileSizeMB() != null) {
             request.setMaxUploadFileSizeMB(SyncopeEnduserApplication.get().getMaxUploadFileSizeMB());
         }
-        request.setCustomForm(customForm);
+        request.setCustomFormAttributes(customFormAttributes);
 
         return request;
     }

http://git-wip-us.apache.org/repos/asf/syncope/blob/19d9e93e/client/enduser/src/main/java/org/apache/syncope/client/enduser/model/CustomAttributesInfo.java
----------------------------------------------------------------------
diff --git a/client/enduser/src/main/java/org/apache/syncope/client/enduser/model/CustomAttributesInfo.java b/client/enduser/src/main/java/org/apache/syncope/client/enduser/model/CustomAttributesInfo.java
index 63eaa0b..c49253c 100644
--- a/client/enduser/src/main/java/org/apache/syncope/client/enduser/model/CustomAttributesInfo.java
+++ b/client/enduser/src/main/java/org/apache/syncope/client/enduser/model/CustomAttributesInfo.java
@@ -26,21 +26,11 @@ public class CustomAttributesInfo implements Serializable {
 
     private static final long serialVersionUID = 878444785696091916L;
 
-    private Boolean show = Boolean.TRUE;
-
     private Map<String, CustomAttribute> attributes = new LinkedHashMap<>();
 
     public CustomAttributesInfo() {
     }
 
-    public Boolean isShow() {
-        return show;
-    }
-
-    public void setShow(final Boolean show) {
-        this.show = show;
-    }
-
     public Map<String, CustomAttribute> getAttributes() {
         return attributes;
     }
@@ -49,11 +39,6 @@ public class CustomAttributesInfo implements Serializable {
         this.attributes = attributes;
     }
 
-    public CustomAttributesInfo show(final Boolean value) {
-        this.show = value;
-        return this;
-    }
-
     public CustomAttributesInfo attributes(final Map<String, CustomAttribute> value) {
         this.attributes = value;
         return this;

http://git-wip-us.apache.org/repos/asf/syncope/blob/19d9e93e/client/enduser/src/main/java/org/apache/syncope/client/enduser/model/CustomTemplate.java
----------------------------------------------------------------------
diff --git a/client/enduser/src/main/java/org/apache/syncope/client/enduser/model/CustomTemplate.java b/client/enduser/src/main/java/org/apache/syncope/client/enduser/model/CustomTemplate.java
new file mode 100644
index 0000000..19d4a5b
--- /dev/null
+++ b/client/enduser/src/main/java/org/apache/syncope/client/enduser/model/CustomTemplate.java
@@ -0,0 +1,77 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.syncope.client.enduser.model;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.List;
+
+public class CustomTemplate implements Serializable {
+
+    private static final long serialVersionUID = -3870675034923683299L;
+
+    private String templateUrl;
+
+    private List<String> css = new ArrayList<>();
+
+    private List<String> js = new ArrayList<>();
+
+    public CustomTemplate() {
+    }
+
+    public String getTemplateUrl() {
+        return templateUrl;
+    }
+
+    public void setTemplateUrl(final String templateUrl) {
+        this.templateUrl = templateUrl;
+    }
+
+    public List<String> getCss() {
+        return css;
+    }
+
+    public void setCss(final List<String> css) {
+        this.css = css;
+    }
+
+    public List<String> getJs() {
+        return js;
+    }
+
+    public void setJs(final List<String> js) {
+        this.js = js;
+    }
+
+    public CustomTemplate templateUrl(final String value) {
+        this.templateUrl = value;
+        return this;
+    }
+
+    public CustomTemplate css(final List<String> value) {
+        this.css = value;
+        return this;
+    }
+
+    public CustomTemplate js(final List<String> value) {
+        this.js = value;
+        return this;
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/syncope/blob/19d9e93e/client/enduser/src/main/java/org/apache/syncope/client/enduser/model/CustomTemplateInfo.java
----------------------------------------------------------------------
diff --git a/client/enduser/src/main/java/org/apache/syncope/client/enduser/model/CustomTemplateInfo.java b/client/enduser/src/main/java/org/apache/syncope/client/enduser/model/CustomTemplateInfo.java
new file mode 100644
index 0000000..d2fe9c1
--- /dev/null
+++ b/client/enduser/src/main/java/org/apache/syncope/client/enduser/model/CustomTemplateInfo.java
@@ -0,0 +1,72 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.syncope.client.enduser.model;
+
+import java.io.Serializable;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+public class CustomTemplateInfo implements Serializable {
+
+    private static final long serialVersionUID = -3422125754029851539L;
+
+    private Map<String, CustomTemplate> templates = new LinkedHashMap<>();
+
+    private CustomTemplateWizard wizard = new CustomTemplateWizard();
+
+    private Map<String, List<String>> generalAssets = new LinkedHashMap<>();
+
+    public CustomTemplateInfo() {
+    }
+
+    public Map<String, CustomTemplate> getTemplates() {
+        return templates;
+    }
+
+    public void setTemplates(final Map<String, CustomTemplate> templates) {
+        this.templates = templates;
+    }
+
+    public CustomTemplateWizard getWizard() {
+        return wizard;
+    }
+
+    public void setWizard(final CustomTemplateWizard wizard) {
+        this.wizard = wizard;
+    }
+
+    public Map<String, List<String>> getGeneralAssets() {
+        return generalAssets;
+    }
+
+    public void setGeneralAssets(final Map<String, List<String>> generalAssets) {
+        this.generalAssets = generalAssets;
+    }
+
+    public CustomTemplateInfo templates(final Map<String, CustomTemplate> templates,
+            final CustomTemplateWizard wizard, final Map<String, List<String>> generalAssets) {
+
+        this.templates = templates;
+        this.wizard = wizard;
+        this.generalAssets = generalAssets;
+        return this;
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/syncope/blob/19d9e93e/client/enduser/src/main/java/org/apache/syncope/client/enduser/model/CustomTemplateUrl.java
----------------------------------------------------------------------
diff --git a/client/enduser/src/main/java/org/apache/syncope/client/enduser/model/CustomTemplateUrl.java b/client/enduser/src/main/java/org/apache/syncope/client/enduser/model/CustomTemplateUrl.java
new file mode 100644
index 0000000..40bde34
--- /dev/null
+++ b/client/enduser/src/main/java/org/apache/syncope/client/enduser/model/CustomTemplateUrl.java
@@ -0,0 +1,37 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.syncope.client.enduser.model;
+
+import java.io.Serializable;
+
+public class CustomTemplateUrl implements Serializable {
+
+    private static final long serialVersionUID = 3971593691907398343L;
+
+    private String url;
+
+    public String getUrl() {
+        return url;
+    }
+
+    public void setUrl(final String url) {
+        this.url = url;
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/syncope/blob/19d9e93e/client/enduser/src/main/java/org/apache/syncope/client/enduser/model/CustomTemplateWizard.java
----------------------------------------------------------------------
diff --git a/client/enduser/src/main/java/org/apache/syncope/client/enduser/model/CustomTemplateWizard.java b/client/enduser/src/main/java/org/apache/syncope/client/enduser/model/CustomTemplateWizard.java
new file mode 100644
index 0000000..3952f9b
--- /dev/null
+++ b/client/enduser/src/main/java/org/apache/syncope/client/enduser/model/CustomTemplateWizard.java
@@ -0,0 +1,49 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.syncope.client.enduser.model;
+
+import java.io.Serializable;
+import java.util.HashMap;
+import java.util.Map;
+
+public class CustomTemplateWizard implements Serializable {
+
+    private static final long serialVersionUID = -4290154059045309105L;
+
+    private String firstStep;
+
+    private Map<String, CustomTemplateUrl> steps = new HashMap<>();
+
+    public String getFirstStep() {
+        return firstStep;
+    }
+
+    public void setFirstStep(final String firstStep) {
+        this.firstStep = firstStep;
+    }
+
+    public Map<String, CustomTemplateUrl> getSteps() {
+        return steps;
+    }
+
+    public void setSteps(final Map<String, CustomTemplateUrl> steps) {
+        this.steps = steps;
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/syncope/blob/19d9e93e/client/enduser/src/main/java/org/apache/syncope/client/enduser/model/PlatformInfoRequest.java
----------------------------------------------------------------------
diff --git a/client/enduser/src/main/java/org/apache/syncope/client/enduser/model/PlatformInfoRequest.java b/client/enduser/src/main/java/org/apache/syncope/client/enduser/model/PlatformInfoRequest.java
index c4daa44..bd80359 100644
--- a/client/enduser/src/main/java/org/apache/syncope/client/enduser/model/PlatformInfoRequest.java
+++ b/client/enduser/src/main/java/org/apache/syncope/client/enduser/model/PlatformInfoRequest.java
@@ -37,7 +37,7 @@ public class PlatformInfoRequest implements Serializable {
 
     private int maxUploadFileSizeMB;
 
-    private Map<String, CustomAttributesInfo> customForm;
+    private Map<String, CustomAttributesInfo> customFormAttributes;
 
     public PlatformInfoRequest() {
     }
@@ -90,12 +90,12 @@ public class PlatformInfoRequest implements Serializable {
         this.maxUploadFileSizeMB = maxUploadFileSizeMB;
     }
 
-    public Map<String, CustomAttributesInfo> getCustomForm() {
-        return customForm;
+    public Map<String, CustomAttributesInfo> getCustomFormAttributes() {
+        return customFormAttributes;
     }
 
-    public void setCustomForm(final Map<String, CustomAttributesInfo> customForm) {
-        this.customForm = customForm;
+    public void setCustomFormAttributes(final Map<String, CustomAttributesInfo> customFormAttributes) {
+        this.customFormAttributes = customFormAttributes;
     }
 
 }

http://git-wip-us.apache.org/repos/asf/syncope/blob/19d9e93e/client/enduser/src/main/java/org/apache/syncope/client/enduser/resources/DynamicTemplateResource.java
----------------------------------------------------------------------
diff --git a/client/enduser/src/main/java/org/apache/syncope/client/enduser/resources/DynamicTemplateResource.java b/client/enduser/src/main/java/org/apache/syncope/client/enduser/resources/DynamicTemplateResource.java
new file mode 100644
index 0000000..86b19bd
--- /dev/null
+++ b/client/enduser/src/main/java/org/apache/syncope/client/enduser/resources/DynamicTemplateResource.java
@@ -0,0 +1,79 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.syncope.client.enduser.resources;
+
+import static org.apache.syncope.client.enduser.resources.BaseResource.MAPPER;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.syncope.client.enduser.SyncopeEnduserApplication;
+import org.apache.syncope.client.enduser.SyncopeEnduserConstants;
+import org.apache.syncope.client.enduser.SyncopeEnduserSession;
+import org.apache.syncope.client.enduser.annotations.Resource;
+import org.apache.syncope.client.enduser.model.CustomTemplateInfo;
+import org.apache.syncope.client.enduser.util.SaltGenerator;
+import org.apache.wicket.request.resource.IResource;
+import org.apache.wicket.util.cookies.CookieUtils;
+
+@Resource(key = "info", path = "/api/dynamicTemplate")
+public class DynamicTemplateResource extends BaseResource {
+
+    private static final long serialVersionUID = 7181372091437530936L;
+
+    @Override
+    protected ResourceResponse newResourceResponse(final IResource.Attributes attributes) {
+        ResourceResponse response = new ResourceResponse();
+        response.setContentType(MediaType.APPLICATION_JSON);
+
+        try {
+            final CookieUtils sessionCookieUtils = SyncopeEnduserSession.get().getCookieUtils();
+            // set XSRF_TOKEN cookie
+            if (!SyncopeEnduserSession.get().isXsrfTokenGenerated() && (sessionCookieUtils.getCookie(
+                    SyncopeEnduserConstants.XSRF_COOKIE) == null || StringUtils.isBlank(
+                            sessionCookieUtils.getCookie(SyncopeEnduserConstants.XSRF_COOKIE).getValue()))) {
+                LOG.debug("Set XSRF-TOKEN cookie");
+                SyncopeEnduserSession.get().setXsrfTokenGenerated(true);
+                sessionCookieUtils.save(SyncopeEnduserConstants.XSRF_COOKIE,
+                        SaltGenerator.generate(SyncopeEnduserSession.get().getId()));
+            }
+            response.setTextEncoding(StandardCharsets.UTF_8.name());
+            response.setWriteCallback(new WriteCallback() {
+
+                @Override
+                public void writeData(final IResource.Attributes attributes) throws IOException {
+                    CustomTemplateInfo customTemplate = SyncopeEnduserApplication.get().getCustomTemplate();
+                    attributes.getResponse().write(MAPPER.writeValueAsString(customTemplate));
+                }
+            });
+            response.setStatusCode(Response.Status.OK.getStatusCode());
+        } catch (Exception e) {
+            LOG.error("Error retrieving syncope custom dynamic template", e);
+            response.setError(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode(), new StringBuilder()
+                    .append("ErrorMessage{{ ")
+                    .append(e.getMessage())
+                    .append(" }}")
+                    .toString());
+        }
+
+        return response;
+    }
+}

http://git-wip-us.apache.org/repos/asf/syncope/blob/19d9e93e/client/enduser/src/main/java/org/apache/syncope/client/enduser/resources/ExternalResourceResource.java
----------------------------------------------------------------------
diff --git a/client/enduser/src/main/java/org/apache/syncope/client/enduser/resources/ExternalResourceResource.java b/client/enduser/src/main/java/org/apache/syncope/client/enduser/resources/ExternalResourceResource.java
index d354f56..2d7736c 100644
--- a/client/enduser/src/main/java/org/apache/syncope/client/enduser/resources/ExternalResourceResource.java
+++ b/client/enduser/src/main/java/org/apache/syncope/client/enduser/resources/ExternalResourceResource.java
@@ -20,12 +20,15 @@ package org.apache.syncope.client.enduser.resources;
 
 import java.io.IOException;
 import java.nio.charset.StandardCharsets;
+import java.util.Collections;
 import java.util.List;
 import javax.servlet.http.HttpServletRequest;
 import javax.ws.rs.core.MediaType;
 import javax.ws.rs.core.Response;
+import org.apache.syncope.client.enduser.SyncopeEnduserApplication;
 import org.apache.syncope.client.enduser.SyncopeEnduserSession;
 import org.apache.syncope.client.enduser.annotations.Resource;
+import org.apache.syncope.client.enduser.model.CustomTemplateInfo;
 import org.apache.syncope.common.rest.api.service.SyncopeService;
 import org.apache.wicket.request.resource.AbstractResource;
 import org.apache.wicket.request.resource.IResource;
@@ -49,10 +52,13 @@ public class ExternalResourceResource extends BaseResource {
                 return response;
             }
 
-            final List<String> resources = SyncopeEnduserSession.get().
-                    getService(SyncopeService.class).platform().getResources();
+            CustomTemplateInfo customTemplate =
+                    SyncopeEnduserApplication.get().getCustomTemplate();
+            final List<String> resources = customTemplate.getWizard().getSteps().containsKey("groups")
+                    ? SyncopeEnduserSession.get().
+                            getService(SyncopeService.class).platform().getResources()
+                    : Collections.<String>emptyList();
 
-            response.setTextEncoding(StandardCharsets.UTF_8.name());
             response.setWriteCallback(new AbstractResource.WriteCallback() {
 
                 @Override
@@ -60,6 +66,8 @@ public class ExternalResourceResource extends BaseResource {
                     attributes.getResponse().write(MAPPER.writeValueAsString(resources));
                 }
             });
+
+            response.setTextEncoding(StandardCharsets.UTF_8.name());
             response.setStatusCode(Response.Status.OK.getStatusCode());
         } catch (Exception e) {
             LOG.error("Error retrieving available resources", e);

http://git-wip-us.apache.org/repos/asf/syncope/blob/19d9e93e/client/enduser/src/main/java/org/apache/syncope/client/enduser/resources/GroupResource.java
----------------------------------------------------------------------
diff --git a/client/enduser/src/main/java/org/apache/syncope/client/enduser/resources/GroupResource.java b/client/enduser/src/main/java/org/apache/syncope/client/enduser/resources/GroupResource.java
index c59e5d7..802ffdf 100644
--- a/client/enduser/src/main/java/org/apache/syncope/client/enduser/resources/GroupResource.java
+++ b/client/enduser/src/main/java/org/apache/syncope/client/enduser/resources/GroupResource.java
@@ -28,8 +28,10 @@ import java.util.Map;
 import javax.servlet.http.HttpServletRequest;
 import javax.ws.rs.core.MediaType;
 import javax.ws.rs.core.Response;
+import org.apache.syncope.client.enduser.SyncopeEnduserApplication;
 import org.apache.syncope.client.enduser.SyncopeEnduserSession;
 import org.apache.syncope.client.enduser.annotations.Resource;
+import org.apache.syncope.client.enduser.model.CustomTemplateInfo;
 import org.apache.syncope.common.lib.SyncopeConstants;
 import org.apache.syncope.common.lib.to.GroupTO;
 import org.apache.syncope.common.rest.api.service.SyncopeService;
@@ -55,35 +57,51 @@ public class GroupResource extends BaseResource {
                 return response;
             }
 
-            String realm = URLDecoder.decode(attributes.getParameters().get("realm").
-                    toString(SyncopeConstants.ROOT_REALM), "UTF-8");
-            StringValue term = attributes.getParameters().get("term");
-
-            final GroupResponse groupResponse = new GroupResponse();
-            final int totGroups = SyncopeEnduserSession.get().
-                    getService(SyncopeService.class).numbers().getTotalGroups();
-            final List<GroupTO> groupTOs = SyncopeEnduserSession.get().
-                    getService(SyncopeService.class).searchAssignableGroups(
-                    realm,
-                    term.isNull() || term.isEmpty() ? null : URLDecoder.decode(term.toString(), "UTF-8"),
-                    1,
-                    30).getResult();
-            groupResponse.setTotGroups(totGroups);
-
-            Map<String, String> groups = new HashMap<>();
-            for (GroupTO groupTO : groupTOs) {
-                groups.put(groupTO.getKey(), groupTO.getName());
+            CustomTemplateInfo customTemplate =
+                    SyncopeEnduserApplication.get().getCustomTemplate();
+            if (customTemplate.getWizard().getSteps().containsKey("groups")) {
+                String realm = URLDecoder.decode(attributes.getParameters().get("realm").
+                        toString(SyncopeConstants.ROOT_REALM), "UTF-8");
+                StringValue term = attributes.getParameters().get("term");
+
+                final GroupResponse groupResponse = new GroupResponse();
+                final int totGroups = SyncopeEnduserSession.get().
+                        getService(SyncopeService.class).numbers().getTotalGroups();
+                final List<GroupTO> groupTOs = SyncopeEnduserSession.get().
+                        getService(SyncopeService.class).searchAssignableGroups(
+                        realm,
+                        term.isNull() || term.isEmpty() ? null : URLDecoder.decode(term.toString(), "UTF-8"),
+                        1,
+                        30).getResult();
+                groupResponse.setTotGroups(totGroups);
+
+                Map<String, String> groups = new HashMap<>();
+                for (GroupTO groupTO : groupTOs) {
+                    groups.put(groupTO.getKey(), groupTO.getName());
+                }
+                groupResponse.setGroupTOs(groups);
+
+                response.setWriteCallback(new AbstractResource.WriteCallback() {
+
+                    @Override
+                    public void writeData(final Attributes attributes) throws IOException {
+                        attributes.getResponse().write(MAPPER.writeValueAsString(groupResponse));
+                    }
+                });
+            } else {
+                final GroupResponse groupResponse = new GroupResponse();
+                groupResponse.setTotGroups(0);
+                Map<String, String> groups = new HashMap<>();
+                groupResponse.setGroupTOs(groups);
+                response.setWriteCallback(new AbstractResource.WriteCallback() {
+
+                    @Override
+                    public void writeData(final Attributes attributes) throws IOException {
+                        attributes.getResponse().write(MAPPER.writeValueAsString(groupResponse));
+                    }
+                });
             }
-            groupResponse.setGroupTOs(groups);
-
             response.setTextEncoding(StandardCharsets.UTF_8.name());
-            response.setWriteCallback(new AbstractResource.WriteCallback() {
-
-                @Override
-                public void writeData(final Attributes attributes) throws IOException {
-                    attributes.getResponse().write(MAPPER.writeValueAsString(groupResponse));
-                }
-            });
             response.setStatusCode(Response.Status.OK.getStatusCode());
         } catch (Exception e) {
             LOG.error("Error retrieving available groups", e);

http://git-wip-us.apache.org/repos/asf/syncope/blob/19d9e93e/client/enduser/src/main/java/org/apache/syncope/client/enduser/resources/InfoResource.java
----------------------------------------------------------------------
diff --git a/client/enduser/src/main/java/org/apache/syncope/client/enduser/resources/InfoResource.java b/client/enduser/src/main/java/org/apache/syncope/client/enduser/resources/InfoResource.java
index 23af8fc..0eb96b8 100644
--- a/client/enduser/src/main/java/org/apache/syncope/client/enduser/resources/InfoResource.java
+++ b/client/enduser/src/main/java/org/apache/syncope/client/enduser/resources/InfoResource.java
@@ -57,18 +57,20 @@ public class InfoResource extends BaseResource {
                         SyncopeEnduserSession.get().getId()));
             }
             response.setTextEncoding(StandardCharsets.UTF_8.name());
+
             response.setWriteCallback(new WriteCallback() {
 
                 @Override
                 public void writeData(final IResource.Attributes attributes) throws IOException {
-                    Map<String, CustomAttributesInfo> customForm = SyncopeEnduserApplication.get().getCustomForm();
+                    Map<String, CustomAttributesInfo> customFormAttributes =
+                            SyncopeEnduserApplication.get().getCustomFormAttributes();
                     attributes.getResponse().write(
                             MAPPER.writeValueAsString(
                                     PlatformInfoAdapter.toPlatformInfoRequest(
                                             SyncopeEnduserSession.get().getPlatformInfo(),
-                                            customForm == null
+                                            customFormAttributes == null
                                                     ? new HashMap<String, CustomAttributesInfo>()
-                                                    : customForm)));
+                                                    : customFormAttributes)));
                 }
             });
             response.setStatusCode(Response.Status.OK.getStatusCode());

http://git-wip-us.apache.org/repos/asf/syncope/blob/19d9e93e/client/enduser/src/main/java/org/apache/syncope/client/enduser/resources/SchemaResource.java
----------------------------------------------------------------------
diff --git a/client/enduser/src/main/java/org/apache/syncope/client/enduser/resources/SchemaResource.java b/client/enduser/src/main/java/org/apache/syncope/client/enduser/resources/SchemaResource.java
index f5373ad..a216bfe 100644
--- a/client/enduser/src/main/java/org/apache/syncope/client/enduser/resources/SchemaResource.java
+++ b/client/enduser/src/main/java/org/apache/syncope/client/enduser/resources/SchemaResource.java
@@ -37,6 +37,7 @@ import org.apache.syncope.client.enduser.SyncopeEnduserSession;
 import org.apache.syncope.client.enduser.annotations.Resource;
 import org.apache.syncope.client.enduser.model.CustomAttribute;
 import org.apache.syncope.client.enduser.model.CustomAttributesInfo;
+import org.apache.syncope.client.enduser.model.CustomTemplateInfo;
 import org.apache.syncope.client.enduser.model.SchemaResponse;
 import org.apache.syncope.common.lib.to.SchemaTO;
 import org.apache.syncope.common.lib.to.TypeExtensionTO;
@@ -90,38 +91,47 @@ public class SchemaResource extends BaseResource {
             }
 
             // USER from customization, if empty or null ignore it, use it to filter attributes otherwise
-            Map<String, CustomAttributesInfo> customForm = SyncopeEnduserApplication.get().getCustomForm();
+            Map<String, CustomAttributesInfo> customFormAttributes =
+                    SyncopeEnduserApplication.get().getCustomFormAttributes();
+            CustomTemplateInfo customTemplate =
+                    SyncopeEnduserApplication.get().getCustomTemplate();
 
             SchemaService schemaService = SyncopeEnduserSession.get().getService(SchemaService.class);
             final List<SchemaTO> plainSchemas = classes.isEmpty()
                     ? Collections.<SchemaTO>emptyList()
-                    : customForm == null || customForm.isEmpty() || customForm.get(SchemaType.PLAIN.name()) == null
+                    : customFormAttributes == null
+                    || customFormAttributes.isEmpty()
+                    || customFormAttributes.get(SchemaType.PLAIN.name()) == null
                     ? schemaService.search(
                             new SchemaQuery.Builder().type(SchemaType.PLAIN).anyTypeClasses(classes).build())
-                    : customForm.get(SchemaType.PLAIN.name()).isShow()
+                    : customTemplate.getWizard().getSteps().containsKey("plainSchemas")
                     ? customizeSchemas(schemaService.search(new SchemaQuery.Builder().type(SchemaType.PLAIN).
-                            anyTypeClasses(classes).build()), group, customForm.get(SchemaType.PLAIN.name()).
-                            getAttributes())
+                            anyTypeClasses(classes).build()), group,
+                            customFormAttributes.get(SchemaType.PLAIN.name()).getAttributes())
                     : Collections.<SchemaTO>emptyList();
             final List<SchemaTO> derSchemas = classes.isEmpty()
                     ? Collections.<SchemaTO>emptyList()
-                    : customForm == null || customForm.isEmpty() || customForm.get(SchemaType.DERIVED.name()) == null
+                    : customFormAttributes == null
+                    || customFormAttributes.isEmpty()
+                    || customFormAttributes.get(SchemaType.DERIVED.name()) == null
                     ? schemaService.search(
                             new SchemaQuery.Builder().type(SchemaType.DERIVED).anyTypeClasses(classes).build())
-                    : customForm.get(SchemaType.DERIVED.name()).isShow()
+                    : customTemplate.getWizard().getSteps().containsKey("derivedSchemas")
                     ? customizeSchemas(schemaService.search(new SchemaQuery.Builder().type(SchemaType.DERIVED).
-                            anyTypeClasses(classes).build()), group, customForm.get(SchemaType.DERIVED.name()).
-                            getAttributes())
+                            anyTypeClasses(classes).build()), group,
+                            customFormAttributes.get(SchemaType.DERIVED.name()).getAttributes())
                     : Collections.<SchemaTO>emptyList();
             final List<SchemaTO> virSchemas = classes.isEmpty()
                     ? Collections.<SchemaTO>emptyList()
-                    : customForm == null || customForm.isEmpty() || customForm.get(SchemaType.VIRTUAL.name()) == null
+                    : customFormAttributes == null
+                    || customFormAttributes.isEmpty()
+                    || customFormAttributes.get(SchemaType.VIRTUAL.name()) == null
                     ? schemaService.search(
                             new SchemaQuery.Builder().type(SchemaType.VIRTUAL).anyTypeClasses(classes).build())
-                    : customForm.get(SchemaType.VIRTUAL.name()).isShow()
+                    : customTemplate.getWizard().getSteps().containsKey("virtualSchemas")
                     ? customizeSchemas(schemaService.search(new SchemaQuery.Builder().type(SchemaType.VIRTUAL).
-                            anyTypeClasses(classes).build()), group, customForm.get(SchemaType.VIRTUAL.name()).
-                            getAttributes())
+                            anyTypeClasses(classes).build()), group,
+                            customFormAttributes.get(SchemaType.VIRTUAL.name()).getAttributes())
                     : Collections.<SchemaTO>emptyList();
 
             if (group != null) {
@@ -161,9 +171,9 @@ public class SchemaResource extends BaseResource {
     }
 
     private List<SchemaTO> customizeSchemas(final List<SchemaTO> schemaTOs, final String groupParam,
-            final Map<String, CustomAttribute> customForm) {
+            final Map<String, CustomAttribute> customFormAttributes) {
 
-        if (customForm.isEmpty()) {
+        if (customFormAttributes.isEmpty()) {
             return schemaTOs;
         }
         final boolean isGroupBlank = StringUtils.isBlank(groupParam);
@@ -172,7 +182,7 @@ public class SchemaResource extends BaseResource {
 
             @Override
             public boolean evaluate(final SchemaTO object) {
-                return customForm.containsKey(isGroupBlank
+                return customFormAttributes.containsKey(isGroupBlank
                         ? object.getKey()
                         : compositeSchemaKey(groupParam, object.getKey()));
             }
@@ -182,7 +192,7 @@ public class SchemaResource extends BaseResource {
 
             @Override
             public int compare(final SchemaTO schemaTO1, final SchemaTO schemaTO2) {
-                List<String> order = new ArrayList<>(customForm.keySet());
+                List<String> order = new ArrayList<>(customFormAttributes.keySet());
                 return order.indexOf(isGroupBlank
                         ? schemaTO1.getKey()
                         : compositeSchemaKey(groupParam, schemaTO1.getKey()))

http://git-wip-us.apache.org/repos/asf/syncope/blob/19d9e93e/client/enduser/src/main/java/org/apache/syncope/client/enduser/resources/UserSelfCreateResource.java
----------------------------------------------------------------------
diff --git a/client/enduser/src/main/java/org/apache/syncope/client/enduser/resources/UserSelfCreateResource.java b/client/enduser/src/main/java/org/apache/syncope/client/enduser/resources/UserSelfCreateResource.java
index 00bc356..4747b55 100644
--- a/client/enduser/src/main/java/org/apache/syncope/client/enduser/resources/UserSelfCreateResource.java
+++ b/client/enduser/src/main/java/org/apache/syncope/client/enduser/resources/UserSelfCreateResource.java
@@ -84,7 +84,8 @@ public class UserSelfCreateResource extends BaseUserSelfResource {
                 LOG.trace("Request is [{}]", userTO);
 
                 // check if request is compliant with customization form rules
-                if (UserRequestValidator.compliant(userTO, SyncopeEnduserApplication.get().getCustomForm(), true)) {
+                if (UserRequestValidator.compliant(userTO,
+                        SyncopeEnduserApplication.get().getCustomFormAttributes(), true)) {
 
                     // 1. membership attributes management
                     Set<AttrTO> membAttrs = new HashSet<>();

http://git-wip-us.apache.org/repos/asf/syncope/blob/19d9e93e/client/enduser/src/main/java/org/apache/syncope/client/enduser/resources/UserSelfReadResource.java
----------------------------------------------------------------------
diff --git a/client/enduser/src/main/java/org/apache/syncope/client/enduser/resources/UserSelfReadResource.java b/client/enduser/src/main/java/org/apache/syncope/client/enduser/resources/UserSelfReadResource.java
index 7d95cdc..e40cad0 100644
--- a/client/enduser/src/main/java/org/apache/syncope/client/enduser/resources/UserSelfReadResource.java
+++ b/client/enduser/src/main/java/org/apache/syncope/client/enduser/resources/UserSelfReadResource.java
@@ -92,7 +92,7 @@ public class UserSelfReadResource extends BaseUserSelfResource {
                 membership.getVirAttrs().clear();
             }
             // USER from customization, if empty or null ignore it, use it to filter attributes otherwise
-            applyFromCustomization(userTO, SyncopeEnduserApplication.get().getCustomForm());
+            applyFromCustomization(userTO, SyncopeEnduserApplication.get().getCustomFormAttributes());
 
             // 1.1 Date -> millis conversion for PLAIN attributes of USER
             for (PlainSchemaTO plainSchema : SyncopeEnduserSession.get().getDatePlainSchemas()) {
@@ -122,20 +122,20 @@ public class UserSelfReadResource extends BaseUserSelfResource {
         return response;
     }
 
-    private void applyFromCustomization(final UserTO userTO, final Map<String, CustomAttributesInfo> customForm) {
-        if (customForm != null && !customForm.isEmpty()) {
+    private void applyFromCustomization(final UserTO userTO,
+            final Map<String, CustomAttributesInfo> customFormAttributes) {
+        if (customFormAttributes != null && !customFormAttributes.isEmpty()) {
             // filter PLAIN attributes
-            customizeAttrTOs(userTO.getPlainAttrs(), customForm.get(SchemaType.PLAIN.name()));
+            customizeAttrTOs(userTO.getPlainAttrs(), customFormAttributes.get(SchemaType.PLAIN.name()));
             // filter DERIVED attributes
-            customizeAttrTOs(userTO.getDerAttrs(), customForm.get(SchemaType.DERIVED.name()));
+            customizeAttrTOs(userTO.getDerAttrs(), customFormAttributes.get(SchemaType.DERIVED.name()));
             // filter VIRTUAL attributes
-            customizeAttrTOs(userTO.getVirAttrs(), customForm.get(SchemaType.VIRTUAL.name()));
+            customizeAttrTOs(userTO.getVirAttrs(), customFormAttributes.get(SchemaType.VIRTUAL.name()));
         }
     }
 
     private void customizeAttrTOs(final Set<AttrTO> attrs, final CustomAttributesInfo customAttributesInfo) {
         if (customAttributesInfo != null
-                && customAttributesInfo.isShow()
                 && !customAttributesInfo.getAttributes().isEmpty()) {
 
             CollectionUtils.filter(attrs, new Predicate<AttrTO>() {
@@ -146,7 +146,7 @@ public class UserSelfReadResource extends BaseUserSelfResource {
                 }
             });
 
-        } else if (customAttributesInfo != null && !customAttributesInfo.isShow()) {
+        } else if (customAttributesInfo != null) {
             attrs.clear();
         }
     }

http://git-wip-us.apache.org/repos/asf/syncope/blob/19d9e93e/client/enduser/src/main/java/org/apache/syncope/client/enduser/resources/UserSelfUpdateResource.java
----------------------------------------------------------------------
diff --git a/client/enduser/src/main/java/org/apache/syncope/client/enduser/resources/UserSelfUpdateResource.java b/client/enduser/src/main/java/org/apache/syncope/client/enduser/resources/UserSelfUpdateResource.java
index 34cd279..13a89fd 100644
--- a/client/enduser/src/main/java/org/apache/syncope/client/enduser/resources/UserSelfUpdateResource.java
+++ b/client/enduser/src/main/java/org/apache/syncope/client/enduser/resources/UserSelfUpdateResource.java
@@ -69,10 +69,11 @@ public class UserSelfUpdateResource extends BaseUserSelfResource {
             }
 
             UserTO userTO = MAPPER.readValue(request.getReader().readLine(), UserTO.class);
-            Map<String, CustomAttributesInfo> customForm = SyncopeEnduserApplication.get().getCustomForm();
+            Map<String, CustomAttributesInfo> customFormAttributes =
+                    SyncopeEnduserApplication.get().getCustomFormAttributes();
 
             // check if request is compliant with customization form rules
-            if (UserRequestValidator.compliant(userTO, customForm, false)) {
+            if (UserRequestValidator.compliant(userTO, customFormAttributes, false)) {
                 // 1. membership attributes management
                 Set<AttrTO> membAttrs = new HashSet<>();
                 for (AttrTO attr : userTO.getPlainAttrs()) {
@@ -161,7 +162,7 @@ public class UserSelfUpdateResource extends BaseUserSelfResource {
                 // get old user object from session
                 UserTO selfTO = SyncopeEnduserSession.get().getSelfTO();
                 // align "userTO" and "selfTO" objects
-                if (customForm != null && !customForm.isEmpty()) {
+                if (customFormAttributes != null && !customFormAttributes.isEmpty()) {
                     completeUserObject(userTO, selfTO);
                 }
                 // create diff patch

http://git-wip-us.apache.org/repos/asf/syncope/blob/19d9e93e/client/enduser/src/main/java/org/apache/syncope/client/enduser/util/UserRequestValidator.java
----------------------------------------------------------------------
diff --git a/client/enduser/src/main/java/org/apache/syncope/client/enduser/util/UserRequestValidator.java b/client/enduser/src/main/java/org/apache/syncope/client/enduser/util/UserRequestValidator.java
index 1d02e4e..d7464fd 100644
--- a/client/enduser/src/main/java/org/apache/syncope/client/enduser/util/UserRequestValidator.java
+++ b/client/enduser/src/main/java/org/apache/syncope/client/enduser/util/UserRequestValidator.java
@@ -21,8 +21,10 @@ package org.apache.syncope.client.enduser.util;
 import java.util.Map;
 import org.apache.commons.collections4.IterableUtils;
 import org.apache.commons.collections4.Predicate;
+import org.apache.commons.lang3.StringUtils;
 import org.apache.syncope.client.enduser.model.CustomAttribute;
 import org.apache.syncope.client.enduser.model.CustomAttributesInfo;
+import org.apache.syncope.client.enduser.model.CustomTemplateInfo;
 import org.apache.syncope.common.lib.EntityTOUtils;
 import org.apache.syncope.common.lib.to.AttrTO;
 import org.apache.syncope.common.lib.to.UserTO;
@@ -37,26 +39,26 @@ public final class UserRequestValidator {
     private UserRequestValidator() {
     }
 
-    public static boolean compliant(final UserTO userTO, final Map<String, CustomAttributesInfo> customForm,
+    public static boolean compliant(final UserTO userTO, final Map<String, CustomAttributesInfo> customFormAttributes,
             final boolean checkDefaultValues) {
 
-        if (customForm == null || customForm.isEmpty()) {
+        if (customFormAttributes == null || customFormAttributes.isEmpty()) {
             return true;
         }
 
         return validateAttributes(EntityTOUtils.buildAttrMap(userTO.getPlainAttrs()),
-                customForm.get(SchemaType.PLAIN.name()), checkDefaultValues)
+                customFormAttributes.get(SchemaType.PLAIN.name()), checkDefaultValues)
                 && validateAttributes(EntityTOUtils.buildAttrMap(userTO.getDerAttrs()),
-                        customForm.get(SchemaType.DERIVED.name()), checkDefaultValues)
+                        customFormAttributes.get(SchemaType.DERIVED.name()), checkDefaultValues)
                 && validateAttributes(EntityTOUtils.buildAttrMap(userTO.getVirAttrs()),
-                        customForm.get(SchemaType.VIRTUAL.name()), checkDefaultValues);
+                        customFormAttributes.get(SchemaType.VIRTUAL.name()), checkDefaultValues);
     }
 
     private static boolean validateAttributes(final Map<String, AttrTO> attrMap,
             final CustomAttributesInfo customAttrInfo, final boolean checkDefaultValues) {
 
         return customAttrInfo == null
-                || (customAttrInfo.getAttributes().isEmpty() && customAttrInfo.isShow())
+                || (customAttrInfo.getAttributes().isEmpty())
                 || IterableUtils.matchesAll(attrMap.entrySet(), new Predicate<Map.Entry<String, AttrTO>>() {
 
                     @Override
@@ -75,6 +77,21 @@ public final class UserRequestValidator {
 
     }
 
+    public static boolean validateSteps(final CustomTemplateInfo customTemplateInfo) {
+        return customTemplateInfo != null
+                && StringUtils.isNotBlank(customTemplateInfo.getWizard().getFirstStep())
+                && !customTemplateInfo.getWizard().getSteps().isEmpty();
+
+    }
+
+    public static boolean validateStep(final String stepName, final CustomTemplateInfo customTemplateInfo) {
+        return customTemplateInfo != null
+                && !customTemplateInfo.getWizard().getSteps().isEmpty()
+                && customTemplateInfo.getWizard().getSteps().containsKey(stepName)
+                && StringUtils.isNotBlank(customTemplateInfo.getWizard().getSteps().get(stepName).getUrl());
+
+    }
+
     private static boolean isValid(final AttrTO attrTO, final CustomAttribute customAttribute) {
         return customAttribute.isReadonly()
                 ? IterableUtils.matchesAll(attrTO.getValues(), new Predicate<String>() {

http://git-wip-us.apache.org/repos/asf/syncope/blob/19d9e93e/client/enduser/src/main/resources/META-INF/resources/app/css/app.css
----------------------------------------------------------------------
diff --git a/client/enduser/src/main/resources/META-INF/resources/app/css/app.css b/client/enduser/src/main/resources/META-INF/resources/app/css/app.css
index a91d764..494fb66 100644
--- a/client/enduser/src/main/resources/META-INF/resources/app/css/app.css
+++ b/client/enduser/src/main/resources/META-INF/resources/app/css/app.css
@@ -17,7 +17,8 @@ specific language governing permissions and limitations
 under the License.
 */
 
-/* app general css stylesheet */
+/* App general css stylesheet
+============================================================================= */
 
 .growl-container > .growl-item.ng-enter,
 .growl-container > .growl-item.ng-leave {
@@ -27,73 +28,73 @@ under the License.
   transition:1s linear all;
 }
 
-.k-notification-wrap{
-  white-space: normal !important;
-  word-wrap: break-word !important;
-  font-size: 12px;
-
+.disable-link {
+  pointer-events: none;
+  cursor: default;
 }
 
-.k-notification{
-  width : 320px;
-  font-size: 12px;
+.form-control:disabled, 
+.form-control[readonly] {
+  cursor: not-allowed;
 }
 
-.suggestions{
-  font-size: 10px;
-  display: inline-block;
-  margin-bottom: 5px;
+.card-container.card {
+  width: 350px;
+  padding: 40px 40px 0px;
 }
 
-#resetpassword{
-  background: -moz-linear-gradient(top, #a9db80 0%, #96c56f 100%); /* FF3.6+ */
-  background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#a9db80), color-stop(100%,#96c56f)); /* Chrome,Safari4+ */
-  background: -webkit-linear-gradient(top, #a9db80 0%,#96c56f 100%); /* Chrome10+,Safari5.1+ */
-  background: -o-linear-gradient(top, #a9db80 0%,#96c56f 100%); /* Opera 11.10+ */
-  background: -ms-linear-gradient(top, #a9db80 0%,#96c56f 100%); /* IE10+ */
-  color: black;
-  margin-left: 5px;
-  /*width: 15%;*/
-}
-#resetpassword:hover {
-  background: #658D5D;
-}
-#captchaImg{
-  display: block;
-  margin: 0 auto;
+.card {
+  background-color: #F7F7F7;
+  /* just in case there no content*/
+  padding: 20px 25px 30px;
+  margin: 0 auto 25px;
+  margin-top: 50px;
+  /* shadows and rounded borders */
+  -moz-border-radius: 2px;
+  -webkit-border-radius: 2px;
+  border-radius: 2px;
+  -moz-box-shadow: 0px 2px 2px rgba(0, 0, 0, 0.3);
+  -webkit-box-shadow: 0px 2px 2px rgba(0, 0, 0, 0.3);
+  box-shadow: 0px 2px 2px rgba(0, 0, 0, 0.3);
 }
 
-.disable-link{
-  pointer-events: none;
-  cursor: default;
+.form-control {
+  display: block;
+  width: 100%;
+  height: 34px;
+  padding: 6px 12px;
+  font-size: 14px;
+  line-height: 1.42857;
+  color: #555;
+  background-color: #FFF;
+  /*background-image: none;*/
+  border: 1px solid #CCC;
+  border-radius: 4px;
+  box-shadow: 0px 1px 1px rgba(0, 0, 0, 0.075) inset;
+  transition: border-color 0.15s ease-in-out 0s, box-shadow 0.15s ease-in-out 0s;
 }
 
-.treasure-overlay-spinner-container{
-  z-index : 10001;
+#captchaButtons {
+  margin-top: 5%;
+  margin-bottom: 10px;
 }
 
-treasure-overlay-spinner .treasure-overlay-spinner-content {
-  height: 100%;
-}
-treasure-overlay-spinner {
-  height: 100%;
-  top: 0;
-  bottom: 0;
-  position: fixed;
-  right: 0;
-  left: 0;
-  overflow-y: auto;
+/* <!-- Useless with Bootstrap > 3 */
+.p-0 {
+  padding-left: 0 !important;
+  padding-right: 0 !important;
 }
 
-treasure-overlay-spinner .treasure-overlay-spinner {
-  position: fixed;
+.float-left {
+  float: left !important;
 }
-
-treasure-overlay-spinner .treasure-overlay-spinner-container {
-  position: fixed;
-  background: rgba(0, 0, 0, 0.5490196078431373);
+.float-right {
+  float: right !important;
 }
 
-treasure-overlay-spinner.treasure-overlay-spinner-active-remove{
-  transition: all 150ms ease-in 0s
+.btn-default {
+  color: #333;
+  background-color: #fff;
+  border-color: #ccc;
 }
+/* Useless with Bootstrap > 3 --> */
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/syncope/blob/19d9e93e/client/enduser/src/main/resources/META-INF/resources/app/css/customSpinner.css
----------------------------------------------------------------------
diff --git a/client/enduser/src/main/resources/META-INF/resources/app/css/customSpinner.css b/client/enduser/src/main/resources/META-INF/resources/app/css/customSpinner.css
new file mode 100644
index 0000000..bbfe1de
--- /dev/null
+++ b/client/enduser/src/main/resources/META-INF/resources/app/css/customSpinner.css
@@ -0,0 +1,49 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements.  See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership.  The ASF licenses this file
+to you under the Apache License, Version 2.0 (the
+"License"); you may not use this file except in compliance
+with the License.  You may obtain a copy of the License at
+
+  http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing,
+software distributed under the License is distributed on an
+"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+KIND, either express or implied.  See the License for the
+specific language governing permissions and limitations
+under the License.
+*/
+
+.treasure-overlay-spinner-container{
+  z-index : 10001;
+}
+
+treasure-overlay-spinner .treasure-overlay-spinner-content {
+  height: 100%;
+}
+treasure-overlay-spinner {
+  height: 100%;
+  top: 0;
+  bottom: 0;
+  position: fixed;
+  right: 0;
+  left: 0;
+  overflow-y: auto;
+}
+
+treasure-overlay-spinner .treasure-overlay-spinner {
+  position: fixed;
+  pointer-events: none;
+}
+
+treasure-overlay-spinner .treasure-overlay-spinner-container {
+  position: fixed;
+  background: rgba(0, 0, 0, 0.5490196078431373);
+}
+
+treasure-overlay-spinner.treasure-overlay-spinner-active-remove {
+  transition: all 150ms ease-in 0s;
+}
\ No newline at end of file