keycloak-aplcache

Changes

forms/pom.xml 53(+9 -44)

forms/src/main/java/org/keycloak/forms/model/Property.java 21(+0 -21)

forms/src/main/java/org/keycloak/forms/model/RequiredCredential.java 27(+0 -27)

forms/src/main/java/org/keycloak/forms/model/SocialProvider.java 27(+0 -27)

forms/src/main/java/org/keycloak/service/FormServiceImpl.java 207(+0 -207)

forms/src/main/resources/META-INF/resources/forms/theme/default/css/zocial/zocial.less 281(+0 -281)

forms/src/main/resources/META-INF/resources/forms/theme/default/template-login-action.ftl 84(+0 -84)

forms/src/main/resources/META-INF/services/org.keycloak.services.FormService 1(+0 -1)

server/pom.xml 27(+26 -1)

services/pom.xml 16(+16 -0)

services/src/main/java/org/keycloak/services/FormService.java 210(+0 -210)

services/src/main/java/org/keycloak/services/resources/flows/FormFlows.java 267(+0 -267)

services/src/main/java/org/keycloak/services/resources/flows/Pages.java 61(+0 -61)

Details

diff --git a/admin-ui/src/main/resources/META-INF/resources/admin/js/app.js b/admin-ui/src/main/resources/META-INF/resources/admin/js/app.js
index aa32945..bda811f 100755
--- a/admin-ui/src/main/resources/META-INF/resources/admin/js/app.js
+++ b/admin-ui/src/main/resources/META-INF/resources/admin/js/app.js
@@ -47,6 +47,9 @@ module.config([ '$routeProvider', function($routeProvider) {
             resolve : {
                 realm : function(RealmLoader) {
                     return RealmLoader();
+                },
+                serverInfo : function(ServerInfoLoader) {
+                    return ServerInfoLoader();
                 }
             },
             controller : 'RealmDetailCtrl'
@@ -78,6 +81,9 @@ module.config([ '$routeProvider', function($routeProvider) {
             resolve : {
                 realm : function(RealmLoader) {
                     return RealmLoader();
+                },
+                serverInfo : function(ServerInfoLoader) {
+                    return ServerInfoLoader();
                 }
             },
             controller : 'RealmSocialCtrl'
@@ -780,4 +786,10 @@ module.filter('remove', function() {
 
         return out;
     };
+});
+
+module.filter('capitalize', function() {
+    return function(input) {
+        return input.substring(0, 1).toUpperCase() + input.substring(1);
+    }
 });
\ No newline at end of file
diff --git a/admin-ui/src/main/resources/META-INF/resources/admin/js/controllers/realm.js b/admin-ui/src/main/resources/META-INF/resources/admin/js/controllers/realm.js
index 3ff8682..31f5834 100755
--- a/admin-ui/src/main/resources/META-INF/resources/admin/js/controllers/realm.js
+++ b/admin-ui/src/main/resources/META-INF/resources/admin/js/controllers/realm.js
@@ -141,8 +141,9 @@ module.controller('RealmCreateCtrl', function($scope, Current, Realm, $upload, $
 });
 
 
-module.controller('RealmDetailCtrl', function($scope, Current, Realm, realm, $http, $location, Dialog, Notifications) {
+module.controller('RealmDetailCtrl', function($scope, Current, Realm, realm, serverInfo, $http, $location, Dialog, Notifications) {
     $scope.createRealm = !realm.realm;
+    $scope.serverInfo = serverInfo;
 
     console.log('RealmDetailCtrl');
 
@@ -259,14 +260,7 @@ module.controller('RealmDetailCtrl', function($scope, Current, Realm, realm, $ht
 module.controller('RealmRequiredCredentialsCtrl', function($scope, Realm, realm, $http, $location, Dialog, Notifications, PasswordPolicy) {
     console.log('RealmRequiredCredentialsCtrl');
 
-    $scope.realm = {
-        id : realm.realm, realm : realm.realm, social : realm.social,
-        requiredCredentials : realm.requiredCredentials,
-        requiredApplicationCredentials : realm.requiredApplicationCredentials,
-        requiredOAuthClientCredentials : realm.requiredOAuthClientCredentials,
-        registrationAllowed : realm.registrationAllowed,
-        passwordPolicy: realm.passwordPolicy
-    };
+    $scope.realm = realm;
 
     var oldCopy = angular.copy($scope.realm);
 
@@ -274,8 +268,12 @@ module.controller('RealmRequiredCredentialsCtrl', function($scope, Realm, realm,
     $scope.policyMessages = PasswordPolicy.policyMessages;
 
     $scope.policy = PasswordPolicy.parse(realm.passwordPolicy);
+    var oldPolicy = angular.copy($scope.policy);
 
     $scope.addPolicy = function(policy){
+        if (!$scope.policy) {
+            $scope.policy = [];
+        }
         $scope.policy.push(policy);
     }
 
@@ -298,7 +296,7 @@ module.controller('RealmRequiredCredentialsCtrl', function($scope, Realm, realm,
     }, true);
 
     $scope.$watch('policy', function(oldVal, newVal) {
-        if (oldVal != newVal) {
+        if (!angular.equals($scope.policy, oldPolicy)) {
             $scope.realm.passwordPolicy = PasswordPolicy.toString($scope.policy);
             $scope.changed = true;
         }
@@ -311,14 +309,13 @@ module.controller('RealmRequiredCredentialsCtrl', function($scope, Realm, realm,
             $location.url("/realms/" + realm.realm + "/required-credentials");
             Notifications.success("Your changes have been saved to the realm.");
             oldCopy = angular.copy($scope.realm);
+            oldPolicy = angular.copy($scope.policy);
         });
     };
 
     $scope.reset = function() {
         $scope.realm = angular.copy(oldCopy);
-        $scope.policy = PasswordPolicy.parse(oldCopy.passwordPolicy);
-        console.debug(realm.passwordPolicy);
-
+        $scope.policy = angular.copy(oldPolicy);
         $scope.changed = false;
     };
 });
@@ -473,97 +470,41 @@ module.controller('RealmDefaultRolesCtrl', function ($scope, Realm, realm, appli
 
 });
 
-module.controller('RealmSocialCtrl', function($scope, realm, Realm, $location, Notifications) {
+module.controller('RealmSocialCtrl', function($scope, realm, Realm, serverInfo, $location, Notifications) {
     console.log('RealmSocialCtrl');
 
-    $scope.realm = { id : realm.id, realm : realm.realm, social : realm.social, registrationAllowed : realm.registrationAllowed,
-        tokenLifespan : realm.tokenLifespan,  accessCodeLifespan : realm.accessCodeLifespan };
-
-    if (!realm["socialProviders"]){
-        $scope.realm["socialProviders"] = {};
-    } else {
-        $scope.realm["socialProviders"] = realm.socialProviders;
-    }
+    $scope.realm = angular.copy(realm);
+    $scope.serverInfo = serverInfo;
 
-    // Hardcoded provider list in form of map providerId:providerName
-    $scope.allProviders = { google:"Google", facebook:"Facebook", twitter:"Twitter" };
-    $scope.availableProviders = [];
+    $scope.allProviders = serverInfo.socialProviders;
+    $scope.configuredProviders = [];
 
-    for (var provider in $scope.allProviders){
-        $scope.availableProviders.push(provider);
-    }
+    $scope.$watch('realm.socialProviders', function(socialProviders) {
+        $scope.configuredProviders = [];
+         for (var providerConfig in socialProviders) {
+             var i = providerConfig.split('.');
+             if (i.length == 2 && i[1] == 'key') {
+                 $scope.configuredProviders.push(i[0]);
+             }
+         }
+    }, true);
 
     var oldCopy = angular.copy($scope.realm);
     $scope.changed = false;
     $scope.callbackUrl = $location.absUrl().replace(/\/admin.*/, "/rest/social/callback");
 
-    // To get rid of the "undefined" option in the provider select list
-    // Setting the 1st option from the list (if the list is not empty)
-    var selectFirstProvider = function(){
-        if ($scope.unsetProviders.length > 0){
-            $scope.newProviderId = $scope.unsetProviders[0];
-        } else {
-            $scope.newProviderId = null;
+    $scope.addProvider = function(pId) {
+        if (!$scope.realm.socialProviders) {
+            $scope.realm.socialProviders = {};
         }
-    }
-
-    // Fill in configured providers
-    var initSocial = function() {
-        // postSaveProviders is used for remembering providers which were already validated after pressing the save button
-        // thanks to this it's easy to distinguish between newly added fields and those already tried to be saved
-        $scope.postSaveProviders = [];
-        $scope.unsetProviders = [];
-        $scope.configuredProviders = [];
 
-        for (var providerConfig in $scope.realm.socialProviders){
-            // Get the provider ID which is before the '.' (i.e. google in google.key or google.secret)
-            if ($scope.realm.socialProviders.hasOwnProperty(providerConfig)){
-                var pId = providerConfig.split('.')[0];
-                if ($scope.configuredProviders.indexOf(pId) < 0){
-                    $scope.configuredProviders.push(pId);
-                }
-            }
-        }
-
-        // If no providers are already configured, you can add any of them
-        if ($scope.configuredProviders.length == 0){
-            $scope.unsetProviders = $scope.availableProviders.slice(0);
-        } else {
-            for (var i = 0; i < $scope.availableProviders.length; i++){
-                var providerId = $scope.availableProviders[i];
-                if ($scope.configuredProviders.indexOf(providerId) < 0){
-                    $scope.unsetProviders.push(providerId);
-                }
-            }
-        }
-
-        selectFirstProvider();
-    };
-
-    initSocial();
-
-    $scope.addProvider = function() {
-        if ($scope.availableProviders.indexOf($scope.newProviderId) > -1){
-            $scope.realm.socialProviders[$scope.newProviderId+".key"]="";
-            $scope.realm.socialProviders[$scope.newProviderId+".secret"]="";
-            $scope.configuredProviders.push($scope.newProviderId);
-            $scope.unsetProviders.splice($scope.unsetProviders.indexOf($scope.newProviderId),1);
-            selectFirstProvider();
-        }
+        $scope.realm.socialProviders[pId + ".key"] = "";
+        $scope.realm.socialProviders[pId + ".secret"] = "";
     };
 
     $scope.removeProvider = function(pId) {
         delete $scope.realm.socialProviders[pId+".key"];
         delete $scope.realm.socialProviders[pId+".secret"];
-        $scope.configuredProviders.splice($scope.configuredProviders.indexOf(pId),1);
-
-        // Removing from postSaveProviders, so the empty fields are not red if the provider is added to the list again
-        var rId = $scope.postSaveProviders.indexOf(pId);
-        if (rId > -1){
-            $scope.postSaveProviders.splice(rId,1)
-        }
-
-        $scope.unsetProviders.push(pId);
     };
 
     $scope.$watch('realm', function() {
@@ -586,8 +527,6 @@ module.controller('RealmSocialCtrl', function($scope, realm, Realm, $location, N
     $scope.reset = function() {
         $scope.realm = angular.copy(oldCopy);
         $scope.changed = false;
-        // Initialize lists of configured and unset providers again
-        initSocial();
     };
 
 });
diff --git a/admin-ui/src/main/resources/META-INF/resources/admin/js/loaders.js b/admin-ui/src/main/resources/META-INF/resources/admin/js/loaders.js
index d22e03b..faf05cd 100755
--- a/admin-ui/src/main/resources/META-INF/resources/admin/js/loaders.js
+++ b/admin-ui/src/main/resources/META-INF/resources/admin/js/loaders.js
@@ -35,6 +35,10 @@ module.factory('RealmListLoader', function(Loader, Realm, $q) {
 	return Loader.get(Realm);
 });
 
+module.factory('ServerInfoLoader', function(Loader, ServerInfo, $q) {
+    return Loader.get(ServerInfo);
+});
+
 module.factory('RealmLoader', function(Loader, Realm, $route, $q) {
 	return Loader.get(Realm, function() {
 		return {
diff --git a/admin-ui/src/main/resources/META-INF/resources/admin/js/services.js b/admin-ui/src/main/resources/META-INF/resources/admin/js/services.js
index 33e47a9..b572a10 100755
--- a/admin-ui/src/main/resources/META-INF/resources/admin/js/services.js
+++ b/admin-ui/src/main/resources/META-INF/resources/admin/js/services.js
@@ -1,645 +1,649 @@
-'use strict';
-
-var module = angular.module('keycloak.services', [ 'ngResource' ]);
-
-module.service('Dialog', function($dialog) {
-	var dialog = {};
-
-	var escapeHtml = function(str) {
-		var div = document.createElement('div');
-		div.appendChild(document.createTextNode(str));
-		return div.innerHTML;
-	};
-
-	dialog.confirmDelete = function(name, type, success) {
-		var title = 'Delete ' + escapeHtml(type.charAt(0).toUpperCase() + type.slice(1));
-		var msg = '<span class="primary">Are you sure you want to permanently delete the ' + escapeHtml(type) + ' "' + escapeHtml(name) + '"?</span>' +
-            '<span>This action can\'t be undone.</span>';
-		var btns = [ {
-			result : 'cancel',
-			label : 'Cancel'
-		}, {
-			result : 'ok',
-			label : 'Delete',
-			cssClass : 'destructive'
-		} ];
-
-		$dialog.messageBox(title, msg, btns).open().then(function(result) {
-			if (result == "ok") {
-				success();
-			}
-		});
-	}
-
-    dialog.confirmGenerateKeys = function(name, type, success) {
-        var title = 'Generate new keys for realm';
-        var msg = '<span class="primary">Are you sure you want to permanently generate new keys for ' + name + '"?</span>' +
-            '<span>This action can\'t be undone.</span>';
-        var btns = [ {
-            result : 'cancel',
-            label : 'Cancel'
-        }, {
-            result : 'ok',
-            label : 'Generate new keys',
-            cssClass : 'destructive'
-        } ];
-
-        $dialog.messageBox(title, msg, btns).open().then(function(result) {
-            if (result == "ok") {
-                success();
-            }
-        });
-    }
-
-	return dialog
-});
-
-module.factory('Notifications', function($rootScope, $timeout) {
-	// time (in ms) the notifications are shown
-	var delay = 5000;
-
-	var notifications = {};
-
-	var scheduled = null;
-	var schedulePop = function() {
-		if (scheduled) {
-			$timeout.cancel(scheduled);
-		}
-
-		scheduled = $timeout(function() {
-			$rootScope.notification = null;
-			scheduled = null;
-		}, delay);
-	};
-
-	if (!$rootScope.notifications) {
-		$rootScope.notifications = [];
-	}
-
-	notifications.message = function(type, header, message) {
-		$rootScope.notification = {
-			type : type,
-			header: header,
-			message : message
-		};
-
-		schedulePop();
-	}
-
-	notifications.info = function(message) {
-		notifications.message("info", "Info!", message);
-	};
-
-	notifications.success = function(message) {
-		notifications.message("success", "Success!", message);
-	};
-
-	notifications.error = function(message) {
-		notifications.message("error", "Error!", message);
-	};
-
-	notifications.warn = function(message) {
-		notifications.message("warn", "Warning!", message);
-	};
-
-	return notifications;
-});
-
-module.factory('Realm', function($resource) {
-	return $resource('/auth/rest/admin/realms/:id', {
-		id : '@realm'
-	}, {
-		update : {
-			method : 'PUT'
-		},
-        create : {
-            method : 'POST',
-            params : { id : ''}
-        }
-
-    });
-});
-
-module.factory('User', function($resource) {
-	return $resource('/auth/rest/admin/realms/:realm/users/:userId', {
-		realm : '@realm',
-		userId : '@userId'
-	}, {
-        update : {
-            method : 'PUT'
-        }
-	});
-});
-
-module.factory('UserCredentials', function($resource) {
-    return $resource('/auth/rest/admin/realms/:realm/users/:userId/credentials', {
-        realm : '@realm',
-        userId : '@userId'
-    }, {
-        update : {
-            method : 'PUT',
-            isArray : true
-        }
-    });
-});
-
-module.factory('RealmRoleMapping', function($resource) {
-    return $resource('/auth/rest/admin/realms/:realm/users/:userId/role-mappings/realm', {
-        realm : '@realm',
-        userId : '@userId'
-    });
-});
-
-module.factory('ApplicationRoleMapping', function($resource) {
-    return $resource('/auth/rest/admin/realms/:realm/users/:userId/role-mappings/applications/:application', {
-        realm : '@realm',
-        userId : '@userId',
-        application : "@application"
-    });
-});
-
-module.factory('ApplicationRealmScopeMapping', function($resource) {
-    return $resource('/auth/rest/admin/realms/:realm/applications/:application/scope-mappings/realm', {
-        realm : '@realm',
-        application : '@application'
-    });
-});
-
-module.factory('ApplicationApplicationScopeMapping', function($resource) {
-    return $resource('/auth/rest/admin/realms/:realm/applications/:application/scope-mappings/applications/:targetApp', {
-        realm : '@realm',
-        application : '@application',
-        targetApp : '@targetApp'
-    });
-});
-
-
-
-module.factory('RealmRoles', function($resource) {
-    return $resource('/auth/rest/admin/realms/:realm/roles', {
-        realm : '@realm'
-    });
-});
-
-module.factory('RoleRealmComposites', function($resource) {
-    return $resource('/auth/rest/admin/realms/:realm/roles-by-id/:role/composites/realm', {
-        realm : '@realm',
-        role : '@role'
-    });
-});
-
-module.factory('RoleApplicationComposites', function($resource) {
-    return $resource('/auth/rest/admin/realms/:realm/roles-by-id/:role/composites/applications/:application', {
-        realm : '@realm',
-        role : '@role',
-        application : "@application"
-    });
-});
-
-
-function roleControl($scope, realm, role, roles, applications,
-                     ApplicationRole, RoleById, RoleRealmComposites, RoleApplicationComposites,
-                     $http, $location, Notifications, Dialog) {
-
-    $scope.$watch(function () {
-        return $location.path();
-    }, function () {
-        $scope.path = $location.path().substring(1).split("/");
-    });
-
-    $scope.$watch('role', function () {
-        if (!angular.equals($scope.role, role)) {
-            $scope.changed = true;
-        }
-    }, true);
-
-    $scope.update = function () {
-        RoleById.update({
-            realm: realm.realm,
-            role: role.id
-        }, $scope.role, function () {
-            $scope.changed = false;
-            role = angular.copy($scope.role);
-            Notifications.success("Your changes have been saved to the role.");
-        });
-    };
-
-    $scope.reset = function () {
-        $scope.role = angular.copy(role);
-        $scope.changed = false;
-    };
-
-    if (!role.id) return;
-
-    $scope.realmRoles = angular.copy(roles);
-    $scope.selectedRealmRoles = [];
-    $scope.selectedRealmMappings = [];
-    $scope.realmMappings = [];
-    $scope.applications = applications;
-    $scope.applicationRoles = [];
-    $scope.selectedApplicationRoles = [];
-    $scope.selectedApplicationMappings = [];
-    $scope.applicationMappings = [];
-
-    console.log('remove self');
-    for (var j = 0; j < $scope.realmRoles.length; j++) {
-        if ($scope.realmRoles[j].id == role.id) {
-            var realmRole = $scope.realmRoles[j];
-            var idx = $scope.realmRoles.indexOf(realmRole);
-            $scope.realmRoles.splice(idx, 1);
-            break;
-        }
-    }
-
-
-    $scope.realmMappings = RoleRealmComposites.query({realm : realm.realm, role : role.id}, function(){
-        for (var i = 0; i < $scope.realmMappings.length; i++) {
-            var role = $scope.realmMappings[i];
-            for (var j = 0; j < $scope.realmRoles.length; j++) {
-                var realmRole = $scope.realmRoles[j];
-                if (realmRole.id == role.id) {
-                    var idx = $scope.realmRoles.indexOf(realmRole);
-                    if (idx != -1) {
-                        $scope.realmRoles.splice(idx, 1);
-                        break;
-                    }
-                }
-            }
-        }
-    });
-
-    $scope.addRealmRole = function() {
-        $http.post('/auth/rest/admin/realms/' + realm.realm + '/roles-by-id/' + role.id + '/composites',
-                $scope.selectedRealmRoles).success(function() {
-                for (var i = 0; i < $scope.selectedRealmRoles.length; i++) {
-                    var role = $scope.selectedRealmRoles[i];
-                    var idx = $scope.realmRoles.indexOf($scope.selectedRealmRoles[i]);
-                    if (idx != -1) {
-                        $scope.realmRoles.splice(idx, 1);
-                        $scope.realmMappings.push(role);
-                    }
-                }
-                $scope.selectRealmRoles = [];
-            });
-    };
-
-    $scope.deleteRealmRole = function() {
-        $http.delete('/auth/rest/admin/realms/' + realm.realm + '/roles-by-id/' + role.id + '/composites',
-            {data : $scope.selectedRealmMappings, headers : {"content-type" : "application/json"}}).success(function() {
-                for (var i = 0; i < $scope.selectedRealmMappings.length; i++) {
-                    var role = $scope.selectedRealmMappings[i];
-                    var idx = $scope.realmMappings.indexOf($scope.selectedRealmMappings[i]);
-                    if (idx != -1) {
-                        $scope.realmMappings.splice(idx, 1);
-                        $scope.realmRoles.push(role);
-                    }
-                }
-                $scope.selectedRealmMappings = [];
-            });
-    };
-
-    $scope.addApplicationRole = function() {
-        $http.post('/auth/rest/admin/realms/' + realm.realm + '/roles-by-id/' + role.id + '/composites',
-                $scope.selectedApplicationRoles).success(function() {
-                for (var i = 0; i < $scope.selectedApplicationRoles.length; i++) {
-                    var role = $scope.selectedApplicationRoles[i];
-                    var idx = $scope.applicationRoles.indexOf($scope.selectedApplicationRoles[i]);
-                    if (idx != -1) {
-                        $scope.applicationRoles.splice(idx, 1);
-                        $scope.applicationMappings.push(role);
-                    }
-                }
-                $scope.selectedApplicationRoles = [];
-            });
-    };
-
-    $scope.deleteApplicationRole = function() {
-        $http.delete('/auth/rest/admin/realms/' + realm.realm + '/roles-by-id/' + role.id + '/composites',
-            {data : $scope.selectedApplicationMappings, headers : {"content-type" : "application/json"}}).success(function() {
-                for (var i = 0; i < $scope.selectedApplicationMappings.length; i++) {
-                    var role = $scope.selectedApplicationMappings[i];
-                    var idx = $scope.applicationMappings.indexOf($scope.selectedApplicationMappings[i]);
-                    if (idx != -1) {
-                        $scope.applicationMappings.splice(idx, 1);
-                        $scope.applicationRoles.push(role);
-                    }
-                }
-                $scope.selectedApplicationMappings = [];
-            });
-    };
-
-
-    $scope.changeApplication = function() {
-        $scope.applicationRoles = ApplicationRole.query({realm : realm.realm, application : $scope.compositeApp.name}, function() {
-                $scope.applicationMappings = RoleApplicationComposites.query({realm : realm.realm, role : role.id, application : $scope.compositeApp.name}, function(){
-                    for (var i = 0; i < $scope.applicationMappings.length; i++) {
-                        var role = $scope.applicationMappings[i];
-                        for (var j = 0; j < $scope.applicationRoles.length; j++) {
-                            var realmRole = $scope.applicationRoles[j];
-                            if (realmRole.id == role.id) {
-                                var idx = $scope.applicationRoles.indexOf(realmRole);
-                                if (idx != -1) {
-                                    $scope.applicationRoles.splice(idx, 1);
-                                    break;
-                                }
-                            }
-                        }
-                    }
-                });
-                for (var j = 0; j < $scope.applicationRoles.length; j++) {
-                    if ($scope.applicationRoles[j] == role.id) {
-                        var appRole = $scope.applicationRoles[j];
-                        var idx = $scope.applicationRoles.indexof(appRole);
-                        $scope.applicationRoles.splice(idx, 1);
-                        break;
-                    }
-                }
-            }
-        );
-    };
-
-
-
-
-}
-
-
-module.factory('Role', function($resource) {
-    return $resource('/auth/rest/admin/realms/:realm/roles/:role', {
-        realm : '@realm',
-        role : '@role'
-    },  {
-        update : {
-            method : 'PUT'
-        }
-    });
-});
-
-module.factory('RoleById', function($resource) {
-    return $resource('/auth/rest/admin/realms/:realm/roles-by-id/:role', {
-        realm : '@realm',
-        role : '@role'
-    },  {
-        update : {
-            method : 'PUT'
-        }
-    });
-});
-
-module.factory('ApplicationRole', function($resource) {
-    return $resource('/auth/rest/admin/realms/:realm/applications/:application/roles/:role', {
-        realm : '@realm',
-        application : "@application",
-        role : '@role'
-    },  {
-        update : {
-            method : 'PUT'
-        }
-    });
-});
-
-
-module.factory('Application', function($resource) {
-    return $resource('/auth/rest/admin/realms/:realm/applications/:application', {
-        realm : '@realm',
-        application : '@name'
-    },  {
-        update : {
-            method : 'PUT'
-        }
-    });
-});
-
-module.factory('ApplicationInstallation', function($resource) {
-    var url = '/auth/rest/admin/realms/:realm/applications/:application/installation';
-    var resource = $resource('/auth/rest/admin/realms/:realm/applications/:application/installation', {
-        realm : '@realm',
-        application : '@application'
-    },  {
-        update : {
-            method : 'PUT'
-        }
-    });
-    resource.url = function(parameters) {
-        return url.replace(':realm', parameters.realm).replace(':application', parameters.application);
-    }
-    return resource;
-});
-
-module.factory('ApplicationCredentials', function($resource) {
-    return $resource('/auth/rest/admin/realms/:realm/applications/:application/credentials', {
-        realm : '@realm',
-        application : '@application'
-    },  {
-        update : {
-            method : 'PUT',
-            isArray : true
-        }
-    });
-});
-
-module.factory('ApplicationOrigins', function($resource) {
-    return $resource('/auth/rest/admin/realms/:realm/applications/:application/allowed-origins', {
-        realm : '@realm',
-        application : '@application'
-    },  {
-        update : {
-            method : 'PUT',
-            isArray : true
-        }
-    });
-});
-
-module.factory('OAuthClient', function($resource) {
-    return $resource('/auth/rest/admin/realms/:realm/oauth-clients/:id', {
-        realm : '@realm',
-        id : '@id'
-    },  {
-        update : {
-            method : 'PUT'
-        }
-    });
-});
-
-module.factory('OAuthClientCredentials', function($resource) {
-    return $resource('/auth/rest/admin/realms/:realm/oauth-clients/:oauth/credentials', {
-        realm : '@realm',
-        oauth : '@oauth'
-    },  {
-        update : {
-            method : 'PUT',
-            isArray : true
-        }
-    });
-});
-
-module.factory('OAuthClientRealmScopeMapping', function($resource) {
-    return $resource('/auth/rest/admin/realms/:realm/oauth-clients/:oauth/scope-mappings/realm', {
-        realm : '@realm',
-        oauth : '@oauth'
-    });
-});
-
-module.factory('OAuthClientApplicationScopeMapping', function($resource) {
-    return $resource('/auth/rest/admin/realms/:realm/oauth-clients/:oauth/scope-mappings/applications/:targetApp', {
-        realm : '@realm',
-        oauth : '@oauth',
-        targetApp : '@targetApp'
-    });
-});
-
-module.factory('OAuthClientInstallation', function($resource) {
-    var url = '/auth/rest/admin/realms/:realm/oauth-clients/:oauth/installation';
-    var resource = $resource('/auth/rest/admin/realms/:realm/oauth-clients/:oauth/installation', {
-        realm : '@realm',
-        oauth : '@oauth'
-    },  {
-        update : {
-            method : 'PUT'
-        }
-    });
-    resource.url = function(parameters) {
-        return url.replace(':realm', parameters.realm).replace(':oauth', parameters.oauth);
-    }
-    return resource;
-});
-
-
-module.factory('Current', function(Realm, $route) {
-    var current = {};
-
-    current.realms = {};
-    current.realm = null;
-    current.applications = {};
-    current.application = null;
-
-    current.refresh = function() {
-        current.realm = null;
-        current.realms = Realm.query(null, function(realms) {
-            if ($route.current.params.realm) {
-                for (var i = 0; i < realms.length; i++) {
-                    if (realms[i].realm == $route.current.params.realm) {
-                        current.realm =  realms[i];
-                    }
-                }
-            }
-        });
-    }
-
-    current.refresh();
-
-    return current;
-});
-
-module.factory('TimeUnit', function() {
-    var t = {};
-
-    t.autoUnit = function(time) {
-        var unit = 'Seconds';
-        if (time % 60 == 0) {
-            unit = 'Minutes';
-            time  = time / 60;
-        }
-        if (time % 60 == 0) {
-            unit = 'Hours';
-            time = time / 60;
-        }
-        if (time % 24 == 0) {
-            unit = 'Days'
-            time = time / 24;
-        }
-        return unit;
-    }
-
-    t.toSeconds = function(time, unit) {
-        switch (unit) {
-            case 'Seconds': return time;
-            case 'Minutes': return time * 60;
-            case 'Hours': return time * 360;
-            case 'Days': return time * 86400;
-            default: throw 'invalid unit ' + unit;
-        }
-    }
-
-    t.toUnit = function(time, unit) {
-        switch (unit) {
-            case 'Seconds': return time;
-            case 'Minutes': return Math.ceil(time / 60);
-            case 'Hours': return Math.ceil(time / 360);
-            case 'Days': return Math.ceil(time / 86400);
-            default: throw 'invalid unit ' + unit;
-        }
-    }
-
-    t.convert = function(time, from, to) {
-        var seconds = t.toSeconds(time, from);
-        return t.toUnit(seconds, to);
-    }
-
-    return t;
-});
-
-
-module.factory('PasswordPolicy', function() {
-    var p = {};
-
-    p.policyMessages = {
-        length:         "Minimal password length (integer type). Default value is 8.",
-        digits:         "Minimal number (integer type) of digits in password. Default value is 1.",
-        lowerCase:      "Minimal number (integer type) of lowercase characters in password. Default value is 1.",
-        upperCase:      "Minimal number (integer type) of uppercase characters in password. Default value is 1.",
-        specialChars:   "Minimal number (integer type) of special characters in password. Default value is 1."
-    }
-
-    p.allPolicies = [
-        { name: 'length', value: 8 },
-        { name: 'digits', value: 1 },
-        { name: 'lowerCase', value: 1 },
-        { name: 'upperCase', value: 1 },
-        { name: 'specialChars', value: 1 }
-    ];
-
-    p.parse = function(policyString) {
-        var policies = [];
-
-        if (!policyString || policyString.length == 0){
-            return policies;
-        }
-
-        var policyArray = policyString.split(" and ");
-
-        for (var i = 0; i < policyArray.length; i ++){
-            var policyToken = policyArray[i];
-            var re = /(\w+)\(*(\d*)\)*/;
-
-            var policyEntry = re.exec(policyToken);
-
-            policies.push({ name: policyEntry[1], value: parseInt(policyEntry[2]) });
-
-        }
-
-        return policies;
-    };
-
-    p.toString = function(policies) {
-        if (!policies || policies.length == 0) {
-            return null;
-        }
-
-        var policyString = "";
-
-        for (var i in policies){
-            policyString += policies[i].name;
-            if ( policies[i].value ){
-                policyString += '(' + policies[i].value + ')';
-            }
-            policyString += " and ";
-        }
-
-        policyString = policyString.substring(0, policyString.length - 5);
-
-        return policyString;
-    };
-
-    return p;
+'use strict';
+
+var module = angular.module('keycloak.services', [ 'ngResource' ]);
+
+module.service('Dialog', function($dialog) {
+	var dialog = {};
+
+	var escapeHtml = function(str) {
+		var div = document.createElement('div');
+		div.appendChild(document.createTextNode(str));
+		return div.innerHTML;
+	};
+
+	dialog.confirmDelete = function(name, type, success) {
+		var title = 'Delete ' + escapeHtml(type.charAt(0).toUpperCase() + type.slice(1));
+		var msg = '<span class="primary">Are you sure you want to permanently delete the ' + escapeHtml(type) + ' "' + escapeHtml(name) + '"?</span>' +
+            '<span>This action can\'t be undone.</span>';
+		var btns = [ {
+			result : 'cancel',
+			label : 'Cancel'
+		}, {
+			result : 'ok',
+			label : 'Delete',
+			cssClass : 'destructive'
+		} ];
+
+		$dialog.messageBox(title, msg, btns).open().then(function(result) {
+			if (result == "ok") {
+				success();
+			}
+		});
+	}
+
+    dialog.confirmGenerateKeys = function(name, type, success) {
+        var title = 'Generate new keys for realm';
+        var msg = '<span class="primary">Are you sure you want to permanently generate new keys for ' + name + '"?</span>' +
+            '<span>This action can\'t be undone.</span>';
+        var btns = [ {
+            result : 'cancel',
+            label : 'Cancel'
+        }, {
+            result : 'ok',
+            label : 'Generate new keys',
+            cssClass : 'destructive'
+        } ];
+
+        $dialog.messageBox(title, msg, btns).open().then(function(result) {
+            if (result == "ok") {
+                success();
+            }
+        });
+    }
+
+	return dialog
+});
+
+module.factory('Notifications', function($rootScope, $timeout) {
+	// time (in ms) the notifications are shown
+	var delay = 5000;
+
+	var notifications = {};
+
+	var scheduled = null;
+	var schedulePop = function() {
+		if (scheduled) {
+			$timeout.cancel(scheduled);
+		}
+
+		scheduled = $timeout(function() {
+			$rootScope.notification = null;
+			scheduled = null;
+		}, delay);
+	};
+
+	if (!$rootScope.notifications) {
+		$rootScope.notifications = [];
+	}
+
+	notifications.message = function(type, header, message) {
+		$rootScope.notification = {
+			type : type,
+			header: header,
+			message : message
+		};
+
+		schedulePop();
+	}
+
+	notifications.info = function(message) {
+		notifications.message("info", "Info!", message);
+	};
+
+	notifications.success = function(message) {
+		notifications.message("success", "Success!", message);
+	};
+
+	notifications.error = function(message) {
+		notifications.message("error", "Error!", message);
+	};
+
+	notifications.warn = function(message) {
+		notifications.message("warn", "Warning!", message);
+	};
+
+	return notifications;
+});
+
+module.factory('Realm', function($resource) {
+	return $resource('/auth/rest/admin/realms/:id', {
+		id : '@realm'
+	}, {
+		update : {
+			method : 'PUT'
+		},
+        create : {
+            method : 'POST',
+            params : { id : ''}
+        }
+
+    });
+});
+
+module.factory('ServerInfo', function($resource) {
+    return $resource('/auth/rest/admin/serverinfo');
+});
+
+module.factory('User', function($resource) {
+	return $resource('/auth/rest/admin/realms/:realm/users/:userId', {
+		realm : '@realm',
+		userId : '@userId'
+	}, {
+        update : {
+            method : 'PUT'
+        }
+	});
+});
+
+module.factory('UserCredentials', function($resource) {
+    return $resource('/auth/rest/admin/realms/:realm/users/:userId/credentials', {
+        realm : '@realm',
+        userId : '@userId'
+    }, {
+        update : {
+            method : 'PUT',
+            isArray : true
+        }
+    });
+});
+
+module.factory('RealmRoleMapping', function($resource) {
+    return $resource('/auth/rest/admin/realms/:realm/users/:userId/role-mappings/realm', {
+        realm : '@realm',
+        userId : '@userId'
+    });
+});
+
+module.factory('ApplicationRoleMapping', function($resource) {
+    return $resource('/auth/rest/admin/realms/:realm/users/:userId/role-mappings/applications/:application', {
+        realm : '@realm',
+        userId : '@userId',
+        application : "@application"
+    });
+});
+
+module.factory('ApplicationRealmScopeMapping', function($resource) {
+    return $resource('/auth/rest/admin/realms/:realm/applications/:application/scope-mappings/realm', {
+        realm : '@realm',
+        application : '@application'
+    });
+});
+
+module.factory('ApplicationApplicationScopeMapping', function($resource) {
+    return $resource('/auth/rest/admin/realms/:realm/applications/:application/scope-mappings/applications/:targetApp', {
+        realm : '@realm',
+        application : '@application',
+        targetApp : '@targetApp'
+    });
+});
+
+
+
+module.factory('RealmRoles', function($resource) {
+    return $resource('/auth/rest/admin/realms/:realm/roles', {
+        realm : '@realm'
+    });
+});
+
+module.factory('RoleRealmComposites', function($resource) {
+    return $resource('/auth/rest/admin/realms/:realm/roles-by-id/:role/composites/realm', {
+        realm : '@realm',
+        role : '@role'
+    });
+});
+
+module.factory('RoleApplicationComposites', function($resource) {
+    return $resource('/auth/rest/admin/realms/:realm/roles-by-id/:role/composites/applications/:application', {
+        realm : '@realm',
+        role : '@role',
+        application : "@application"
+    });
+});
+
+
+function roleControl($scope, realm, role, roles, applications,
+                     ApplicationRole, RoleById, RoleRealmComposites, RoleApplicationComposites,
+                     $http, $location, Notifications, Dialog) {
+
+    $scope.$watch(function () {
+        return $location.path();
+    }, function () {
+        $scope.path = $location.path().substring(1).split("/");
+    });
+
+    $scope.$watch('role', function () {
+        if (!angular.equals($scope.role, role)) {
+            $scope.changed = true;
+        }
+    }, true);
+
+    $scope.update = function () {
+        RoleById.update({
+            realm: realm.realm,
+            role: role.id
+        }, $scope.role, function () {
+            $scope.changed = false;
+            role = angular.copy($scope.role);
+            Notifications.success("Your changes have been saved to the role.");
+        });
+    };
+
+    $scope.reset = function () {
+        $scope.role = angular.copy(role);
+        $scope.changed = false;
+    };
+
+    if (!role.id) return;
+
+    $scope.realmRoles = angular.copy(roles);
+    $scope.selectedRealmRoles = [];
+    $scope.selectedRealmMappings = [];
+    $scope.realmMappings = [];
+    $scope.applications = applications;
+    $scope.applicationRoles = [];
+    $scope.selectedApplicationRoles = [];
+    $scope.selectedApplicationMappings = [];
+    $scope.applicationMappings = [];
+
+    console.log('remove self');
+    for (var j = 0; j < $scope.realmRoles.length; j++) {
+        if ($scope.realmRoles[j].id == role.id) {
+            var realmRole = $scope.realmRoles[j];
+            var idx = $scope.realmRoles.indexOf(realmRole);
+            $scope.realmRoles.splice(idx, 1);
+            break;
+        }
+    }
+
+
+    $scope.realmMappings = RoleRealmComposites.query({realm : realm.realm, role : role.id}, function(){
+        for (var i = 0; i < $scope.realmMappings.length; i++) {
+            var role = $scope.realmMappings[i];
+            for (var j = 0; j < $scope.realmRoles.length; j++) {
+                var realmRole = $scope.realmRoles[j];
+                if (realmRole.id == role.id) {
+                    var idx = $scope.realmRoles.indexOf(realmRole);
+                    if (idx != -1) {
+                        $scope.realmRoles.splice(idx, 1);
+                        break;
+                    }
+                }
+            }
+        }
+    });
+
+    $scope.addRealmRole = function() {
+        $http.post('/auth/rest/admin/realms/' + realm.realm + '/roles-by-id/' + role.id + '/composites',
+                $scope.selectedRealmRoles).success(function() {
+                for (var i = 0; i < $scope.selectedRealmRoles.length; i++) {
+                    var role = $scope.selectedRealmRoles[i];
+                    var idx = $scope.realmRoles.indexOf($scope.selectedRealmRoles[i]);
+                    if (idx != -1) {
+                        $scope.realmRoles.splice(idx, 1);
+                        $scope.realmMappings.push(role);
+                    }
+                }
+                $scope.selectRealmRoles = [];
+            });
+    };
+
+    $scope.deleteRealmRole = function() {
+        $http.delete('/auth/rest/admin/realms/' + realm.realm + '/roles-by-id/' + role.id + '/composites',
+            {data : $scope.selectedRealmMappings, headers : {"content-type" : "application/json"}}).success(function() {
+                for (var i = 0; i < $scope.selectedRealmMappings.length; i++) {
+                    var role = $scope.selectedRealmMappings[i];
+                    var idx = $scope.realmMappings.indexOf($scope.selectedRealmMappings[i]);
+                    if (idx != -1) {
+                        $scope.realmMappings.splice(idx, 1);
+                        $scope.realmRoles.push(role);
+                    }
+                }
+                $scope.selectedRealmMappings = [];
+            });
+    };
+
+    $scope.addApplicationRole = function() {
+        $http.post('/auth/rest/admin/realms/' + realm.realm + '/roles-by-id/' + role.id + '/composites',
+                $scope.selectedApplicationRoles).success(function() {
+                for (var i = 0; i < $scope.selectedApplicationRoles.length; i++) {
+                    var role = $scope.selectedApplicationRoles[i];
+                    var idx = $scope.applicationRoles.indexOf($scope.selectedApplicationRoles[i]);
+                    if (idx != -1) {
+                        $scope.applicationRoles.splice(idx, 1);
+                        $scope.applicationMappings.push(role);
+                    }
+                }
+                $scope.selectedApplicationRoles = [];
+            });
+    };
+
+    $scope.deleteApplicationRole = function() {
+        $http.delete('/auth/rest/admin/realms/' + realm.realm + '/roles-by-id/' + role.id + '/composites',
+            {data : $scope.selectedApplicationMappings, headers : {"content-type" : "application/json"}}).success(function() {
+                for (var i = 0; i < $scope.selectedApplicationMappings.length; i++) {
+                    var role = $scope.selectedApplicationMappings[i];
+                    var idx = $scope.applicationMappings.indexOf($scope.selectedApplicationMappings[i]);
+                    if (idx != -1) {
+                        $scope.applicationMappings.splice(idx, 1);
+                        $scope.applicationRoles.push(role);
+                    }
+                }
+                $scope.selectedApplicationMappings = [];
+            });
+    };
+
+
+    $scope.changeApplication = function() {
+        $scope.applicationRoles = ApplicationRole.query({realm : realm.realm, application : $scope.compositeApp.name}, function() {
+                $scope.applicationMappings = RoleApplicationComposites.query({realm : realm.realm, role : role.id, application : $scope.compositeApp.name}, function(){
+                    for (var i = 0; i < $scope.applicationMappings.length; i++) {
+                        var role = $scope.applicationMappings[i];
+                        for (var j = 0; j < $scope.applicationRoles.length; j++) {
+                            var realmRole = $scope.applicationRoles[j];
+                            if (realmRole.id == role.id) {
+                                var idx = $scope.applicationRoles.indexOf(realmRole);
+                                if (idx != -1) {
+                                    $scope.applicationRoles.splice(idx, 1);
+                                    break;
+                                }
+                            }
+                        }
+                    }
+                });
+                for (var j = 0; j < $scope.applicationRoles.length; j++) {
+                    if ($scope.applicationRoles[j] == role.id) {
+                        var appRole = $scope.applicationRoles[j];
+                        var idx = $scope.applicationRoles.indexof(appRole);
+                        $scope.applicationRoles.splice(idx, 1);
+                        break;
+                    }
+                }
+            }
+        );
+    };
+
+
+
+
+}
+
+
+module.factory('Role', function($resource) {
+    return $resource('/auth/rest/admin/realms/:realm/roles/:role', {
+        realm : '@realm',
+        role : '@role'
+    },  {
+        update : {
+            method : 'PUT'
+        }
+    });
+});
+
+module.factory('RoleById', function($resource) {
+    return $resource('/auth/rest/admin/realms/:realm/roles-by-id/:role', {
+        realm : '@realm',
+        role : '@role'
+    },  {
+        update : {
+            method : 'PUT'
+        }
+    });
+});
+
+module.factory('ApplicationRole', function($resource) {
+    return $resource('/auth/rest/admin/realms/:realm/applications/:application/roles/:role', {
+        realm : '@realm',
+        application : "@application",
+        role : '@role'
+    },  {
+        update : {
+            method : 'PUT'
+        }
+    });
+});
+
+
+module.factory('Application', function($resource) {
+    return $resource('/auth/rest/admin/realms/:realm/applications/:application', {
+        realm : '@realm',
+        application : '@name'
+    },  {
+        update : {
+            method : 'PUT'
+        }
+    });
+});
+
+module.factory('ApplicationInstallation', function($resource) {
+    var url = '/auth/rest/admin/realms/:realm/applications/:application/installation';
+    var resource = $resource('/auth/rest/admin/realms/:realm/applications/:application/installation', {
+        realm : '@realm',
+        application : '@application'
+    },  {
+        update : {
+            method : 'PUT'
+        }
+    });
+    resource.url = function(parameters) {
+        return url.replace(':realm', parameters.realm).replace(':application', parameters.application);
+    }
+    return resource;
+});
+
+module.factory('ApplicationCredentials', function($resource) {
+    return $resource('/auth/rest/admin/realms/:realm/applications/:application/credentials', {
+        realm : '@realm',
+        application : '@application'
+    },  {
+        update : {
+            method : 'PUT',
+            isArray : true
+        }
+    });
+});
+
+module.factory('ApplicationOrigins', function($resource) {
+    return $resource('/auth/rest/admin/realms/:realm/applications/:application/allowed-origins', {
+        realm : '@realm',
+        application : '@application'
+    },  {
+        update : {
+            method : 'PUT',
+            isArray : true
+        }
+    });
+});
+
+module.factory('OAuthClient', function($resource) {
+    return $resource('/auth/rest/admin/realms/:realm/oauth-clients/:id', {
+        realm : '@realm',
+        id : '@id'
+    },  {
+        update : {
+            method : 'PUT'
+        }
+    });
+});
+
+module.factory('OAuthClientCredentials', function($resource) {
+    return $resource('/auth/rest/admin/realms/:realm/oauth-clients/:oauth/credentials', {
+        realm : '@realm',
+        oauth : '@oauth'
+    },  {
+        update : {
+            method : 'PUT',
+            isArray : true
+        }
+    });
+});
+
+module.factory('OAuthClientRealmScopeMapping', function($resource) {
+    return $resource('/auth/rest/admin/realms/:realm/oauth-clients/:oauth/scope-mappings/realm', {
+        realm : '@realm',
+        oauth : '@oauth'
+    });
+});
+
+module.factory('OAuthClientApplicationScopeMapping', function($resource) {
+    return $resource('/auth/rest/admin/realms/:realm/oauth-clients/:oauth/scope-mappings/applications/:targetApp', {
+        realm : '@realm',
+        oauth : '@oauth',
+        targetApp : '@targetApp'
+    });
+});
+
+module.factory('OAuthClientInstallation', function($resource) {
+    var url = '/auth/rest/admin/realms/:realm/oauth-clients/:oauth/installation';
+    var resource = $resource('/auth/rest/admin/realms/:realm/oauth-clients/:oauth/installation', {
+        realm : '@realm',
+        oauth : '@oauth'
+    },  {
+        update : {
+            method : 'PUT'
+        }
+    });
+    resource.url = function(parameters) {
+        return url.replace(':realm', parameters.realm).replace(':oauth', parameters.oauth);
+    }
+    return resource;
+});
+
+
+module.factory('Current', function(Realm, $route) {
+    var current = {};
+
+    current.realms = {};
+    current.realm = null;
+    current.applications = {};
+    current.application = null;
+
+    current.refresh = function() {
+        current.realm = null;
+        current.realms = Realm.query(null, function(realms) {
+            if ($route.current.params.realm) {
+                for (var i = 0; i < realms.length; i++) {
+                    if (realms[i].realm == $route.current.params.realm) {
+                        current.realm =  realms[i];
+                    }
+                }
+            }
+        });
+    }
+
+    current.refresh();
+
+    return current;
+});
+
+module.factory('TimeUnit', function() {
+    var t = {};
+
+    t.autoUnit = function(time) {
+        var unit = 'Seconds';
+        if (time % 60 == 0) {
+            unit = 'Minutes';
+            time  = time / 60;
+        }
+        if (time % 60 == 0) {
+            unit = 'Hours';
+            time = time / 60;
+        }
+        if (time % 24 == 0) {
+            unit = 'Days'
+            time = time / 24;
+        }
+        return unit;
+    }
+
+    t.toSeconds = function(time, unit) {
+        switch (unit) {
+            case 'Seconds': return time;
+            case 'Minutes': return time * 60;
+            case 'Hours': return time * 360;
+            case 'Days': return time * 86400;
+            default: throw 'invalid unit ' + unit;
+        }
+    }
+
+    t.toUnit = function(time, unit) {
+        switch (unit) {
+            case 'Seconds': return time;
+            case 'Minutes': return Math.ceil(time / 60);
+            case 'Hours': return Math.ceil(time / 360);
+            case 'Days': return Math.ceil(time / 86400);
+            default: throw 'invalid unit ' + unit;
+        }
+    }
+
+    t.convert = function(time, from, to) {
+        var seconds = t.toSeconds(time, from);
+        return t.toUnit(seconds, to);
+    }
+
+    return t;
+});
+
+
+module.factory('PasswordPolicy', function() {
+    var p = {};
+
+    p.policyMessages = {
+        length:         "Minimal password length (integer type). Default value is 8.",
+        digits:         "Minimal number (integer type) of digits in password. Default value is 1.",
+        lowerCase:      "Minimal number (integer type) of lowercase characters in password. Default value is 1.",
+        upperCase:      "Minimal number (integer type) of uppercase characters in password. Default value is 1.",
+        specialChars:   "Minimal number (integer type) of special characters in password. Default value is 1."
+    }
+
+    p.allPolicies = [
+        { name: 'length', value: 8 },
+        { name: 'digits', value: 1 },
+        { name: 'lowerCase', value: 1 },
+        { name: 'upperCase', value: 1 },
+        { name: 'specialChars', value: 1 }
+    ];
+
+    p.parse = function(policyString) {
+        var policies = [];
+
+        if (!policyString || policyString.length == 0){
+            return policies;
+        }
+
+        var policyArray = policyString.split(" and ");
+
+        for (var i = 0; i < policyArray.length; i ++){
+            var policyToken = policyArray[i];
+            var re = /(\w+)\(*(\d*)\)*/;
+
+            var policyEntry = re.exec(policyToken);
+
+            policies.push({ name: policyEntry[1], value: parseInt(policyEntry[2]) });
+
+        }
+
+        return policies;
+    };
+
+    p.toString = function(policies) {
+        if (!policies || policies.length == 0) {
+            return null;
+        }
+
+        var policyString = "";
+
+        for (var i in policies){
+            policyString += policies[i].name;
+            if ( policies[i].value ){
+                policyString += '(' + policies[i].value + ')';
+            }
+            policyString += " and ";
+        }
+
+        policyString = policyString.substring(0, policyString.length - 5);
+
+        return policyString;
+    };
+
+    return p;
 });
\ No newline at end of file
diff --git a/admin-ui/src/main/resources/META-INF/resources/admin/partials/realm-credentials.html b/admin-ui/src/main/resources/META-INF/resources/admin/partials/realm-credentials.html
index d9e505c..b8352c0 100755
--- a/admin-ui/src/main/resources/META-INF/resources/admin/partials/realm-credentials.html
+++ b/admin-ui/src/main/resources/META-INF/resources/admin/partials/realm-credentials.html
@@ -57,8 +57,8 @@
                                     <div class="actions">
                                         <div class="select-rcue">
                                             <select ng-model="selectedPolicy"
-                                                    ng-options="p.name for p in (allPolicies|remove:policy:'name')"
-                                                    data-ng-change="addPolicy(selectedPolicy); selectedAllPolicies = null">
+                                                    ng-options="(p.name|capitalize) for p in (allPolicies|remove:policy:'name')"
+                                                    data-ng-change="addPolicy(selectedPolicy); selectedPolicy = null">
                                                 <option value="" disabled selected>Add policy...</option>
                                             </select>
                                         </div>
@@ -75,7 +75,7 @@
                             <tr ng-repeat="p in policy">
                                 <td>
                                     <div class="clearfix">
-                                        <input class="input-small disabled" type="text" value="{{p.name}}" readonly>
+                                        <input class="input-small disabled" type="text" value="{{p.name|capitalize}}" readonly>
                                     </div>
                                 </td>
                                 <td>
diff --git a/admin-ui/src/main/resources/META-INF/resources/admin/partials/realm-detail.html b/admin-ui/src/main/resources/META-INF/resources/admin/partials/realm-detail.html
index c71a4a6..2b6d4b8 100755
--- a/admin-ui/src/main/resources/META-INF/resources/admin/partials/realm-detail.html
+++ b/admin-ui/src/main/resources/META-INF/resources/admin/partials/realm-detail.html
@@ -66,6 +66,24 @@
                             <input ng-model="realm.requireSsl" name="requireSsl" id="requireSsl" onoffswitch />
                         </div>
                     </fieldset>
+                    <fieldset>
+                        <legend uncollapsed><span class="text">Optional Settings</span></legend>
+                        <div class="form-group">
+                            <label for="loginTheme">Login Theme</label>
+
+                            <div class="controls">
+                                <select id="loginTheme" name="loginTheme" ng-model="realm.loginTheme" ng-options="t for t in serverInfo.themes.login"></select>
+                            </div>
+                        </div>
+                        <div class="form-group">
+                            <label for="accountTheme">Account Theme</label>
+
+                            <div class="controls">
+                                <select id="accountTheme" name="accountTheme" ng-model="realm.accountTheme" ng-options="t for t in serverInfo.themes.account"></select>
+                            </div>
+                        </div>
+                    </fieldset>
+
 
                     <div class="form-actions" data-ng-show="createRealm">
                         <button type="submit" kc-save class="primary" data-ng-show="changed">Save
diff --git a/admin-ui/src/main/resources/META-INF/resources/admin/partials/realm-social.html b/admin-ui/src/main/resources/META-INF/resources/admin/partials/realm-social.html
index 5a75457..999b6be 100755
--- a/admin-ui/src/main/resources/META-INF/resources/admin/partials/realm-social.html
+++ b/admin-ui/src/main/resources/META-INF/resources/admin/partials/realm-social.html
@@ -38,11 +38,10 @@
                                         <div class="actions">
                                             <div class="select-rcue">
                                                 <select ng-model="newProviderId"
-                                                        ng-options="p as allProviders[p] for p in unsetProviders"
-                                                        placeholder="Please select"></select>
-                                            </div>
-                                            <div>
-                                                <button ng-click="addProvider()" ng-disabled="">Add Provider</button>
+                                                        ng-options="(p|capitalize) for p in (allProviders|remove:configuredProviders)"
+                                                        data-ng-change="addProvider(newProviderId); newProviderId = null">
+                                                    <option value="" disabled selected>Add provider...</option>
+                                                </select>
                                             </div>
                                         </div>
                                     </th>
@@ -58,7 +57,7 @@
                                 <tr ng-repeat="pId in configuredProviders">
                                     <td>
                                         <div class="clearfix">
-                                            <input class="input-small disabled" type="text" value="{{allProviders[pId]}}" readonly>
+                                            <input class="input-small disabled" type="text" value="{{pId|capitalize}}" readonly>
                                         </div>
                                     </td>
                                     <td>
diff --git a/core/src/main/java/org/keycloak/representations/idm/RealmRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/RealmRepresentation.java
index b1fc9be..e31b41c 100755
--- a/core/src/main/java/org/keycloak/representations/idm/RealmRepresentation.java
+++ b/core/src/main/java/org/keycloak/representations/idm/RealmRepresentation.java
@@ -41,6 +41,8 @@ public class RealmRepresentation {
     protected List<OAuthClientRepresentation> oauthClients;
     protected Map<String, String> socialProviders;
     protected Map<String, String> smtpServer;
+    protected String loginTheme;
+    protected String accountTheme;
 
     public String getSelf() {
         return self;
@@ -317,4 +319,20 @@ public class RealmRepresentation {
     public void setRoles(RolesRepresentation roles) {
         this.roles = roles;
     }
+
+    public String getLoginTheme() {
+        return loginTheme;
+    }
+
+    public void setLoginTheme(String loginTheme) {
+        this.loginTheme = loginTheme;
+    }
+
+    public String getAccountTheme() {
+        return accountTheme;
+    }
+
+    public void setAccountTheme(String accountTheme) {
+        this.accountTheme = accountTheme;
+    }
 }
diff --git a/core/src/main/java/org/keycloak/util/ProviderLoader.java b/core/src/main/java/org/keycloak/util/ProviderLoader.java
new file mode 100644
index 0000000..3685b4f
--- /dev/null
+++ b/core/src/main/java/org/keycloak/util/ProviderLoader.java
@@ -0,0 +1,70 @@
+package org.keycloak.util;
+
+import java.util.Iterator;
+import java.util.ServiceLoader;
+
+/**
+ * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+public class ProviderLoader<T> implements Iterable<T> {
+
+    private ServiceLoader<T> serviceLoader;
+
+    public static <T> Iterable<T> load(Class<T> service) {
+        ServiceLoader<T> providers = ServiceLoader.load(service);
+        return new ProviderLoader(providers);
+    }
+
+    private ProviderLoader(ServiceLoader<T> serviceLoader) {
+        this.serviceLoader = serviceLoader;
+    }
+
+    @Override
+    public Iterator iterator() {
+        return new ProviderIterator(serviceLoader.iterator());
+    }
+
+    private static class ProviderIterator<T> implements Iterator<T> {
+
+        private Iterator<T> itr;
+
+        private T next;
+
+        private ProviderIterator(Iterator<T> itr) {
+            this.itr = itr;
+            setNext();
+        }
+
+        @Override
+        public boolean hasNext() {
+            return next != null;
+        }
+
+        @Override
+        public T next() {
+            T n = next;
+            setNext();
+            return n;
+        }
+
+        @Override
+        public void remove() {
+            throw new UnsupportedOperationException();
+        }
+
+        private void setNext() {
+            next = null;
+            while (itr.hasNext()) {
+                if (itr.hasNext()) {
+                    T n = itr.next();
+                    if (!System.getProperties().containsKey(n.getClass().getName() + ".disabled")) {
+                        next = n;
+                        return;
+                    }
+                }
+            }
+        }
+
+    }
+
+}
diff --git a/distribution/appliance-dist/assembly.xml b/distribution/appliance-dist/assembly.xml
index fc2cb01..172942c 100755
--- a/distribution/appliance-dist/assembly.xml
+++ b/distribution/appliance-dist/assembly.xml
@@ -36,6 +36,10 @@
             <outputDirectory>keycloak/standalone/deployments</outputDirectory>
         </fileSet>
         <fileSet>
+            <directory>${project.build.directory}/unpacked/examples/themes</directory>
+            <outputDirectory>keycloak/standalone/configuration/themes</outputDirectory>
+        </fileSet>
+        <fileSet>
             <directory>${project.build.directory}/unpacked/adapter</directory>
             <outputDirectory>keycloak</outputDirectory>
         </fileSet>
diff --git a/distribution/examples-docs-zip/build.xml b/distribution/examples-docs-zip/build.xml
index 584c83f..5f7a9a6 100755
--- a/distribution/examples-docs-zip/build.xml
+++ b/distribution/examples-docs-zip/build.xml
@@ -41,5 +41,8 @@
                 <exclude name="**/*.iml"/>
             </fileset>
         </copy>
+        <copy todir="target/examples/themes" overwrite="true">
+            <fileset dir="../../examples/themes"/>
+        </copy>
     </target>
 </project>
\ No newline at end of file
diff --git a/examples/themes/login/sunrise/resources/css/styles.css b/examples/themes/login/sunrise/resources/css/styles.css
new file mode 100644
index 0000000..bf9d004
--- /dev/null
+++ b/examples/themes/login/sunrise/resources/css/styles.css
@@ -0,0 +1,201 @@
+@import url('../../keycloak/lib/zocial/zocial.css');
+
+body {
+    background-color: #040507;
+    background-image: url('../img/bkgrnd.jpg');
+    background-size: cover;
+    background-repeat: no-repeat;
+
+    color: #ccc;
+    font-family: sans-serif;
+}
+
+a {
+    color: #fff;
+    text-decoration: none;
+}
+
+.content {
+    position: absolute;
+    top: 25%;
+    left: 50%;
+    width: 550px;
+    margin-left: -225px;
+}
+
+h2 {
+    position: fixed;
+    top: 50px;
+    left: 0;
+    width: 100%;
+    text-align: center;
+    margin: 0 auto;
+    color: rgba(255, 255, 255, 0.08);
+    text-shadow: none;
+    font-size: 80px;
+}
+
+div.app-form {
+    float: left;
+    width: 350px;
+}
+
+div.app-form label {
+    display: block;
+    font-size: 16px;
+}
+
+div.social-login {
+    border-left: 1px solid rgba(255, 255, 255, 0.2);
+    float: right;
+    width: 150px;
+    padding: 20px 0 200px 40px;
+}
+
+div.info-area {
+    position: fixed;
+    bottom: 0;
+    left: 0;
+    margin-top: 40px;
+    background-color: rgba(0, 0, 0, 0.4);
+    padding: 20px;
+    width: 100%;
+}
+
+div.info-area p {
+    margin-right: 30px;
+    display: inline;
+    text-shadow: none;
+}
+
+input[type=text], input[type=password] {
+    color: #ddd;
+    font-size: 18px;
+    margin-bottom: 20px;
+    background-color: rgba(3,70,114,0.15);
+    border: 0px solid rgba(0,0,0,0.2);
+    box-shadow:inset 0 0 2px 2px rgba(0,0,0,0.15);
+    padding: 10px;
+    width: 296px;
+}
+
+input[type=text]:hover, input[type=password]:hover {
+    background-color: rgba(3,70,114,0.4);
+}
+
+input[type=submit] {
+    border: none;
+
+    background: -webkit-linear-gradient(top, rgba(255,255,255,0.8), rgba(255,255,255,0.1));
+    background: -moz-linear-gradient(top, rgba(255,255,255,0.8), rgba(255,255,255,0.1));
+    background: -ms-linear-gradient(top, rgba(255,255,255,0.8), rgba(255,255,255,0.1));
+    background: -o-linear-gradient(top, rgba(255,255,255,0.8), rgba(255,255,255,0.1));
+
+    box-shadow: 0px 0px 6px rgba(0,0,0,0.5);
+
+    color: rgba(0,0,0,0.6);
+
+    font-size: 14px;
+    font-weight: bold;
+
+    padding: 10px;
+    margin-top: 20px;
+    margin-right: 10px;
+    width: 150px;
+}
+
+input[type=submit]:hover {
+    background-color: rgba(255,255,255,0.8);
+}
+
+p.powered {
+    font-size: 12px;
+    position: fixed;
+    top: 10px;
+    right: 10px;
+    margin: 0;
+    padding: 0;
+    text-shadow: none;
+}
+
+p.powered a {
+    color: #ccc;
+    text-decoration: none;
+}
+
+div.feedback {
+    box-shadow: 0px 0px 6px rgba(0,0,0,0.5);
+    position: fixed;
+    top: 0;
+    left: 0;
+    width: 100%;
+    text-align: center;
+}
+
+div.success {
+    background-color: rgba(155,155,255,0.1);
+}
+
+div.warning {
+    background-color: rgba(255,175,0,0.1);
+}
+
+div.error {
+    background-color: rgba(255,0,0,0.1);
+}
+
+div.feedback p {
+    margin: 0;
+    padding: 1em;
+}
+
+div.rcue-logo {
+    background-image: url('../img/logo.png');
+    background-repeat: no-repeat;
+    height: 500px;
+    position: absolute;
+    left: 30px;
+    top: 30px;
+    width: 500px;
+    z-index: -1;
+}
+
+div.social-login span {
+    display: none;
+}
+
+div.social-login p {
+    display: none;
+}
+
+div.social-login ul {
+    list-style: none;
+    margin: 0;
+    padding: 0;
+}
+
+div.social-login ul li {
+    margin-bottom: 20px;
+}
+
+div.social-login ul li span {
+    display: inline;
+    width: 100px;
+}
+
+a.zocial {
+    border: none;
+    background: -webkit-linear-gradient(top, rgba(255,255,255,0.8), rgba(255,255,255,0.1)) !important;
+    background: -moz-linear-gradient(top, rgba(255,255,255,0.8), rgba(255,255,255,0.1)) !important;
+    background: -ms-linear-gradient(top, rgba(255,255,255,0.8), rgba(255,255,255,0.1)) !important;
+    background: -o-linear-gradient(top, rgba(255,255,255,0.8), rgba(255,255,255,0.1)) !important;
+    box-shadow: 0px 0px 6px rgba(0,0,0,0.5);
+    color: rgba(0,0,0,0.6);
+    width: 130px;
+    text-shadow: none;
+    -webkit-border-radius: 0;
+    -moz-border-radius: 0;
+    border-radius: 0;
+    padding-top: 0.2em;
+    padding-bottom: 0.2em;
+}
diff --git a/examples/themes/login/sunrise/resources/img/bkgrnd.jpg b/examples/themes/login/sunrise/resources/img/bkgrnd.jpg
new file mode 100644
index 0000000..bc11ffb
Binary files /dev/null and b/examples/themes/login/sunrise/resources/img/bkgrnd.jpg differ
diff --git a/examples/themes/login/sunrise/resources/img/logo.png b/examples/themes/login/sunrise/resources/img/logo.png
new file mode 100644
index 0000000..6293471
Binary files /dev/null and b/examples/themes/login/sunrise/resources/img/logo.png differ
diff --git a/examples/themes/login/sunrise/theme.properties b/examples/themes/login/sunrise/theme.properties
new file mode 100644
index 0000000..458d39c
--- /dev/null
+++ b/examples/themes/login/sunrise/theme.properties
@@ -0,0 +1,2 @@
+parent=base
+styles=css/styles.css
\ No newline at end of file
diff --git a/forms/account-api/pom.xml b/forms/account-api/pom.xml
new file mode 100755
index 0000000..e9eeb1a
--- /dev/null
+++ b/forms/account-api/pom.xml
@@ -0,0 +1,49 @@
+<?xml version="1.0"?>
+<project>
+	<parent>
+		<artifactId>keycloak-forms</artifactId>
+		<groupId>org.keycloak</groupId>
+		<version>1.0-alpha-2-SNAPSHOT</version>
+		<relativePath>../pom.xml</relativePath>
+	</parent>
+	<modelVersion>4.0.0</modelVersion>
+
+	<artifactId>keycloak-account-api</artifactId>
+	<name>Keycloak Account Management API</name>
+	<description />
+
+	<dependencies>
+		<dependency>
+			<groupId>org.keycloak</groupId>
+			<artifactId>keycloak-core</artifactId>
+			<version>${project.version}</version>
+		</dependency>
+        <dependency>
+            <groupId>org.keycloak</groupId>
+            <artifactId>keycloak-model-api</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.jboss.resteasy</groupId>
+            <artifactId>jaxrs-api</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.jboss.resteasy</groupId>
+            <artifactId>resteasy-jaxrs</artifactId>
+        </dependency>
+	</dependencies>
+
+	<build>
+		<plugins>
+			<plugin>
+				<groupId>org.apache.maven.plugins</groupId>
+				<artifactId>maven-compiler-plugin</artifactId>
+				<configuration>
+					<source>1.6</source>
+					<target>1.6</target>
+				</configuration>
+			</plugin>
+		</plugins>
+	</build>
+
+</project>
diff --git a/forms/account-api/src/main/java/org/keycloak/account/Account.java b/forms/account-api/src/main/java/org/keycloak/account/Account.java
new file mode 100644
index 0000000..61e92e2
--- /dev/null
+++ b/forms/account-api/src/main/java/org/keycloak/account/Account.java
@@ -0,0 +1,30 @@
+package org.keycloak.account;
+
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.UserModel;
+
+import javax.ws.rs.core.MultivaluedMap;
+import javax.ws.rs.core.Response;
+
+/**
+ * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+public interface Account {
+
+    public Response createResponse(AccountPages page);
+
+    public Account setError(String message);
+
+    public Account setSuccess(String message);
+
+    public Account setWarning(String message);
+
+    public Account setUser(UserModel user);
+
+    public Account setStatus(Response.Status status);
+
+    public Account setRealm(RealmModel realm);
+
+    public Account setReferrer(String referrer);
+
+}
diff --git a/forms/account-api/src/main/java/org/keycloak/account/AccountLoader.java b/forms/account-api/src/main/java/org/keycloak/account/AccountLoader.java
new file mode 100644
index 0000000..8f9fbec
--- /dev/null
+++ b/forms/account-api/src/main/java/org/keycloak/account/AccountLoader.java
@@ -0,0 +1,17 @@
+package org.keycloak.account;
+
+import java.util.ServiceLoader;
+
+/**
+ * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+public class AccountLoader {
+
+    private AccountLoader() {
+    }
+
+    public static AccountProvider load() {
+        return ServiceLoader.load(AccountProvider.class).iterator().next();
+    }
+
+}
diff --git a/forms/account-api/src/main/java/org/keycloak/account/AccountPages.java b/forms/account-api/src/main/java/org/keycloak/account/AccountPages.java
new file mode 100644
index 0000000..2fc9a29
--- /dev/null
+++ b/forms/account-api/src/main/java/org/keycloak/account/AccountPages.java
@@ -0,0 +1,10 @@
+package org.keycloak.account;
+
+/**
+ * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+public enum AccountPages {
+
+    ACCOUNT, PASSWORD, TOTP;
+
+}
diff --git a/forms/account-api/src/main/java/org/keycloak/account/AccountProvider.java b/forms/account-api/src/main/java/org/keycloak/account/AccountProvider.java
new file mode 100644
index 0000000..37ffe89
--- /dev/null
+++ b/forms/account-api/src/main/java/org/keycloak/account/AccountProvider.java
@@ -0,0 +1,12 @@
+package org.keycloak.account;
+
+import javax.ws.rs.core.UriInfo;
+
+/**
+ * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+public interface AccountProvider {
+
+    public Account createAccount(UriInfo uriInfo);
+
+}
diff --git a/forms/account-freemarker/pom.xml b/forms/account-freemarker/pom.xml
new file mode 100755
index 0000000..d5db947
--- /dev/null
+++ b/forms/account-freemarker/pom.xml
@@ -0,0 +1,70 @@
+<?xml version="1.0"?>
+<project>
+	<parent>
+		<artifactId>keycloak-forms</artifactId>
+		<groupId>org.keycloak</groupId>
+		<version>1.0-alpha-2-SNAPSHOT</version>
+		<relativePath>../pom.xml</relativePath>
+	</parent>
+	<modelVersion>4.0.0</modelVersion>
+
+	<artifactId>keycloak-account-freemarker</artifactId>
+	<name>Keycloak Account Management FreeMarker</name>
+	<description />
+
+	<dependencies>
+		<dependency>
+			<groupId>org.keycloak</groupId>
+			<artifactId>keycloak-core</artifactId>
+			<version>${project.version}</version>
+		</dependency>
+		<dependency>
+			<groupId>org.keycloak</groupId>
+			<artifactId>keycloak-account-api</artifactId>
+			<version>${project.version}</version>
+		</dependency>
+        <dependency>
+            <groupId>org.keycloak</groupId>
+            <artifactId>keycloak-model-api</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.keycloak</groupId>
+            <artifactId>keycloak-forms-common-freemarker</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+		<dependency>
+			<groupId>org.keycloak</groupId>
+			<artifactId>keycloak-services</artifactId>
+			<version>${project.version}</version>
+		</dependency>
+		<dependency>
+			<groupId>org.keycloak</groupId>
+			<artifactId>keycloak-social-core</artifactId>
+			<version>${project.version}</version>
+		</dependency>
+        <dependency>
+            <groupId>org.jboss.resteasy</groupId>
+            <artifactId>resteasy-jaxrs</artifactId>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+			<groupId>org.freemarker</groupId>
+			<artifactId>freemarker</artifactId>
+		</dependency>
+	</dependencies>
+
+	<build>
+		<plugins>
+			<plugin>
+				<groupId>org.apache.maven.plugins</groupId>
+				<artifactId>maven-compiler-plugin</artifactId>
+				<configuration>
+					<source>1.6</source>
+					<target>1.6</target>
+				</configuration>
+			</plugin>
+		</plugins>
+	</build>
+
+</project>
diff --git a/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/FreeMarkerAccount.java b/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/FreeMarkerAccount.java
new file mode 100644
index 0000000..14c4a61
--- /dev/null
+++ b/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/FreeMarkerAccount.java
@@ -0,0 +1,158 @@
+package org.keycloak.account.freemarker;
+
+import org.jboss.resteasy.logging.Logger;
+import org.keycloak.account.Account;
+import org.keycloak.account.AccountPages;
+import org.keycloak.account.freemarker.model.AccountBean;
+import org.keycloak.account.freemarker.model.MessageBean;
+import org.keycloak.account.freemarker.model.TotpBean;
+import org.keycloak.account.freemarker.model.UrlBean;
+import org.keycloak.freemarker.FreeMarkerException;
+import org.keycloak.freemarker.FreeMarkerUtil;
+import org.keycloak.freemarker.Theme;
+import org.keycloak.freemarker.ThemeLoader;
+import org.keycloak.models.ApplicationModel;
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.UserModel;
+
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.UriInfo;
+import java.io.IOException;
+import java.net.URI;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Properties;
+
+/**
+ * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+public class FreeMarkerAccount implements Account {
+
+    private static final Logger logger = Logger.getLogger(FreeMarkerAccount.class);
+
+    private UserModel user;
+    private Response.Status status = Response.Status.OK;
+    private RealmModel realm;
+    private String referrer;
+
+    public static enum MessageType {SUCCESS, WARNING, ERROR}
+
+    private UriInfo uriInfo;
+
+    private String message;
+    private MessageType messageType;
+
+    public FreeMarkerAccount(UriInfo uriInfo) {
+        this.uriInfo = uriInfo;
+    }
+
+    @Override
+    public Response createResponse(AccountPages page) {
+        Map<String, Object> attributes = new HashMap<String, Object>();
+
+        Theme theme;
+        try {
+            theme = ThemeLoader.createTheme(realm.getAccountTheme(), Theme.Type.ACCOUNT);
+        } catch (FreeMarkerException e) {
+            logger.error("Failed to create theme", e);
+            return Response.serverError().build();
+        }
+
+        try {
+            attributes.put("properties", theme.getProperties());
+        } catch (IOException e) {
+            logger.warn("Failed to load properties", e);
+        }
+
+        Properties messages;
+        try {
+            messages = theme.getMessages();
+            attributes.put("rb", messages);
+        } catch (IOException e) {
+            logger.warn("Failed to load messages", e);
+            messages = new Properties();
+        }
+
+        URI baseUri = uriInfo.getBaseUri();
+
+        if (message != null) {
+            attributes.put("message", new MessageBean(messages.containsKey(message) ? messages.getProperty(message) : message, messageType));
+        }
+
+        attributes.put("url", new UrlBean(realm, theme, baseUri, getReferrerUri()));
+
+        switch (page) {
+            case ACCOUNT:
+                attributes.put("account", new AccountBean(user));
+                break;
+            case TOTP:
+                attributes.put("totp", new TotpBean(user, baseUri));
+                break;
+        }
+
+        try {
+            String result = FreeMarkerUtil.processTemplate(attributes, Templates.getTemplate(page), theme);
+            return Response.status(status).type(MediaType.TEXT_HTML).entity(result).build();
+        } catch (FreeMarkerException e) {
+            logger.error("Failed to process template", e);
+            return Response.serverError().build();
+        }
+    }
+
+    private String getReferrerUri() {
+        if (referrer != null) {
+            for (ApplicationModel a : realm.getApplications()) {
+                if (a.getName().equals(referrer)) {
+                    return a.getBaseUrl();
+                }
+            }
+        }
+        return null;
+    }
+
+    @Override
+    public Account setError(String message) {
+        this.message = message;
+        this.messageType = MessageType.ERROR;
+        return this;
+    }
+
+    @Override
+    public Account setSuccess(String message) {
+        this.message = message;
+        this.messageType = MessageType.SUCCESS;
+        return this;
+    }
+
+    @Override
+    public Account setWarning(String message) {
+        this.message = message;
+        this.messageType = MessageType.WARNING;
+        return this;
+    }
+
+    @Override
+    public Account setUser(UserModel user) {
+        this.user = user;
+        return this;
+    }
+
+    @Override
+    public Account setRealm(RealmModel realm) {
+        this.realm = realm;
+        return this;
+    }
+
+    @Override
+    public Account setStatus(Response.Status status) {
+        this.status = status;
+        return this;
+    }
+
+    @Override
+    public Account setReferrer(String referrer) {
+        this.referrer = referrer;
+        return this;
+    }
+}
diff --git a/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/FreeMarkerAccountProvider.java b/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/FreeMarkerAccountProvider.java
new file mode 100644
index 0000000..17d8fb9
--- /dev/null
+++ b/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/FreeMarkerAccountProvider.java
@@ -0,0 +1,18 @@
+package org.keycloak.account.freemarker;
+
+import org.keycloak.account.Account;
+import org.keycloak.account.AccountProvider;
+
+import javax.ws.rs.core.UriInfo;
+
+/**
+ * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+public class FreeMarkerAccountProvider implements AccountProvider {
+
+    @Override
+    public Account createAccount(UriInfo uriInfo) {
+        return new FreeMarkerAccount(uriInfo);
+    }
+
+}
diff --git a/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/model/AccountBean.java b/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/model/AccountBean.java
new file mode 100644
index 0000000..b07d38b
--- /dev/null
+++ b/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/model/AccountBean.java
@@ -0,0 +1,32 @@
+package org.keycloak.account.freemarker.model;
+
+import org.keycloak.models.UserModel;
+
+/**
+ * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+public class AccountBean {
+
+    private UserModel user;
+
+    public AccountBean(UserModel user) {
+        this.user = user;
+    }
+
+    public String getFirstName() {
+        return user.getFirstName();
+    }
+
+    public String getLastName() {
+        return user.getLastName();
+    }
+
+    public String getUsername() {
+        return user.getLoginName();
+    }
+
+    public String getEmail() {
+        return user.getEmail();
+    }
+
+}
diff --git a/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/model/UrlBean.java b/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/model/UrlBean.java
new file mode 100644
index 0000000..46dbb4e
--- /dev/null
+++ b/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/model/UrlBean.java
@@ -0,0 +1,63 @@
+package org.keycloak.account.freemarker.model;
+
+import org.keycloak.freemarker.Theme;
+import org.keycloak.models.RealmModel;
+import org.keycloak.services.resources.flows.Urls;
+
+import java.net.URI;
+
+/**
+ * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+public class UrlBean {
+
+    private String realm;
+    private Theme theme;
+    private URI baseURI;
+    private String referrerURI;
+
+    public UrlBean(RealmModel realm, Theme theme, URI baseURI, String referrerURI) {
+        this.realm = realm.getName();
+        this.theme = theme;
+        this.baseURI = baseURI;
+        this.referrerURI = referrerURI;
+    }
+
+    public String getAccessUrl() {
+        return Urls.accountAccessPage(baseURI, realm).toString();
+    }
+
+    public String getAccountUrl() {
+        return Urls.accountPage(baseURI, realm).toString();
+    }
+
+    public String getPasswordUrl() {
+        return Urls.accountPasswordPage(baseURI, realm).toString();
+    }
+
+    public String getSocialUrl() {
+        return Urls.accountSocialPage(baseURI, realm).toString();
+    }
+
+    public String getTotpUrl() {
+        return Urls.accountTotpPage(baseURI, realm).toString();
+    }
+
+    public String getTotpRemoveUrl() {
+        return Urls.accountTotpRemove(baseURI, realm).toString();
+    }
+
+    public String getLogoutUrl() {
+        return Urls.accountLogout(baseURI, realm).toString();
+    }
+
+    public String getReferrerURI() {
+        return referrerURI;
+    }
+
+    public String getResourcesPath() {
+        URI uri = Urls.themeRoot(baseURI);
+        return uri.getPath() + "/" + theme.getType().toString().toLowerCase() +"/" + theme.getName();
+    }
+
+}
diff --git a/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/Templates.java b/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/Templates.java
new file mode 100644
index 0000000..4e701a3
--- /dev/null
+++ b/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/Templates.java
@@ -0,0 +1,23 @@
+package org.keycloak.account.freemarker;
+
+import org.keycloak.account.AccountPages;
+
+/**
+ * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+public class Templates {
+
+    public static String getTemplate(AccountPages page) {
+        switch (page) {
+            case ACCOUNT:
+                return "account.ftl";
+            case PASSWORD:
+                return "password.ftl";
+            case TOTP:
+                return "totp.ftl";
+            default:
+                throw new IllegalArgumentException();
+        }
+    }
+
+}
diff --git a/forms/account-freemarker/src/main/resources/META-INF/services/org.keycloak.account.AccountProvider b/forms/account-freemarker/src/main/resources/META-INF/services/org.keycloak.account.AccountProvider
new file mode 100644
index 0000000..2f97162
--- /dev/null
+++ b/forms/account-freemarker/src/main/resources/META-INF/services/org.keycloak.account.AccountProvider
@@ -0,0 +1 @@
+org.keycloak.account.freemarker.FreeMarkerAccountProvider
\ No newline at end of file
diff --git a/forms/common-freemarker/pom.xml b/forms/common-freemarker/pom.xml
new file mode 100755
index 0000000..bf850f4
--- /dev/null
+++ b/forms/common-freemarker/pom.xml
@@ -0,0 +1,49 @@
+<?xml version="1.0"?>
+<project>
+	<parent>
+		<artifactId>keycloak-forms</artifactId>
+		<groupId>org.keycloak</groupId>
+		<version>1.0-alpha-2-SNAPSHOT</version>
+		<relativePath>../pom.xml</relativePath>
+	</parent>
+	<modelVersion>4.0.0</modelVersion>
+
+	<artifactId>keycloak-forms-common-freemarker</artifactId>
+	<name>Keycloak Forms Common FreeMarker</name>
+	<description />
+
+	<dependencies>
+        <dependency>
+            <groupId>org.keycloak</groupId>
+            <artifactId>keycloak-core</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.jboss.resteasy</groupId>
+            <artifactId>resteasy-jaxrs</artifactId>
+        </dependency>
+        <dependency>
+			<groupId>org.freemarker</groupId>
+			<artifactId>freemarker</artifactId>
+		</dependency>
+        <dependency>
+            <groupId>junit</groupId>
+            <artifactId>junit</artifactId>
+            <scope>test</scope>
+        </dependency>    
+	</dependencies>
+
+	<build>
+		<plugins>
+			<plugin>
+				<groupId>org.apache.maven.plugins</groupId>
+				<artifactId>maven-compiler-plugin</artifactId>
+				<configuration>
+					<source>1.6</source>
+					<target>1.6</target>
+				</configuration>
+			</plugin>
+		</plugins>
+	</build>
+
+</project>
diff --git a/forms/common-freemarker/src/main/java/org/keycloak/freemarker/FreeMarkerException.java b/forms/common-freemarker/src/main/java/org/keycloak/freemarker/FreeMarkerException.java
new file mode 100644
index 0000000..1af9b20
--- /dev/null
+++ b/forms/common-freemarker/src/main/java/org/keycloak/freemarker/FreeMarkerException.java
@@ -0,0 +1,15 @@
+package org.keycloak.freemarker;
+
+/**
+ * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+public class FreeMarkerException extends Exception {
+
+    public FreeMarkerException(String message) {
+        super(message);
+    }
+
+    public FreeMarkerException(String message, Throwable cause) {
+        super(message, cause);
+    }
+}
diff --git a/forms/common-freemarker/src/main/java/org/keycloak/freemarker/FreeMarkerUtil.java b/forms/common-freemarker/src/main/java/org/keycloak/freemarker/FreeMarkerUtil.java
new file mode 100644
index 0000000..8b1c67c
--- /dev/null
+++ b/forms/common-freemarker/src/main/java/org/keycloak/freemarker/FreeMarkerUtil.java
@@ -0,0 +1,53 @@
+package org.keycloak.freemarker;
+
+import freemarker.cache.URLTemplateLoader;
+import freemarker.template.Configuration;
+import freemarker.template.Template;
+import freemarker.template.TemplateException;
+
+import java.io.IOException;
+import java.io.StringWriter;
+import java.io.Writer;
+import java.net.URL;
+
+/**
+ * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+public class FreeMarkerUtil {
+
+    public static String processTemplate(Object data, String templateName, Theme theme) throws FreeMarkerException {
+        Writer out = new StringWriter();
+        Configuration cfg = new Configuration();
+
+        try {
+            cfg.setTemplateLoader(new ThemeTemplateLoader(theme));
+            Template template = cfg.getTemplate(templateName);
+
+            template.process(data, out);
+        } catch (Exception e) {
+            throw new FreeMarkerException("Failed to process template " + templateName, e);
+        }
+
+        return out.toString();
+    }
+
+    public static class ThemeTemplateLoader extends URLTemplateLoader {
+
+        private Theme theme;
+
+        public ThemeTemplateLoader(Theme theme) {
+            this.theme = theme;
+        }
+
+        @Override
+        protected URL getURL(String name) {
+            try {
+                return theme.getTemplate(name);
+            } catch (IOException e) {
+                return null;
+            }
+        }
+
+    }
+
+}
diff --git a/forms/common-freemarker/src/main/java/org/keycloak/freemarker/Theme.java b/forms/common-freemarker/src/main/java/org/keycloak/freemarker/Theme.java
new file mode 100644
index 0000000..c5c7e36
--- /dev/null
+++ b/forms/common-freemarker/src/main/java/org/keycloak/freemarker/Theme.java
@@ -0,0 +1,33 @@
+package org.keycloak.freemarker;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URL;
+import java.util.Properties;
+
+/**
+ * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+public interface Theme {
+
+    public enum Type { LOGIN, ACCOUNT };
+
+    public String getName();
+
+    public String getParentName();
+
+    public Type getType();
+
+    public URL getTemplate(String name) throws IOException;
+
+    public InputStream getTemplateAsStream(String name) throws IOException;
+
+    public URL getResource(String path) throws IOException;
+
+    public InputStream getResourceAsStream(String path) throws IOException;
+
+    public Properties getMessages() throws IOException;
+
+    public Properties getProperties() throws IOException;
+
+}
diff --git a/forms/common-freemarker/src/main/java/org/keycloak/freemarker/ThemeLoader.java b/forms/common-freemarker/src/main/java/org/keycloak/freemarker/ThemeLoader.java
new file mode 100644
index 0000000..f62b4f9
--- /dev/null
+++ b/forms/common-freemarker/src/main/java/org/keycloak/freemarker/ThemeLoader.java
@@ -0,0 +1,166 @@
+package org.keycloak.freemarker;
+
+import org.jboss.resteasy.logging.Logger;
+import org.keycloak.util.ProviderLoader;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URL;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.ListIterator;
+import java.util.Properties;
+
+/**
+ * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+public class ThemeLoader {
+
+    private static final Logger logger = Logger.getLogger(ThemeLoader.class);
+    public static final String BASE = "base";
+    public static String DEFAULT = BASE;
+
+    public static Theme createTheme(String name, Theme.Type type) throws FreeMarkerException {
+        if (name == null) {
+            name = DEFAULT;
+        }
+
+        Iterable<ThemeProvider> providers = ProviderLoader.load(ThemeProvider.class);
+
+        Theme theme = findTheme(providers, name, type);
+        if (theme.getParentName() != null) {
+            List<Theme> themes = new LinkedList<Theme>();
+            themes.add(theme);
+
+            for (String parentName = theme.getParentName(); parentName != null; parentName = theme.getParentName()) {
+                theme = findTheme(providers, parentName, type);
+                themes.add(theme);
+            }
+
+            return new ExtendingTheme(themes);
+        } else {
+            return theme;
+        }
+    }
+
+    private static Theme findTheme(Iterable<ThemeProvider> providers, String name, Theme.Type type) throws FreeMarkerException {
+        for (ThemeProvider p : providers) {
+            if (p.hasTheme(name, type)) {
+                try {
+                    return p.createTheme(name, type);
+                } catch (IOException e) {
+                    if (name.equals(BASE)) {
+                        throw new FreeMarkerException("Failed to create " + type.toString().toLowerCase() + " theme", e);
+                    } else {
+                        logger.error("Failed to create " + type.toString().toLowerCase() + " theme", e);
+                        return findTheme(providers, BASE, type);
+                    }
+                }
+            }
+        }
+
+        if (name.equals(BASE)) {
+            throw new FreeMarkerException(type.toString().toLowerCase() + " theme '" + name + "' not found");
+        } else {
+            logger.error(type.toString().toLowerCase() + " theme '" + name + "' not found");
+            return findTheme(providers, BASE, type);
+        }
+    }
+
+    public static class ExtendingTheme implements Theme {
+
+        private List<Theme> themes;
+
+        public ExtendingTheme(List<Theme> themes) {
+            this.themes = themes;
+        }
+
+        @Override
+        public String getName() {
+            return themes.get(0).getName();
+        }
+
+        @Override
+        public String getParentName() {
+            return themes.get(0).getParentName();
+        }
+
+        @Override
+        public Type getType() {
+            return themes.get(0).getType();
+        }
+
+        @Override
+        public URL getTemplate(String name) throws IOException {
+            for (Theme t : themes) {
+                URL template = t.getTemplate(name);
+                if (template != null) {
+                    return template;
+                }
+            }
+            return null;
+        }
+
+        @Override
+        public InputStream getTemplateAsStream(String name) throws IOException {
+            for (Theme t : themes) {
+                InputStream template = t.getTemplateAsStream(name);
+                if (template != null) {
+                    return template;
+                }
+            }
+            return null;
+        }
+
+
+        @Override
+        public URL getResource(String path) throws IOException {
+            for (Theme t : themes) {
+                URL resource = t.getResource(path);
+                if (resource != null) {
+                    return resource;
+                }
+            }
+            return null;
+        }
+
+        @Override
+        public InputStream getResourceAsStream(String path) throws IOException {
+            for (Theme t : themes) {
+                InputStream resource = t.getResourceAsStream(path);
+                if (resource != null) {
+                    return resource;
+                }
+            }
+            return null;
+        }
+
+        @Override
+        public Properties getMessages() throws IOException {
+            Properties messages = new Properties();
+            ListIterator<Theme> itr = themes.listIterator(themes.size());
+            while (itr.hasPrevious()) {
+                Properties m = itr.previous().getMessages();
+                if (m != null) {
+                    messages.putAll(m);
+                }
+            }
+            return messages;
+        }
+
+        @Override
+        public Properties getProperties() throws IOException {
+            Properties properties = new Properties();
+            ListIterator<Theme> itr = themes.listIterator(themes.size());
+            while (itr.hasPrevious()) {
+                Properties p = itr.previous().getProperties();
+                if (p != null) {
+                    properties.putAll(p);
+                }
+            }
+            return properties;
+        }
+
+    }
+
+}
diff --git a/forms/common-freemarker/src/main/java/org/keycloak/freemarker/ThemeProvider.java b/forms/common-freemarker/src/main/java/org/keycloak/freemarker/ThemeProvider.java
new file mode 100644
index 0000000..31de058
--- /dev/null
+++ b/forms/common-freemarker/src/main/java/org/keycloak/freemarker/ThemeProvider.java
@@ -0,0 +1,17 @@
+package org.keycloak.freemarker;
+
+import java.io.IOException;
+import java.util.Set;
+
+/**
+ * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+public interface ThemeProvider {
+
+    public Theme createTheme(String name, Theme.Type type) throws IOException;
+
+    public Set<String> nameSet(Theme.Type type);
+
+    public boolean hasTheme(String name, Theme.Type type);
+
+}
diff --git a/forms/common-themes/pom.xml b/forms/common-themes/pom.xml
new file mode 100755
index 0000000..0e6766f
--- /dev/null
+++ b/forms/common-themes/pom.xml
@@ -0,0 +1,54 @@
+<?xml version="1.0"?>
+<project>
+	<parent>
+		<artifactId>keycloak-forms</artifactId>
+		<groupId>org.keycloak</groupId>
+		<version>1.0-alpha-2-SNAPSHOT</version>
+		<relativePath>../pom.xml</relativePath>
+	</parent>
+	<modelVersion>4.0.0</modelVersion>
+
+	<artifactId>keycloak-forms-common-themes</artifactId>
+	<name>Keycloak Login Default Theme</name>
+	<description />
+
+	<dependencies>
+		<dependency>
+			<groupId>org.keycloak</groupId>
+			<artifactId>keycloak-core</artifactId>
+			<version>${project.version}</version>
+		</dependency>
+        <dependency>
+            <groupId>org.keycloak</groupId>
+            <artifactId>keycloak-model-api</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.keycloak</groupId>
+            <artifactId>keycloak-forms-common-freemarker</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.jboss.resteasy</groupId>
+            <artifactId>jaxrs-api</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.jboss.resteasy</groupId>
+            <artifactId>resteasy-jaxrs</artifactId>
+        </dependency>
+	</dependencies>
+
+	<build>
+		<plugins>
+			<plugin>
+				<groupId>org.apache.maven.plugins</groupId>
+				<artifactId>maven-compiler-plugin</artifactId>
+				<configuration>
+					<source>1.6</source>
+					<target>1.6</target>
+				</configuration>
+			</plugin>
+		</plugins>
+	</build>
+
+</project>
diff --git a/forms/common-themes/src/main/java/org/keycloak/theme/ClassLoaderTheme.java b/forms/common-themes/src/main/java/org/keycloak/theme/ClassLoaderTheme.java
new file mode 100644
index 0000000..d63dc7d
--- /dev/null
+++ b/forms/common-themes/src/main/java/org/keycloak/theme/ClassLoaderTheme.java
@@ -0,0 +1,99 @@
+package org.keycloak.theme;
+
+import org.keycloak.freemarker.Theme;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URL;
+import java.util.Properties;
+
+/**
+ * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+public class ClassLoaderTheme implements Theme {
+
+    private final String name;
+
+    private final String parentName;
+
+    private final Type type;
+
+    private final String templateRoot;
+
+    private final String resourceRoot;
+
+    private final String messages;
+
+    private final Properties properties;
+
+    public ClassLoaderTheme(String name, Type type) throws IOException {
+        this.name = name;
+        this.type = type;
+
+        String themeRoot = "theme/" + type.toString().toLowerCase() + "/" + name + "/";
+
+        this.templateRoot = themeRoot;
+        this.resourceRoot = themeRoot + "resources/";
+        this.messages = themeRoot + "messages/messages.properties";
+        this.properties = new Properties();
+
+        URL p = getClass().getClassLoader().getResource(themeRoot + "theme.properties");
+        if (p != null) {
+            properties.load(p.openStream());
+            this.parentName = properties.getProperty("parent");
+        } else {
+            this.parentName = null;
+        }
+    }
+
+    @Override
+    public String getName() {
+        return name;
+    }
+
+    @Override
+    public String getParentName() {
+        return parentName;
+    }
+
+    @Override
+    public Type getType() {
+        return type;
+    }
+
+    @Override
+    public URL getTemplate(String name) {
+        return getClass().getClassLoader().getResource(templateRoot + name);
+    }
+
+    @Override
+    public InputStream getTemplateAsStream(String name) {
+        return getClass().getClassLoader().getResourceAsStream(templateRoot + name);
+    }
+
+    @Override
+    public URL getResource(String path) {
+        return getClass().getClassLoader().getResource(resourceRoot + path);
+    }
+
+    @Override
+    public InputStream getResourceAsStream(String path) {
+        return getClass().getClassLoader().getResourceAsStream(resourceRoot + path);
+    }
+
+    @Override
+    public Properties getMessages() throws IOException {
+        Properties m = new Properties();
+        URL url = getClass().getClassLoader().getResource(this.messages);
+        if (url != null) {
+            m.load(url.openStream());
+        }
+        return m;
+    }
+
+    @Override
+    public Properties getProperties() {
+        return properties;
+    }
+
+}
diff --git a/forms/common-themes/src/main/java/org/keycloak/theme/DefaultLoginThemeProvider.java b/forms/common-themes/src/main/java/org/keycloak/theme/DefaultLoginThemeProvider.java
new file mode 100644
index 0000000..7d98d3d
--- /dev/null
+++ b/forms/common-themes/src/main/java/org/keycloak/theme/DefaultLoginThemeProvider.java
@@ -0,0 +1,55 @@
+package org.keycloak.theme;
+
+import org.keycloak.freemarker.Theme;
+import org.keycloak.freemarker.ThemeLoader;
+import org.keycloak.freemarker.ThemeProvider;
+
+import java.io.IOException;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+public class DefaultLoginThemeProvider implements ThemeProvider {
+
+    public static final String RCUE = "rcue";
+    public static final String KEYCLOAK = "keycloak";
+
+    static {
+        ThemeLoader.DEFAULT = KEYCLOAK;
+    }
+
+    private static Set<String> defaultThemes = new HashSet<String>();
+
+    static {
+        defaultThemes.add(ThemeLoader.BASE);
+        defaultThemes.add(RCUE);
+        defaultThemes.add(KEYCLOAK);
+    }
+
+    @Override
+    public Theme createTheme(String name, Theme.Type type) throws IOException {
+        if (hasTheme(name, type)) {
+            return new ClassLoaderTheme(name, type);
+        } else {
+            return null;
+        }
+    }
+
+    @Override
+    public Set<String> nameSet(Theme.Type type) {
+        if (type == Theme.Type.LOGIN || type == Theme.Type.ACCOUNT) {
+            return defaultThemes;
+        } else {
+            return Collections.emptySet();
+        }
+    }
+
+    @Override
+    public boolean hasTheme(String name, Theme.Type type) {
+        return nameSet(type).contains(name);
+    }
+
+}
diff --git a/forms/common-themes/src/main/java/org/keycloak/theme/FolderTheme.java b/forms/common-themes/src/main/java/org/keycloak/theme/FolderTheme.java
new file mode 100644
index 0000000..3737aa1
--- /dev/null
+++ b/forms/common-themes/src/main/java/org/keycloak/theme/FolderTheme.java
@@ -0,0 +1,90 @@
+package org.keycloak.theme;
+
+import org.keycloak.freemarker.Theme;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URL;
+import java.util.Properties;
+
+/**
+ * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+public class FolderTheme implements Theme {
+
+    private String parentName;
+    private File themeDir;
+    private Type type;
+    private final Properties properties;
+
+    public FolderTheme(File themeDir, Type type) throws IOException {
+        this.themeDir = themeDir;
+        this.type = type;
+        this.properties = new Properties();
+
+        File propertiesFile = new File(themeDir, "theme.properties");
+        if (propertiesFile .isFile()) {
+            properties.load(new FileInputStream(propertiesFile));
+            parentName = properties.getProperty("parent");
+        }
+    }
+
+    @Override
+    public String getName() {
+        return themeDir.getName();
+    }
+
+    @Override
+    public String getParentName() {
+        return parentName;
+    }
+
+    @Override
+    public Type getType() {
+        return type;
+    }
+
+    @Override
+    public URL getTemplate(String name) throws IOException {
+        File file = new File(themeDir, name);
+        return file.isFile() ? file.toURI().toURL() : null;
+    }
+
+    @Override
+    public InputStream getTemplateAsStream(String name) throws IOException {
+        URL url = getTemplate(name);
+        return url != null ? url.openStream() : null;
+    }
+
+    @Override
+    public URL getResource(String path) throws IOException {
+        if (File.separatorChar != '/') {
+            path = path.replace('/', File.separatorChar);
+        }
+        File file = new File(themeDir, "/resources/" + path);
+        return file.isFile() ? file.toURI().toURL() : null;
+    }
+
+    @Override
+    public InputStream getResourceAsStream(String path) throws IOException {
+        URL url = getResource(path);
+        return url != null ? url.openStream() : null;
+    }
+
+    @Override
+    public Properties getMessages() throws IOException {
+        Properties m = new Properties();
+        File file = new File(themeDir, "messages" + File.separator + "messages.properties");
+        if (file.isFile()) {
+            m.load(new FileInputStream(file));
+        }
+        return m;
+    }
+
+    @Override
+    public Properties getProperties() {
+        return properties;
+    }
+}
diff --git a/forms/common-themes/src/main/java/org/keycloak/theme/FolderThemeProvider.java b/forms/common-themes/src/main/java/org/keycloak/theme/FolderThemeProvider.java
new file mode 100644
index 0000000..0d328b2
--- /dev/null
+++ b/forms/common-themes/src/main/java/org/keycloak/theme/FolderThemeProvider.java
@@ -0,0 +1,72 @@
+package org.keycloak.theme;
+
+import org.keycloak.freemarker.Theme;
+import org.keycloak.freemarker.ThemeProvider;
+
+import java.io.File;
+import java.io.FileFilter;
+import java.io.IOException;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+public class FolderThemeProvider implements ThemeProvider {
+
+    private File rootDir;
+
+    public FolderThemeProvider() {
+        String d = System.getProperty("keycloak.theme.dir");
+        if (d != null) {
+            rootDir = new File(d);
+        }
+    }
+
+    @Override
+    public Theme createTheme(String name, Theme.Type type) throws IOException {
+        if (hasTheme(name, type)) {
+            return new FolderTheme(new File(getTypeDir(type), name), type);
+        }
+        return null;
+    }
+
+    @Override
+    public Set<String> nameSet(Theme.Type type) {
+        File typeDir = getTypeDir(type);
+        if (typeDir != null) {
+            File[] themes = typeDir.listFiles(new FileFilter() {
+                @Override
+                public boolean accept(File pathname) {
+                    return pathname.isDirectory();
+                }
+            });
+
+            Set<String> names = new HashSet<String>();
+            for (File t : themes) {
+                names.add(t.getName());
+            }
+            return names;
+        }
+
+        return Collections.emptySet();
+    }
+
+    private File getTypeDir(Theme.Type type) {
+        if (rootDir != null && rootDir.isDirectory()) {
+            File typeDir = new File(rootDir, type.name().toLowerCase());
+            if (typeDir.isDirectory()) {
+                return typeDir;
+            }
+        }
+        return null;
+    }
+
+    @Override
+    public boolean hasTheme(String name, Theme.Type type) {
+        File typeDir = getTypeDir(type);
+        return typeDir != null && new File(typeDir, name).isDirectory();
+    }
+
+}
diff --git a/forms/common-themes/src/main/resources/META-INF/services/org.keycloak.freemarker.ThemeProvider b/forms/common-themes/src/main/resources/META-INF/services/org.keycloak.freemarker.ThemeProvider
new file mode 100644
index 0000000..8a9390d
--- /dev/null
+++ b/forms/common-themes/src/main/resources/META-INF/services/org.keycloak.freemarker.ThemeProvider
@@ -0,0 +1,2 @@
+org.keycloak.theme.DefaultLoginThemeProvider
+org.keycloak.theme.FolderThemeProvider
\ No newline at end of file
diff --git a/forms/common-themes/src/main/resources/theme/account/base/messages/messages.properties b/forms/common-themes/src/main/resources/theme/account/base/messages/messages.properties
new file mode 100644
index 0000000..c53e7bc
--- /dev/null
+++ b/forms/common-themes/src/main/resources/theme/account/base/messages/messages.properties
@@ -0,0 +1,27 @@
+authenticatorCode=One-time-password
+email=Email
+errorHeader=Error!
+firstName=First name
+lastName=Last name
+password=Password
+passwordConfirm=Password confirmation
+passwordNew=New Password
+successHeader=Success!
+username=Username
+
+missingFirstName=Please specify first name
+missingLastName=Please specify last name
+missingEmail=Please specify email
+missingPassword=Please specify password.
+notMatchPassword=Passwords don't match
+
+missingTotp=Please specify authenticator code
+invalidPasswordExisting=Invalid existing password
+invalidPasswordConfirm=Password confirmation doesn't match
+invalidTotp=Invalid authenticator code
+
+successTotp=Google authenticator configured.
+successTotpRemoved=Google authenticator removed.
+
+accountUpdated=Your account has been updated
+accountPasswordUpdated=Your password has been updated
\ No newline at end of file
diff --git a/forms/common-themes/src/main/resources/theme/account/keycloak/resources/css/styles.css b/forms/common-themes/src/main/resources/theme/account/keycloak/resources/css/styles.css
new file mode 100644
index 0000000..2df50df
--- /dev/null
+++ b/forms/common-themes/src/main/resources/theme/account/keycloak/resources/css/styles.css
@@ -0,0 +1,13 @@
+@IMPORT url("../../rcue/css/styles.css");
+
+.header.rcue {
+    border-top: none !important;
+}
+
+.header.rcue .navbar.utility {
+    background: #083556 !important;
+}
+
+.header.rcue .navbar-inner {
+    background: #083556 !important;
+}
diff --git a/forms/common-themes/src/main/resources/theme/account/keycloak/theme.properties b/forms/common-themes/src/main/resources/theme/account/keycloak/theme.properties
new file mode 100644
index 0000000..9898eae
--- /dev/null
+++ b/forms/common-themes/src/main/resources/theme/account/keycloak/theme.properties
@@ -0,0 +1,2 @@
+parent=rcue
+styles=../rcue/css/styles.css css/styles.css
\ No newline at end of file
diff --git a/forms/common-themes/src/main/resources/theme/account/rcue/resources/css/styles.css b/forms/common-themes/src/main/resources/theme/account/rcue/resources/css/styles.css
new file mode 100644
index 0000000..41a9d55
--- /dev/null
+++ b/forms/common-themes/src/main/resources/theme/account/rcue/resources/css/styles.css
@@ -0,0 +1,14 @@
+@IMPORT url("reset.css");
+
+@IMPORT url("../lib/bootstrap/css/bootstrap.css");
+@IMPORT url("sprites.css");
+
+@IMPORT url("http://fonts.googleapis.com/css?family=Open+Sans:400,300,300italic,400italic,600,600italic,700,700italic,800,800italic");
+
+@IMPORT url("base.css");
+@IMPORT url("forms.css");
+@IMPORT url("header.css");
+@IMPORT url("icons.css");
+@IMPORT url("tables.css");
+
+@IMPORT url("admin-console.css");
\ No newline at end of file
diff --git a/forms/common-themes/src/main/resources/theme/account/rcue/theme.properties b/forms/common-themes/src/main/resources/theme/account/rcue/theme.properties
new file mode 100644
index 0000000..298aa65
--- /dev/null
+++ b/forms/common-themes/src/main/resources/theme/account/rcue/theme.properties
@@ -0,0 +1,2 @@
+parent=base
+styles=css/styles.css
diff --git a/forms/common-themes/src/main/resources/theme/login/keycloak/resources/css/styles.css b/forms/common-themes/src/main/resources/theme/login/keycloak/resources/css/styles.css
new file mode 100644
index 0000000..5988a48
--- /dev/null
+++ b/forms/common-themes/src/main/resources/theme/login/keycloak/resources/css/styles.css
@@ -0,0 +1,4 @@
+body.rcue-login-register {
+    background: #083556;
+    background-image: none;
+}
diff --git a/forms/common-themes/src/main/resources/theme/login/keycloak/theme.properties b/forms/common-themes/src/main/resources/theme/login/keycloak/theme.properties
new file mode 100644
index 0000000..9898eae
--- /dev/null
+++ b/forms/common-themes/src/main/resources/theme/login/keycloak/theme.properties
@@ -0,0 +1,2 @@
+parent=rcue
+styles=../rcue/css/styles.css css/styles.css
\ No newline at end of file
diff --git a/forms/common-themes/src/main/resources/theme/login/rcue/resources/css/base.css b/forms/common-themes/src/main/resources/theme/login/rcue/resources/css/base.css
new file mode 100644
index 0000000..f258d8f
--- /dev/null
+++ b/forms/common-themes/src/main/resources/theme/login/rcue/resources/css/base.css
@@ -0,0 +1,52 @@
+* {
+  -moz-box-sizing: border-box;
+  -o-box-sizing: border-box;
+  -webkit-box-sizing: border-box;
+  box-sizing: border-box;
+  margin: 0;
+  padding: 0;
+  font-family: "Open Sans", sans-serif;
+}
+body {
+  height: 100%;
+  width: 100%;
+  font-family: "Open Sans", sans-serif;
+}
+h1,
+h2,
+h3,
+h4,
+h5,
+h6 {
+  font-weight: normal;
+  font-family: "Overpass", sans-serif;
+}
+a {
+  color: #0099d3;
+  text-decoration: none;
+}
+a:hover {
+  text-decoration: underline;
+}
+/* Styles from Gabriel */
+strong {
+  font-weight: bold;
+}
+.hidden {
+  display: none;
+}
+.feedback.show {
+  display: inline-block !important;
+}
+.pull-right {
+  float: right;
+}
+.block {
+  display: block;
+}
+a:focus {
+  outline: 0 none;
+}
+.clear-font-size {
+  font-size: 1em;
+}
diff --git a/forms/common-themes/src/main/resources/theme/login/rcue/resources/css/forms.css b/forms/common-themes/src/main/resources/theme/login/rcue/resources/css/forms.css
new file mode 100644
index 0000000..c51693c
--- /dev/null
+++ b/forms/common-themes/src/main/resources/theme/login/rcue/resources/css/forms.css
@@ -0,0 +1,515 @@
+fieldset {
+  border: none;
+}
+fieldset.border-top {
+    border-color: #E9E8E8;
+    border-style: solid;
+    border-width: 1px 0 0;
+    padding-top: 2em;
+}
+*::-moz-placeholder,
+::-webkit-input-placeholder {
+  color: #838383;
+  font-style: italic;
+}
+input[type="text"],
+input[type="password"],
+input[type="email"] {
+  font-size: 1.1em;
+  padding: 0 0.545454545454545em;
+  min-width: 18.1818181818182em;
+  height: 2.36363636363636em;
+  border: 1px #b6b6b6 solid;
+  border-radius: 2px;
+  box-shadow: inset 0px 2px 2px rgba(0, 0, 0, 0.1);
+  color: #333;
+}
+input[type="text"]:hover,
+input[type="password"]:hover,
+input[type="email"]:hover {
+  border-color: #62afdb;
+}
+input[type="text"]:focus,
+input[type="password"]:focus,
+input[type="email"]:focus {
+  border-color: #62afdb;
+  box-shadow: #62afdb 0 0 5px;
+}
+input[type="text"].error,
+input[type="password"].error,
+input[type="email"].error {
+  border-color: #ba1212;
+  transition: all 0.33s ease-in-out;
+  -moz-transition: all 0.33s ease-in-out;
+  -webkit-transition: all 0.33s ease-in-out;
+}
+input[type="text"].error:focus,
+input[type="password"].error:focus,
+input[type="email"].error:focus {
+  box-shadow: 0 0 5px #ba1212;
+}
+input[type="button"],
+button,
+input[type="submit"],
+a.button {
+  font-size: 1.3em;
+  padding: 0.30769230769231em 1.07692307692308em;
+  border-width: 1px;
+  border-radius: 2px;
+  color: #fff;
+  font-weight: bold;
+  letter-spacing: 0.04em;
+  box-shadow: 0px 2px 2px rgba(0, 0, 0, 0.15);
+}
+input[type="button"].btn-primary,
+button.btn-primary,
+input[type="submit"].btn-primary,
+a.button.btn-primary {
+  background-image: linear-gradient(top, #00a9ec 0%, #009bd3 100%);
+  background-image: -o-linear-gradient(top, #00a9ec 0%, #009bd3 100%);
+  background-image: -moz-linear-gradient(top, #00a9ec 0%, #009bd3 100%);
+  background-image: -webkit-linear-gradient(top, #00a9ec 0%, #009bd3 100%);
+  background-image: -ms-linear-gradient(top, #00a9ec 0%, #009bd3 100%);
+  background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0, #00a9ec), color-stop(1, 0, #009bd3));
+  border-color: #21799e;
+  border-style: solid;
+}
+input[type="button"].btn-primary:hover,
+input[type="submit"].btn-primary:hover,
+button.btn-primary:hover,
+a.button.btn-primary:hover,
+input[type="button"].btn-primary:focus,
+input[type="submit"].btn-primary:focus,
+button.btn-primary:focus,
+a.button.btn-primary:focus {
+  background-color: #009BD3;
+}
+input[type="button"].btn-primary:active,
+input[type="submit"].btn-primary:active,
+button.btn-primary:active,
+a.button.btn-primary:active {
+  background-color: #0099d4;
+}
+input[type="button"].disabled,
+input[type="submit"].disabled,
+button.disabled,
+a.button.disabled {
+  border-color: #cfcdcd;
+  color: #838383;
+  background-color: transparent;
+  background-image: none;
+  box-shadow: none;
+  font-weight: normal;
+  letter-spacing: 0.06363636363636em;
+}
+
+input[type="button"].btn-secondary,
+button.btn-secondary,
+input[type="submit"].btn-secondary,
+a.button.btn-secondary {
+    background-color: #EEEEEE;
+    border-color: #BBBBBB;
+    color: #4D5258;
+}
+
+input[type="button"].disabled:hover,
+input[type="submit"].disabled:hover,
+button.disabled:hover,
+a.button.disabled:hover {
+  cursor: default;
+}
+input[type="button"].disabled:active,
+input[type="submit"].disabled:active,
+button.disabled:active,
+a.button.disabled:active {
+  box-shadow: none;
+}
+input[type="button"]:hover,
+input[type="submit"]:hover,
+button:hover,
+a.button:hover,
+input[type="button"]:focus,
+input[type="submit"]:focus,
+button:focus,
+a.button:focus {
+  background-image: none;
+  cursor: pointer;
+}
+input[type="button"]:active,
+input[type="submit"]:active,
+button:active,
+a.button:active {
+  background-image: none;
+  cursor: pointer;
+  box-shadow: inset 0 0 5px 2px rgba(0, 0, 0, 0.25);
+}
+input[type="checkbox"] {
+  margin-right: 0.5em;
+}
+/* Code from Hylke */
+button,
+a.button {
+  border-color: #21799e;
+  background-image: linear-gradient(top, #fafafa 0%, #ededed 100%);
+  background-image: -o-linear-gradient(top, #fafafa 0%, #ededed 100%);
+  background-image: -moz-linear-gradient(top, #fafafa 0%, #ededed 100%);
+  background-image: -webkit-linear-gradient(top, #fafafa 0%, #ededed 100%);
+  background-image: -ms-linear-gradient(top, #fafafa 0%, #ededed 100%);
+  background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0, #fafafa), color-stop(1, 0, #ededed));
+  color: #fff;
+  padding: 4px 14px;
+  border: 1px #bbb solid;
+  border-radius: 2px;
+  color: #4d5258;
+  font-weight: bold;
+  font-size: 1.1em;
+  letter-spacing: 0.4px;
+  cursor: pointer;
+  padding-top: 0;
+  padding-bottom: 0;
+  line-height: 2.18181818181818em;
+}
+input[type='submit'].primary,
+button.primary {
+  border-color: #21799e;
+  background-image: linear-gradient(top, #00a9ec 0%, #009bd3 100%);
+  background-image: -o-linear-gradient(top, #00a9ec 0%, #009bd3 100%);
+  background-image: -moz-linear-gradient(top, #00a9ec 0%, #009bd3 100%);
+  background-image: -webkit-linear-gradient(top, #00a9ec 0%, #009bd3 100%);
+  background-image: -ms-linear-gradient(top, #00a9ec 0%, #009bd3 100%);
+  background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0, #00a9ec), color-stop(1, 0, #009bd3));
+  color: #fff;
+}
+button.primary:hover,
+button.primary:focus {
+  background-color: #009BD3;
+}
+button.primary:enabled:active {
+  background-color: #0099d4;
+  box-shadow: inset 0 0 5px 3px #0074ae;
+}
+/* Code from Gabriel */
+.rcue-login-register.register .two-fields input[type="text"] {
+  width: 121px;
+  min-width: 0;
+}
+.rcue-login-register.register .two-fields input + input {
+  margin-left: 10px;
+}
+.search-comp {
+  position: relative;
+  display: inline-block;
+  font-size: 0.90909090909091em;
+}
+.search-comp input[type="text"] {
+  padding-right: 2.45454545454545em;
+}
+.search-comp .icon-search {
+  position: absolute;
+  right: 0.2em;
+  top: 0.4em;
+  opacity: 0.5;
+}
+.search-comp .icon-search:hover {
+  opacity: 1;
+  -webkit-transition: ease-in-out opacity 0.25s;
+  -moz-transition: ease-in-out opacity 0.25s;
+  -o-transition: ease-in-out opacity 0.25s;
+  transition: ease-in-out opacity 0.25s;
+}
+.search-comp .icon-search + .tooltip {
+  width: 20em;
+  font-weight: normal;
+}
+.feedback-aligner {
+  position: absolute;
+  top: 1.5em;
+  text-align: center;
+  width: 100%;
+  height: 0;
+  z-index: 100;
+}
+.feedback-aligner .feedback {
+  position: relative;
+  display: inline-block;
+  text-align: left;
+  border-width: 1px;
+}
+.feedback-aligner .feedback p {
+  border-width: 1px;
+}
+.feedback {
+  position: absolute;
+  opacity: 0;
+  transition: opacity 0.33s ease-in-out;
+  -moz-transition: opacity 0.33s ease-in-out;
+  -webkit-transition: opacity 0.33s ease-in-out;
+}
+.feedback p {
+  padding: 0.90909090909091em 3.63636363636364em;
+  border-style: solid;
+  border-width: 1px 1px 0px 1px;
+  background-repeat: no-repeat;
+  background-position: 1.2em center;
+  font-size: 1.1em;
+  line-height: 1.4em;
+  border-radius: 2px;
+  color: #4d5258;
+  margin-bottom: 0;
+}
+.feedback.show {
+  opacity: 1;
+}
+.feedback.bottom-left {
+  background-position: left bottom;
+  background-repeat: no-repeat;
+  padding-bottom: 1em;
+}
+.feedback.bottom-left p {
+  background-position: 1.27272727272727em center;
+}
+.feedback.error {
+  background-image: url(../img/feedback-error-arrow-down.png);
+}
+.feedback.error p {
+  border-color: #b91415;
+  background-image: url(../img/feedback-error-sign.png);
+  background-color: #f8e7e7;
+}
+.feedback.success {
+  background-image: url(../img/feedback-success-arrow-down.png);
+}
+.feedback.success p {
+  border-color: #4b9e39;
+  background-image: url(../img/feedback-success-sign.png);
+  background-color: #e4f1e1;
+}
+.feedback.warning {
+    background-image: url(../img/feedback-warning-arrow-down.png);
+}
+.feedback.warning p {
+  border-color: #f17528;
+  background-image: url(../img/feedback-warning-sign.png);
+  background-color: #fef1e9;
+}
+button,
+a.button {
+  background-color: #eeeeee;
+}
+a.button {
+  display: inline-block;
+}
+a.button:hover {
+  color: #4D5258;
+  text-decoration: none;
+}
+button[class^="icon-"] {
+  border: none;
+  box-shadow: none;
+  background-color: transparent;
+  padding: 0;
+  line-height: 1em;
+}
+
+legend {
+  font-size: 1em;
+  border-width: 1px 0 0 0;
+  border-style: solid;
+  border-color: #e9e8e8;
+  padding-top: 2em;
+  display: block;
+  margin-bottom: 0;
+  padding-bottom: 0.8em;
+  cursor: pointer;
+}
+legend .icon-collapse {
+  vertical-align: baseline;
+}
+legend .text {
+  font-weight: bold;
+  font-size: 1.25em;
+}
+
+.form-group {
+  display: block;
+  margin-bottom: 1em;
+  position: relative;
+}
+.form-group > label {
+  font-size: 1.1em;
+  font-weight: 300;
+  width: 10em;
+  margin-right: 0.90909090909091em;
+  margin-bottom: 0;
+  float: left;
+  margin-top: 0.45454545454545em;
+}
+.form-group > label.two-lines {
+  margin-top: -2px;
+}
+.form-group > label + span {
+  font-size: 1.1em;
+  display: inline-block;
+  margin-top: 0.454545454545455em;
+}
+.form-group > label + .onoffswitch {
+  float: left;
+}
+.form-group > label.pull-left {
+  margin-top: 4px;
+}
+.form-group .required {
+  position: absolute;
+  left: 10em;
+  font-size: 1.1em;
+  color: #CB2915;
+}
+legend + .form-group {
+  padding-top: 1em;
+}
+legend + table {
+  margin-top: 1em;
+}
+.code {
+  font-family: Courier, monospace;
+}
+.onoffswitch {
+  -moz-user-select: none;
+  height: 26px;
+  position: relative;
+  width: 62px;
+}
+.onoffswitch .onoffswitch-checkbox {
+  display: none;
+}
+.onoffswitch .onoffswitch-label {
+  border: 1px solid #bbb;
+  border-radius: 2px;
+  cursor: pointer;
+  display: block;
+  overflow: hidden;
+  width: 62px;
+}
+.onoffswitch .onoffswitch-inner {
+  display: block;
+  margin-left: -100%;
+  transition: margin 0.3s ease-in 0s;
+  width: 200%;
+}
+.onoffswitch .onoffswitch-inner > span {
+  -moz-box-sizing: border-box;
+  color: white;
+  float: left;
+  font-size: 11px;
+  font-family: "Open Sans", sans-serif;
+  font-weight: bold;
+  height: 24px;
+  line-height: 24px;
+  padding: 0;
+  width: 50%;
+}
+.onoffswitch .onoffswitch-switch {
+  background-image: linear-gradient(top, #fafafa 0%, #ededed 100%);
+  background-image: -o-linear-gradient(top, #fafafa 0%, #ededed 100%);
+  background-image: -moz-linear-gradient(top, #fafafa 0%, #ededed 100%);
+  background-image: -webkit-linear-gradient(top, #fafafa 0%, #ededed 100%);
+  background-image: -ms-linear-gradient(top, #fafafa 0%, #ededed 100%);
+  background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0, #fafafa), color-stop(1, 0, #ededed));
+  border: 1px solid #aaa;
+  border-radius: 2px;
+  bottom: 0;
+  margin: 0;
+  position: absolute;
+  right: 39px;
+  top: 0;
+  transition: all 0.3s ease-in 0s;
+  -webkit-transition: all 0.3s ease-in 0s;
+  width: 23px;
+}
+.onoffswitch .onoffswitch-inner .onoffswitch-active {
+  background-image: linear-gradient(top, #00a9ec 0%, #009bd3 100%);
+  background-image: -o-linear-gradient(top, #00a9ec 0%, #009bd3 100%);
+  background-image: -moz-linear-gradient(top, #00a9ec 0%, #009bd3 100%);
+  background-image: -webkit-linear-gradient(top, #00a9ec 0%, #009bd3 100%);
+  background-image: -ms-linear-gradient(top, #00a9ec 0%, #009bd3 100%);
+  background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0, #00a9ec), color-stop(1, 0, #009bd3));
+  color: #FFFFFF;
+  padding-left: 10px;
+}
+.onoffswitch .onoffswitch-inner .onoffswitch-inactive {
+  background: linear-gradient(#fefefe, #e8e8e8) repeat scroll 0 0 transparent;
+  color: #4d5258;
+  padding-right: 10px;
+  text-align: right;
+}
+.onoffswitch .onoffswitch-checkbox:checked + .onoffswitch-label .onoffswitch-inner {
+  margin-left: 0;
+}
+.onoffswitch .onoffswitch-checkbox:checked + .onoffswitch-label .onoffswitch-switch {
+  right: 0;
+}
+input[type="text"].tiny,
+input[type="password"].tiny,
+input[type="email"].tiny {
+  min-width: 40px;
+  width: 40px;
+}
+.select-rcue:hover {
+  border-color: #62afdb;
+}
+.select-rcue:focus {
+  border-color: #62afdb;
+  box-shadow: #62afdb 0 0 5px;
+}
+.select-rcue.error {
+  border-color: #ba1212;
+  background-color: #f8e7e7;
+  transition: all 0.33s ease-in-out;
+  -moz-transition: all 0.33s ease-in-out;
+  -webkit-transition: all 0.33s ease-in-out;
+}
+.select-rcue.error:focus {
+  box-shadow: 0 0 5px #ba1212;
+}
+.select-rcue select {
+  height: 30px;
+  line-height: 30px;
+  margin-top: -2px;
+  margin-left: -2px;
+  font-size: 1.1em;
+  padding: 5px 0.545454545454545em;
+  background-color: transparent;
+  border: none;
+  width: 150%;
+  font-family: "Open Sans", sans-serif;
+}
+.select-rcue option {
+  line-height: 2em;
+  padding-left: 0.90909090909091em;
+}
+.select-rcue option:hover {
+  background-color: #d5ecf9;
+}
+
+.input-group input + .select-rcue {
+  border-radius: 0 2px 2px 0;
+  border-left: 0;
+  display: inline-block;
+}
+.input-select .input-group input {
+  float: left;
+}
+
+.form-actions {
+  float: right;
+  margin-top: 3em;
+  margin-bottom: 5em;
+}
+.form-actions .primary {
+  float: right;
+  margin-left: 0.90909090909091em;
+}
+.form-actions a {
+  font-size: 1.1em;
+  margin-right: 0.90909090909091em;
+}
diff --git a/forms/common-themes/src/main/resources/theme/login/rcue/resources/css/login-register.css b/forms/common-themes/src/main/resources/theme/login/rcue/resources/css/login-register.css
new file mode 100644
index 0000000..89222db
--- /dev/null
+++ b/forms/common-themes/src/main/resources/theme/login/rcue/resources/css/login-register.css
@@ -0,0 +1,436 @@
+body {
+  font-size: 62.5%;
+  min-height: 60em;
+  min-width: 120em;
+}
+
+body.rcue-login-register {
+    background-image: url("../img/login-screen-background.jpg");
+}
+
+div.rcue-logo {
+    background-image: url("../img/keycloak-logo.png");
+    background-repeat: no-repeat;
+    height: 37px;
+    width: 150px;
+    position: absolute;
+    top: 5em;
+    right: 6.4em;
+}
+
+.rcue-login-register {
+  background-color: #1D2226;
+  background-position: top left;
+  background-size: auto;
+  background-repeat: no-repeat;
+  color: #fff;
+  /* Login area */
+
+  /* Social login area */
+
+  /* Info area */
+
+}
+
+.rcue-login-register h1 a {
+  position: absolute;
+  top: 5em;
+  right: 6.4em;
+}
+.rcue-login-register .content {
+  position: absolute;
+  bottom: 10%;
+  width: 100%;
+  min-width: 76em;
+}
+.rcue-login-register h2 {
+  padding-left: 4.34782608695652em;
+  font-family: "Overpass", sans-serif;
+  font-size: 2.3em;
+  font-weight: 100;
+  text-transform: uppercase;
+  letter-spacing: 0.005em;
+}
+.rcue-login-register h2 strong {
+  font-weight: bold;
+}
+.rcue-login-register .background-area {
+  border-top: 0.1em rgba(255, 255, 255, 0.05) solid;
+  border-bottom: 0.1em rgba(255, 255, 255, 0.05) solid;
+  background-color: rgba(0, 0, 0, 0.3);
+  padding: 3em 0 3em 10em;
+  margin-top: 2.7em;
+  width: 100%;
+  min-width: 120em;
+}
+.rcue-login-register .form-area.separator,
+.rcue-login-register .form-area.social,
+.rcue-login-register .form-area.social.separator {
+  background-repeat: no-repeat;
+  background-position: 42.7em center;
+}
+.rcue-login-register .form-area.separator {
+  background-image: url(../img/login-register-separator.png);
+  background-position: 43.2em center;
+}
+.rcue-login-register .form-area.social {
+  background-image: url(../img/login-register-social.png);
+}
+.rcue-login-register .form-area.social.separator {
+  background-image: url(../img/login-register-social-separator.png);
+}
+.rcue-login-register .background-area .section {
+  float: left;
+  padding: 0 4.5em 0 4.6em;
+  width: auto;
+  position: relative;
+}
+.rcue-login-register .background-area .section,
+.rcue-login-register .background-area .social .section {
+  padding-top: 1.5em;
+  padding-bottom: 1.5em;
+}
+.rcue-login-register .background-area .section h3 {
+  display: none;
+}
+.rcue-login-register .background-area .section:first-child {
+  padding-right: 4.5em;
+}
+.rcue-login-register .section > p {
+  font-size: 1.3em;
+  margin-bottom: 1.53846153846154em;
+  line-height: 1.3em;
+}
+.rcue-login-register .section.app-form {
+  padding-left: 0;
+  position: relative;
+}
+.rcue-login-register form > div {
+  margin-bottom: 1em;
+}
+.rcue-login-register label,
+.rcue-login-register .social-login > p {
+  display: inline-block;
+  font-size: 1.4em;
+  font-weight: 400;
+}
+.rcue-login-register label {
+  width: 8.21428571428571em;
+}
+.rcue-login-register label.two-lines {
+  float: left;
+  margin-top: -0.14285714285714em;
+  line-height: 1.1em;
+}
+.rcue-login-register input[type="text"],
+.rcue-login-register input[type="password"] {
+  width: 24.7272727272727em;
+  /* 272px */
+
+}
+.rcue-login-register form > div.aside-btn {
+  float: left;
+  font-size: 1.1em;
+  margin-left: 10.4545454545454em;
+  margin-top: 0.90909090909091em;
+  margin-bottom: 0;
+}
+.rcue-login-register form > div.aside-btn label {
+  font-size: 1em;
+  width: auto;
+}
+.rcue-login-register form > div.aside-btn input[type="checkbox"] {
+  margin-bottom: 0.54545454545455em;
+  /* 6px */
+}
+.rcue-login-register form > input[type="button"],
+.rcue-login-register form > input[type="submit"]{
+  float: right;
+  margin-top: 0.76923076923077em;
+  margin-left: 0.90909090909091em;
+  /* 10px */
+
+}
+.rcue-login-register p.subtitle {
+  font-size: 1.1em;
+  color: #999;
+  position: absolute;
+  right: 4.09090909090909em;
+  top: -0.636363636363636em;
+}
+.rcue-login-register .feedback.bottom-left {
+  left: 35.7em;
+  bottom: 17em;
+  min-width: 35em;
+}
+.rcue-login-register input.error[type="text"],
+.rcue-login-register input.error[type="password"],
+.rcue-login-register input.error[type="email"] {
+  background-color: #F8E7E7;
+}
+.rcue-login-register .section.social-login > span {
+  display: none;
+}
+.rcue-login-register .section.social-login > p {
+  float: left;
+  margin-top: 0.28571428571429em;
+  /* 14px */
+
+  width: 6.78571428571429em;
+  /* 95px */
+
+}
+.rcue-login-register .section.social-login > ul {
+  float: left;
+}
+.rcue-login-register .section.social-login li {
+  margin-bottom: 2em;
+}
+.rcue-login-register .section.social-login li:last-child {
+  margin-bottom: 0;
+}
+.rcue-login-register .section.info-area {
+  padding-right: 0;
+}
+.rcue-login-register .section.info-area p,
+.rcue-login-register .section.info-area li {
+  font-size: 1.4em;
+  margin-bottom: 1.64285714285714em;
+}
+.rcue-login-register .section.info-area li {
+  color: #999;
+  margin-bottom: 1em;
+}
+.rcue-login-register .section.info-area li:last-child {
+  margin-bottom: 0;
+}
+@media screen and (min-width: 1280px) {
+  .rcue-login-register {
+    background-size: 100% auto;
+  }
+}
+/* Social buttons */
+.zocial,
+a.zocial {
+  padding: 0;
+  line-height: 2.3em;
+  height: 2.3em;
+  width: 131px;
+  border-radius: 2px;
+  box-shadow: none;
+  background-image: none;
+  text-shadow: none;
+}
+.zocial .text,
+a.zocial .text {
+  font-size: 1.2em;
+  line-height: 1.25em;
+  text-align: center;
+  display: block;
+  font-family: "Open Sans", sans-serif;
+  font-weight: normal;
+  border-left: 1px solid rgba(0, 0, 0, 0.15);
+  margin-left: 3em;
+  /* 36 px */
+
+  margin-top: 0.25em;
+  /* 3px */
+
+}
+.zocial:hover,
+a.zocial:hover,
+.zocial:active,
+a.zocial:active,
+.zocial:focus,
+a.zocial:focus {
+  text-decoration: none;
+  background-image: none;
+}
+.zocial:hover,
+a.zocial:hover {
+  background-image: linear-gradient(rgba(0, 0, 0, 0.1) 0%, rgba(0, 0, 0, 0.1) 100%);
+}
+.zocial:before,
+a.zocial:before {
+  margin: 0;
+  padding: 0;
+  box-shadow: none;
+  border: none;
+  width: 3em;
+  /* 36px */
+
+}
+.zocial.facebook:before {
+  width: 2.66666666666667em;
+  /* 32px */
+}
+/* Register page */
+.rcue-login-register.register label {
+  width: 7.5em;
+  /* 105px */
+
+}
+.rcue-login-register.register input[type="text"],
+.rcue-login-register.register input[type="email"],
+.rcue-login-register.register input[type="password"] {
+  width: 22.9090909090909em;
+  /* 252px */
+
+}
+.rcue-login-register.register form > div.aside-btn {
+  margin-left: 9.54545454545454em;
+  /* 105px */
+
+  width: 12.5454545454546em;
+  /* 138px */
+
+}
+.rcue-login-register.register form > div.aside-btn p {
+  line-height: 1.3em;
+}
+.rcue-login-register p.powered {
+  font-size: 1.1em;
+  margin-top: 1.27272727272727em;
+  text-align: right;
+  margin-right: 5.81818181818182em;
+}
+.rcue-login-register p.powered a {
+  color: #666;
+}
+.rcue-login-register p.powered a:hover {
+  color: #0099D3;
+}
+/* Forgot Password page */
+.rcue-login-register.reset .background-area .section.app-form {
+  width: 43.2em;
+}
+.rcue-login-register.oauth .form-actions {
+    margin-bottom: 0;
+    margin-top: 2em;
+}
+.rcue-login-register .background-area .content-area {
+    width: 50em;
+}
+.rcue-login-register .background-area .content-area ul {
+    border-bottom: 1px solid #34393C;
+    margin-bottom: 2em;
+}
+.rcue-login-register .background-area .content-area ul li {
+    border-top: 1px solid #34393C;
+    padding: 2em;
+    position: relative;
+}
+.rcue-login-register .background-area .content-area ul li span {
+    font-size: 1.3em;
+    line-height: 1.3em;
+}
+
+.rcue-login-register .background-area .content-area ul li span:first-child {
+    padding-right: 11.5384615384615em;
+}
+
+.rcue-login-register .background-area .content-area ul li span.parent {
+    position: absolute;
+    left: 26em;
+    top: 1.53846153846154em;
+    width: 12.3076923076923em;
+}
+
+.rcue-login-register .background-area .content-area ul li span.icon-info {
+    float: right;
+    margin-top: 0.5em;
+}
+.rcue-login-register .background-area .content-area p.terms {
+    color: #999999;
+    font-size: 1.1em;
+    line-height: 1.3em;
+}
+
+.rcue-login-register.reset p.subtitle {
+    margin-bottom: 10px;
+    position: inherit;
+    text-align: right;
+}
+
+.rcue-login-register .background-area p.instruction {
+    font-size: 1.3em;
+    line-height: 1.3em;
+    margin-bottom: 1.53846em;
+}
+
+.rcue-login-register .background-area p.instruction.instruction.second {
+    color: #999999;
+}
+.rcue-login-register .background-area p.instruction + .instruction.second {
+    margin-top: -1.23077em;
+}
+
+.rcue-login-register .background-area a.link-right {
+    float: right;
+    font-size: 1.3em;
+}
+
+.rcue-login-register.totp .form-area {
+  background-image: none;
+}
+.rcue-login-register.reset .form-area p.instruction {
+  font-size: 1.3em;
+  line-height: 1.3em;
+  margin-bottom: 1.81818181818182em;
+}
+.rcue-login-register.totp {
+  min-height: 0;
+}
+.rcue-login-register.totp ol li {
+  margin-bottom: 3em;
+  width: 100%;
+}
+.rcue-login-register.totp ol li p {
+  font-size: 1.3em;
+  margin-bottom: 1.92307692307692em;
+}
+.rcue-login-register.totp ol li p strong {
+  text-indent: -1em;
+  float: left;
+  font-size: 1.84615384615385em;
+  font-weight: normal;
+  margin-top: -0.20833333333333em;
+  color: #999;
+}
+.rcue-login-register.totp ol li img {
+  border: 7px solid #fff;
+  width: 150px;
+}
+.rcue-login-register.totp ol li .code {
+  font-size: 1.3em;
+  margin-left: 1.53846153846154em;
+}
+.rcue-login-register.totp ol li form {
+  width: 357px;
+}
+.rcue-login-register.totp ol li form input[type="text"] {
+    width: 22em;
+}
+.rcue-login-register.totp ol li form input[type="submit"] {
+    float: right;
+}
+
+.rcue-login-register.totp ol li:last-child {
+  margin-bottom: 0;
+}
+.rcue-login-register.totp .content {
+  position: inherit;
+  margin-top: 16em;
+}
+.rcue-login-register.email .background-area .section {
+  width: 41.2em;
+}
+.rcue-login-register.email .background-area .section.email {
+  width: 45.8em;
+}
+.rcue-login-register.email label {
+  width: 6.78571428571429em;
+}
+.rcue-login-register.email .feedback.bottom-left {
+  left: 38.3em;
+}
diff --git a/forms/common-themes/src/main/resources/theme/login/rcue/resources/css/reset.css b/forms/common-themes/src/main/resources/theme/login/rcue/resources/css/reset.css
new file mode 100644
index 0000000..7f0b5b6
--- /dev/null
+++ b/forms/common-themes/src/main/resources/theme/login/rcue/resources/css/reset.css
@@ -0,0 +1,71 @@
+/* http://meyerweb.com/eric/tools/css/reset/ 
+   v2.0 | 20110126
+   License: none (public domain)
+*/
+
+html, body, div, span, applet, object, iframe,
+h1, h2, h3, h4, h5, h6, p, blockquote, pre,
+a, abbr, acronym, address, big, cite, code,
+del, dfn, em, img, ins, kbd, q, s, samp,
+small, strike, strong, sub, sup, tt, var,
+b, u, i, center,
+dl, dt, dd, ol, ul, li,
+fieldset, form, label, legend,
+table, caption, tbody, tfoot, thead, tr, th, td,
+article, aside, canvas, details, embed, 
+figure, figcaption, footer, header, hgroup, 
+menu, nav, output, ruby, section, summary,
+time, mark, audio, video {
+	margin: 0;
+	padding: 0;
+	border: 0;
+	font-size: 100%;
+	font: inherit;
+	vertical-align: baseline;
+}
+/* HTML5 display-role reset for older browsers */
+article, aside, details, figcaption, figure, 
+footer, header, hgroup, menu, nav, section {
+	display: block;
+}
+body {
+	line-height: 1;
+}
+ol, ul {
+	list-style: none;
+}
+blockquote, q {
+	quotes: none;
+}
+blockquote:before, blockquote:after,
+q:before, q:after {
+	content: '';
+	content: none;
+}
+table {
+	border-collapse: collapse;
+	border-spacing: 0;
+}
+
+/* Clearfix */
+
+.clearfix:after {
+	content: ".";
+	display: block;
+	clear: both;
+	visibility: hidden;
+	line-height: 0;
+	height: 0;
+}
+ 
+.clearfix {
+	display: inline-block;
+}
+ 
+html[xmlns] .clearfix {
+	display: block;
+}
+ 
+* html .clearfix {
+	height: 1%;
+}
\ No newline at end of file
diff --git a/forms/common-themes/src/main/resources/theme/login/rcue/resources/img/customer-login-screen-bg.jpg b/forms/common-themes/src/main/resources/theme/login/rcue/resources/img/customer-login-screen-bg.jpg
new file mode 100644
index 0000000..1bbfc95
Binary files /dev/null and b/forms/common-themes/src/main/resources/theme/login/rcue/resources/img/customer-login-screen-bg.jpg differ
diff --git a/forms/common-themes/src/main/resources/theme/login/rcue/resources/img/favicon.ico b/forms/common-themes/src/main/resources/theme/login/rcue/resources/img/favicon.ico
new file mode 100644
index 0000000..8864f00
Binary files /dev/null and b/forms/common-themes/src/main/resources/theme/login/rcue/resources/img/favicon.ico differ
diff --git a/forms/common-themes/src/main/resources/theme/login/rcue/resources/img/feedback-error-arrow-down.png b/forms/common-themes/src/main/resources/theme/login/rcue/resources/img/feedback-error-arrow-down.png
new file mode 100644
index 0000000..6f2d9d2
Binary files /dev/null and b/forms/common-themes/src/main/resources/theme/login/rcue/resources/img/feedback-error-arrow-down.png differ
diff --git a/forms/common-themes/src/main/resources/theme/login/rcue/resources/img/feedback-error-sign.png b/forms/common-themes/src/main/resources/theme/login/rcue/resources/img/feedback-error-sign.png
new file mode 100644
index 0000000..0dd5004
Binary files /dev/null and b/forms/common-themes/src/main/resources/theme/login/rcue/resources/img/feedback-error-sign.png differ
diff --git a/forms/common-themes/src/main/resources/theme/login/rcue/resources/img/feedback-success-arrow-down.png b/forms/common-themes/src/main/resources/theme/login/rcue/resources/img/feedback-success-arrow-down.png
new file mode 100644
index 0000000..03cc0c4
Binary files /dev/null and b/forms/common-themes/src/main/resources/theme/login/rcue/resources/img/feedback-success-arrow-down.png differ
diff --git a/forms/common-themes/src/main/resources/theme/login/rcue/resources/img/feedback-success-sign.png b/forms/common-themes/src/main/resources/theme/login/rcue/resources/img/feedback-success-sign.png
new file mode 100644
index 0000000..640bd71
Binary files /dev/null and b/forms/common-themes/src/main/resources/theme/login/rcue/resources/img/feedback-success-sign.png differ
diff --git a/forms/common-themes/src/main/resources/theme/login/rcue/resources/img/feedback-warning-arrow-down.png b/forms/common-themes/src/main/resources/theme/login/rcue/resources/img/feedback-warning-arrow-down.png
new file mode 100644
index 0000000..6f2d9d2
Binary files /dev/null and b/forms/common-themes/src/main/resources/theme/login/rcue/resources/img/feedback-warning-arrow-down.png differ
diff --git a/forms/common-themes/src/main/resources/theme/login/rcue/resources/img/feedback-warning-sign.png b/forms/common-themes/src/main/resources/theme/login/rcue/resources/img/feedback-warning-sign.png
new file mode 100644
index 0000000..f9392a3
Binary files /dev/null and b/forms/common-themes/src/main/resources/theme/login/rcue/resources/img/feedback-warning-sign.png differ
diff --git a/forms/common-themes/src/main/resources/theme/login/rcue/resources/img/keycloak-logo.png b/forms/common-themes/src/main/resources/theme/login/rcue/resources/img/keycloak-logo.png
new file mode 100644
index 0000000..45c51c2
Binary files /dev/null and b/forms/common-themes/src/main/resources/theme/login/rcue/resources/img/keycloak-logo.png differ
diff --git a/forms/common-themes/src/main/resources/theme/login/rcue/resources/img/login-register-separator.png b/forms/common-themes/src/main/resources/theme/login/rcue/resources/img/login-register-separator.png
new file mode 100644
index 0000000..d626a45
Binary files /dev/null and b/forms/common-themes/src/main/resources/theme/login/rcue/resources/img/login-register-separator.png differ
diff --git a/forms/common-themes/src/main/resources/theme/login/rcue/resources/img/login-register-social.png b/forms/common-themes/src/main/resources/theme/login/rcue/resources/img/login-register-social.png
new file mode 100644
index 0000000..e86d738
Binary files /dev/null and b/forms/common-themes/src/main/resources/theme/login/rcue/resources/img/login-register-social.png differ
diff --git a/forms/common-themes/src/main/resources/theme/login/rcue/resources/img/login-register-social-separator.png b/forms/common-themes/src/main/resources/theme/login/rcue/resources/img/login-register-social-separator.png
new file mode 100644
index 0000000..3f08929
Binary files /dev/null and b/forms/common-themes/src/main/resources/theme/login/rcue/resources/img/login-register-social-separator.png differ
diff --git a/forms/common-themes/src/main/resources/theme/login/rcue/resources/img/login-screen-background.jpg b/forms/common-themes/src/main/resources/theme/login/rcue/resources/img/login-screen-background.jpg
new file mode 100644
index 0000000..a50a2fc
Binary files /dev/null and b/forms/common-themes/src/main/resources/theme/login/rcue/resources/img/login-screen-background.jpg differ
diff --git a/forms/common-themes/src/main/resources/theme/login/rcue/resources/img/register-login-bg.png b/forms/common-themes/src/main/resources/theme/login/rcue/resources/img/register-login-bg.png
new file mode 100644
index 0000000..7ddd4ad
Binary files /dev/null and b/forms/common-themes/src/main/resources/theme/login/rcue/resources/img/register-login-bg.png differ
diff --git a/forms/common-themes/src/main/resources/theme/login/rcue/resources/img/sprites-white.png b/forms/common-themes/src/main/resources/theme/login/rcue/resources/img/sprites-white.png
new file mode 100755
index 0000000..cf70cf1
Binary files /dev/null and b/forms/common-themes/src/main/resources/theme/login/rcue/resources/img/sprites-white.png differ
diff --git a/forms/common-themes/src/main/resources/theme/login/rcue/theme.properties b/forms/common-themes/src/main/resources/theme/login/rcue/theme.properties
new file mode 100644
index 0000000..298aa65
--- /dev/null
+++ b/forms/common-themes/src/main/resources/theme/login/rcue/theme.properties
@@ -0,0 +1,2 @@
+parent=base
+styles=css/styles.css
diff --git a/forms/login-api/pom.xml b/forms/login-api/pom.xml
new file mode 100755
index 0000000..ed4d5f2
--- /dev/null
+++ b/forms/login-api/pom.xml
@@ -0,0 +1,49 @@
+<?xml version="1.0"?>
+<project>
+	<parent>
+		<artifactId>keycloak-forms</artifactId>
+		<groupId>org.keycloak</groupId>
+		<version>1.0-alpha-2-SNAPSHOT</version>
+		<relativePath>../pom.xml</relativePath>
+	</parent>
+	<modelVersion>4.0.0</modelVersion>
+
+	<artifactId>keycloak-login-api</artifactId>
+	<name>Keycloak Login API</name>
+	<description />
+
+	<dependencies>
+		<dependency>
+			<groupId>org.keycloak</groupId>
+			<artifactId>keycloak-core</artifactId>
+			<version>${project.version}</version>
+		</dependency>
+        <dependency>
+            <groupId>org.keycloak</groupId>
+            <artifactId>keycloak-model-api</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.jboss.resteasy</groupId>
+            <artifactId>jaxrs-api</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.jboss.resteasy</groupId>
+            <artifactId>resteasy-jaxrs</artifactId>
+        </dependency>
+	</dependencies>
+
+	<build>
+		<plugins>
+			<plugin>
+				<groupId>org.apache.maven.plugins</groupId>
+				<artifactId>maven-compiler-plugin</artifactId>
+				<configuration>
+					<source>1.6</source>
+					<target>1.6</target>
+				</configuration>
+			</plugin>
+		</plugins>
+	</build>
+
+</project>
diff --git a/forms/login-api/src/main/java/org/keycloak/login/FormsLoader.java b/forms/login-api/src/main/java/org/keycloak/login/FormsLoader.java
new file mode 100644
index 0000000..eef384b
--- /dev/null
+++ b/forms/login-api/src/main/java/org/keycloak/login/FormsLoader.java
@@ -0,0 +1,17 @@
+package org.keycloak.login;
+
+import java.util.ServiceLoader;
+
+/**
+ * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+public class FormsLoader {
+
+    private FormsLoader() {
+    }
+
+    public static FormsProvider load() {
+        return ServiceLoader.load(FormsProvider.class).iterator().next();
+    }
+
+}
diff --git a/forms/login-api/src/main/java/org/keycloak/login/FormsPages.java b/forms/login-api/src/main/java/org/keycloak/login/FormsPages.java
new file mode 100644
index 0000000..ef033b5
--- /dev/null
+++ b/forms/login-api/src/main/java/org/keycloak/login/FormsPages.java
@@ -0,0 +1,10 @@
+package org.keycloak.login;
+
+/**
+ * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+public enum FormsPages {
+
+    LOGIN, LOGIN_TOTP, LOGIN_CONFIG_TOTP, LOGIN_VERIFY_EMAIL, OAUTH_GRANT, LOGIN_RESET_PASSWORD, LOGIN_UPDATE_PASSWORD, LOGIN_USERNAME_REMINDER, REGISTER, ERROR, LOGIN_UPDATE_PROFILE;
+
+}
diff --git a/forms/login-api/src/main/java/org/keycloak/login/FormsProvider.java b/forms/login-api/src/main/java/org/keycloak/login/FormsProvider.java
new file mode 100644
index 0000000..1b606f9
--- /dev/null
+++ b/forms/login-api/src/main/java/org/keycloak/login/FormsProvider.java
@@ -0,0 +1,15 @@
+package org.keycloak.login;
+
+import org.jboss.resteasy.spi.HttpRequest;
+import org.keycloak.models.RealmModel;
+
+import javax.ws.rs.core.UriInfo;
+
+/**
+ * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+public interface FormsProvider {
+
+    public LoginForms createForms(RealmModel realm, HttpRequest request, UriInfo uriInfo);
+
+}
diff --git a/forms/login-api/src/main/java/org/keycloak/login/LoginForms.java b/forms/login-api/src/main/java/org/keycloak/login/LoginForms.java
new file mode 100644
index 0000000..c693be2
--- /dev/null
+++ b/forms/login-api/src/main/java/org/keycloak/login/LoginForms.java
@@ -0,0 +1,49 @@
+package org.keycloak.login;
+
+import org.keycloak.models.RoleModel;
+import org.keycloak.models.UserModel;
+
+import javax.ws.rs.core.MultivaluedMap;
+import javax.ws.rs.core.Response;
+import java.util.List;
+
+/**
+ * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+public interface LoginForms {
+
+    public Response createResponse(UserModel.RequiredAction action);
+
+    public Response createLogin();
+
+    public Response createPasswordReset();
+
+    public Response createUsernameReminder();
+
+    public Response createLoginTotp();
+
+    public Response createRegistration();
+
+    public Response createErrorPage();
+
+    public Response createOAuthGrant();
+
+    public LoginForms setAccessCode(String accessCodeId, String accessCode);
+
+    public LoginForms setAccessRequest(List<RoleModel> realmRolesRequested, MultivaluedMap<String,RoleModel> resourceRolesRequested);
+
+    public LoginForms setError(String message);
+
+    public LoginForms setSuccess(String message);
+
+    public LoginForms setWarning(String message);
+
+    public LoginForms setUser(UserModel user);
+
+    public LoginForms setClient(UserModel client);
+
+    public LoginForms setFormData(MultivaluedMap<String, String> formData);
+
+    public LoginForms setStatus(Response.Status status);
+
+}
diff --git a/forms/login-freemarker/pom.xml b/forms/login-freemarker/pom.xml
new file mode 100755
index 0000000..3922a3d
--- /dev/null
+++ b/forms/login-freemarker/pom.xml
@@ -0,0 +1,75 @@
+<?xml version="1.0"?>
+<project>
+	<parent>
+		<artifactId>keycloak-forms</artifactId>
+		<groupId>org.keycloak</groupId>
+		<version>1.0-alpha-2-SNAPSHOT</version>
+		<relativePath>../pom.xml</relativePath>
+	</parent>
+	<modelVersion>4.0.0</modelVersion>
+
+	<artifactId>keycloak-login-freemarker</artifactId>
+	<name>Keycloak Login FreeMarker</name>
+	<description />
+
+	<dependencies>
+		<dependency>
+			<groupId>org.keycloak</groupId>
+			<artifactId>keycloak-core</artifactId>
+			<version>${project.version}</version>
+		</dependency>
+        <dependency>
+            <groupId>org.keycloak</groupId>
+            <artifactId>keycloak-forms-common-freemarker</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+		<dependency>
+			<groupId>org.keycloak</groupId>
+			<artifactId>keycloak-login-api</artifactId>
+			<version>${project.version}</version>
+		</dependency>
+        <dependency>
+            <groupId>org.keycloak</groupId>
+            <artifactId>keycloak-model-api</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+		<dependency>
+			<groupId>org.keycloak</groupId>
+			<artifactId>keycloak-services</artifactId>
+			<version>${project.version}</version>
+		</dependency>
+		<dependency>
+			<groupId>org.keycloak</groupId>
+			<artifactId>keycloak-social-core</artifactId>
+			<version>${project.version}</version>
+		</dependency>
+        <dependency>
+            <groupId>org.jboss.resteasy</groupId>
+            <artifactId>resteasy-jaxrs</artifactId>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+			<groupId>org.freemarker</groupId>
+			<artifactId>freemarker</artifactId>
+		</dependency>
+        <dependency>
+            <groupId>org.jboss</groupId>
+            <artifactId>jboss-vfs</artifactId>
+            <version>3.2.2.Final</version>
+        </dependency>
+	</dependencies>
+
+	<build>
+		<plugins>
+			<plugin>
+				<groupId>org.apache.maven.plugins</groupId>
+				<artifactId>maven-compiler-plugin</artifactId>
+				<configuration>
+					<source>1.6</source>
+					<target>1.6</target>
+				</configuration>
+			</plugin>
+		</plugins>
+	</build>
+
+</project>
diff --git a/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/FreeMarkerLoginForms.java b/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/FreeMarkerLoginForms.java
new file mode 100644
index 0000000..9edaa46
--- /dev/null
+++ b/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/FreeMarkerLoginForms.java
@@ -0,0 +1,273 @@
+package org.keycloak.login.freemarker;
+
+import org.jboss.logging.Logger;
+import org.jboss.resteasy.spi.HttpRequest;
+import org.keycloak.freemarker.FreeMarkerException;
+import org.keycloak.freemarker.FreeMarkerUtil;
+import org.keycloak.freemarker.Theme;
+import org.keycloak.freemarker.ThemeLoader;
+import org.keycloak.login.LoginForms;
+import org.keycloak.login.FormsPages;
+import org.keycloak.login.freemarker.model.LoginBean;
+import org.keycloak.login.freemarker.model.MessageBean;
+import org.keycloak.login.freemarker.model.OAuthGrantBean;
+import org.keycloak.login.freemarker.model.ProfileBean;
+import org.keycloak.login.freemarker.model.RealmBean;
+import org.keycloak.login.freemarker.model.RegisterBean;
+import org.keycloak.login.freemarker.model.SocialBean;
+import org.keycloak.login.freemarker.model.TotpBean;
+import org.keycloak.login.freemarker.model.UrlBean;
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.RoleModel;
+import org.keycloak.models.UserModel;
+import org.keycloak.services.email.EmailException;
+import org.keycloak.services.email.EmailSender;
+import org.keycloak.services.messages.Messages;
+
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.MultivaluedMap;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.UriBuilder;
+import javax.ws.rs.core.UriInfo;
+import java.io.IOException;
+import java.net.URI;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Properties;
+
+/**
+ * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+public class FreeMarkerLoginForms implements LoginForms {
+
+    private static final Logger logger = Logger.getLogger(FreeMarkerLoginForms.class);
+
+    private String message;
+    private String accessCodeId;
+    private String accessCode;
+    private Response.Status status = Response.Status.OK;
+    private List<RoleModel> realmRolesRequested;
+    private MultivaluedMap<String, RoleModel> resourceRolesRequested;
+
+    public static enum MessageType {SUCCESS, WARNING, ERROR}
+
+    private MessageType messageType = MessageType.ERROR;
+
+    private MultivaluedMap<String, String> formData;
+
+    private RealmModel realm;
+
+    // TODO Remove
+    private HttpRequest request;
+
+    private UserModel user;
+
+    private UserModel client;
+
+    private UriInfo uriInfo;
+
+    FreeMarkerLoginForms(RealmModel realm, org.jboss.resteasy.spi.HttpRequest request, UriInfo uriInfo) {
+        this.realm = realm;
+        this.request = request;
+        this.uriInfo = uriInfo;
+    }
+
+    public Response createResponse(UserModel.RequiredAction action) {
+        String actionMessage;
+        FormsPages page;
+
+        switch (action) {
+            case CONFIGURE_TOTP:
+                actionMessage = Messages.ACTION_WARN_TOTP;
+                page = FormsPages.LOGIN_CONFIG_TOTP;
+                break;
+            case UPDATE_PROFILE:
+                actionMessage = Messages.ACTION_WARN_PROFILE;
+                page = FormsPages.LOGIN_UPDATE_PROFILE;
+                break;
+            case UPDATE_PASSWORD:
+                actionMessage = Messages.ACTION_WARN_PASSWD;
+                page = FormsPages.LOGIN_UPDATE_PASSWORD;
+                break;
+            case VERIFY_EMAIL:
+                try {
+                    new EmailSender(realm.getSmtpConfig()).sendEmailVerification(user, realm, accessCodeId, uriInfo);
+                } catch (EmailException e) {
+                    return setError("emailSendError").createErrorPage();
+                }
+
+                actionMessage = Messages.ACTION_WARN_EMAIL;
+                page = FormsPages.LOGIN_VERIFY_EMAIL;
+                break;
+            default:
+                return Response.serverError().build();
+        }
+
+        if (message == null) {
+            setWarning(actionMessage);
+        }
+
+        return createResponse(page);
+    }
+
+    private Response createResponse(FormsPages page) {
+        MultivaluedMap<String, String> queryParameterMap = uriInfo.getQueryParameters();
+
+        String requestURI = uriInfo.getBaseUri().getPath();
+        UriBuilder uriBuilder = UriBuilder.fromUri(requestURI);
+
+        for (String k : queryParameterMap.keySet()) {
+            uriBuilder.replaceQueryParam(k, queryParameterMap.get(k).toArray());
+        }
+
+        if (accessCode != null) {
+            uriBuilder.replaceQueryParam("code", accessCode);
+        }
+
+        Map<String, Object> attributes = new HashMap<String, Object>();
+
+        Theme theme;
+        try {
+            theme = ThemeLoader.createTheme(realm.getLoginTheme(), Theme.Type.LOGIN);
+        } catch (FreeMarkerException e) {
+            logger.error("Failed to create theme", e);
+            return Response.serverError().build();
+        }
+
+        try {
+            attributes.put("properties", theme.getProperties());
+        } catch (IOException e) {
+            logger.warn("Failed to load properties", e);
+        }
+
+        Properties messages;
+        try {
+            messages = theme.getMessages();
+            attributes.put("rb", messages);
+        } catch (IOException e) {
+            logger.warn("Failed to load messages", e);
+            messages = new Properties();
+        }
+
+        if (message != null) {
+            attributes.put("message", new MessageBean(messages.containsKey(message) ? messages.getProperty(message) : message, messageType));
+        }
+
+        URI baseUri = uriBuilder.build();
+
+        if (realm != null) {
+            attributes.put("realm", new RealmBean(realm));
+            attributes.put("social", new SocialBean(realm, baseUri));
+            attributes.put("url", new UrlBean(realm, theme, baseUri));
+        }
+
+        attributes.put("login", new LoginBean(formData));
+
+        switch (page) {
+            case LOGIN_CONFIG_TOTP:
+                attributes.put("totp", new TotpBean(user, baseUri));
+                break;
+            case LOGIN_UPDATE_PROFILE:
+                attributes.put("user", new ProfileBean(user));
+                break;
+            case REGISTER:
+                attributes.put("register", new RegisterBean(formData));
+                break;
+            case OAUTH_GRANT:
+                attributes.put("oauth", new OAuthGrantBean(accessCode, client, realmRolesRequested, resourceRolesRequested));
+                break;
+        }
+
+        try {
+            String result = FreeMarkerUtil.processTemplate(attributes, Templates.getTemplate(page), theme);
+            return Response.status(status).type(MediaType.TEXT_HTML).entity(result).build();
+        } catch (FreeMarkerException e) {
+            logger.error("Failed to process template", e);
+            return Response.serverError().build();
+        }
+    }
+
+    public Response createLogin() {
+        return createResponse(FormsPages.LOGIN);
+    }
+
+    public Response createPasswordReset() {
+        return createResponse(FormsPages.LOGIN_RESET_PASSWORD);
+    }
+
+    public Response createUsernameReminder() {
+        return createResponse(FormsPages.LOGIN_USERNAME_REMINDER);
+    }
+
+    public Response createLoginTotp() {
+        return createResponse(FormsPages.LOGIN_TOTP);
+    }
+
+    public Response createRegistration() {
+        return createResponse(FormsPages.REGISTER);
+    }
+
+    public Response createErrorPage() {
+        setStatus(Response.Status.INTERNAL_SERVER_ERROR);
+        return createResponse(FormsPages.ERROR);
+    }
+
+    public Response createOAuthGrant() {
+        return createResponse(FormsPages.OAUTH_GRANT);
+    }
+
+    public FreeMarkerLoginForms setError(String message) {
+        this.message = message;
+        this.messageType = MessageType.ERROR;
+        return this;
+    }
+
+    public FreeMarkerLoginForms setSuccess(String message) {
+        this.message = message;
+        this.messageType = MessageType.SUCCESS;
+        return this;
+    }
+
+    public FreeMarkerLoginForms setWarning(String message) {
+        this.message = message;
+        this.messageType = MessageType.WARNING;
+        return this;
+    }
+
+    public FreeMarkerLoginForms setUser(UserModel user) {
+        this.user = user;
+        return this;
+    }
+
+    public FreeMarkerLoginForms setClient(UserModel client) {
+        this.client = client;
+        return this;
+    }
+
+    public FreeMarkerLoginForms setFormData(MultivaluedMap<String, String> formData) {
+        this.formData = formData;
+        return this;
+    }
+
+    @Override
+    public LoginForms setAccessCode(String accessCodeId, String accessCode) {
+        this.accessCodeId = accessCodeId;
+        this.accessCode = accessCode;
+        return this;
+    }
+
+    @Override
+    public LoginForms setAccessRequest(List<RoleModel> realmRolesRequested, MultivaluedMap<String, RoleModel> resourceRolesRequested) {
+        this.realmRolesRequested = realmRolesRequested;
+        this.resourceRolesRequested = resourceRolesRequested;
+        return this;
+    }
+
+    @Override
+    public LoginForms setStatus(Response.Status status) {
+        this.status = status;
+        return this;
+    }
+
+}
diff --git a/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/FreeMarkerLoginFormsProvider.java b/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/FreeMarkerLoginFormsProvider.java
new file mode 100644
index 0000000..4d7ddd8
--- /dev/null
+++ b/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/FreeMarkerLoginFormsProvider.java
@@ -0,0 +1,20 @@
+package org.keycloak.login.freemarker;
+
+import org.jboss.resteasy.spi.HttpRequest;
+import org.keycloak.login.LoginForms;
+import org.keycloak.login.FormsProvider;
+import org.keycloak.models.RealmModel;
+
+import javax.ws.rs.core.UriInfo;
+
+/**
+ * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+public class FreeMarkerLoginFormsProvider implements FormsProvider {
+
+    @Override
+    public LoginForms createForms(RealmModel realm, HttpRequest request, UriInfo uriInfo) {
+        return new FreeMarkerLoginForms(realm, request, uriInfo);
+    }
+
+}
diff --git a/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/model/TotpBean.java b/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/model/TotpBean.java
new file mode 100755
index 0000000..bfc8d86
--- /dev/null
+++ b/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/model/TotpBean.java
@@ -0,0 +1,86 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2012, Red Hat, Inc., and individual contributors
+ * as indicated by the @author tags. See the copyright.txt file in the
+ * distribution for a full listing of individual contributors.
+ *
+ * This is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This software is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this software; if not, write to the Free
+ * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
+ * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
+ */
+package org.keycloak.login.freemarker.model;
+
+import org.keycloak.models.UserModel;
+import org.keycloak.models.utils.Base32;
+
+import java.io.UnsupportedEncodingException;
+import java.net.URI;
+import java.net.URLEncoder;
+import java.util.Random;
+
+/**
+ * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+public class TotpBean {
+
+    private String totpSecret;
+    private String totpSecretEncoded;
+    private boolean enabled;
+    private String contextUrl;
+
+    public TotpBean(UserModel user, URI baseUri) {
+        this.enabled = user.isTotp();
+        this.contextUrl = baseUri.getPath();
+
+        totpSecret = randomString(20);
+        totpSecretEncoded = Base32.encode(totpSecret.getBytes());
+    }
+
+    private static String randomString(int length) {
+        String chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVW1234567890";
+        Random r = new Random();
+        StringBuilder sb = new StringBuilder();
+        for (int i = 0; i < length; i++) {
+            char c = chars.charAt(r.nextInt(chars.length()));
+            sb.append(c);
+        }
+        return sb.toString();
+    }
+
+    public boolean isEnabled() {
+        return enabled;
+    }
+
+    public String getTotpSecret() {
+        return totpSecret;
+    }
+
+    public String getTotpSecretEncoded() {
+        StringBuilder sb = new StringBuilder();
+        for (int i = 0; i < totpSecretEncoded.length(); i += 4) {
+            sb.append(totpSecretEncoded.substring(i, i + 4 < totpSecretEncoded.length() ? i + 4 : totpSecretEncoded.length()));
+            if (i + 4 < totpSecretEncoded.length()) {
+                sb.append(" ");
+            }
+        }
+        return sb.toString();
+    }
+
+    public String getTotpSecretQrCodeUrl() throws UnsupportedEncodingException {
+        String contents = URLEncoder.encode("otpauth://totp/keycloak?secret=" + totpSecretEncoded, "utf-8");
+        return contextUrl + "qrcode" + "?size=246x246&contents=" + contents;
+    }
+
+}
+
diff --git a/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/Templates.java b/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/Templates.java
new file mode 100644
index 0000000..69a5e4b
--- /dev/null
+++ b/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/Templates.java
@@ -0,0 +1,39 @@
+package org.keycloak.login.freemarker;
+
+import org.keycloak.login.FormsPages;
+
+/**
+ * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+public class Templates {
+
+    public static String getTemplate(FormsPages page) {
+        switch (page) {
+            case LOGIN:
+                return "login.ftl";
+            case LOGIN_TOTP:
+                return "login-totp.ftl";
+            case LOGIN_CONFIG_TOTP:
+                return "login-config-totp.ftl";
+            case LOGIN_VERIFY_EMAIL:
+                return "login-verify-email.ftl";
+            case OAUTH_GRANT:
+                return "login-oauth-grant.ftl";
+            case LOGIN_RESET_PASSWORD:
+                return "login-reset-password.ftl";
+            case LOGIN_UPDATE_PASSWORD:
+                return "login-update-password.ftl";
+            case LOGIN_USERNAME_REMINDER:
+                return "login-username-reminder.ftl";
+            case REGISTER:
+                return "register.ftl";
+            case ERROR:
+                return "error.ftl";
+            case LOGIN_UPDATE_PROFILE:
+                return "login-update-profile.ftl";
+            default:
+                throw new IllegalArgumentException();
+        }
+    }
+
+}
diff --git a/forms/login-freemarker/src/main/resources/META-INF/services/org.keycloak.login.FormsProvider b/forms/login-freemarker/src/main/resources/META-INF/services/org.keycloak.login.FormsProvider
new file mode 100644
index 0000000..ae28fdb
--- /dev/null
+++ b/forms/login-freemarker/src/main/resources/META-INF/services/org.keycloak.login.FormsProvider
@@ -0,0 +1 @@
+org.keycloak.login.freemarker.FreeMarkerLoginFormsProvider
\ No newline at end of file

forms/pom.xml 53(+9 -44)

diff --git a/forms/pom.xml b/forms/pom.xml
index f444dee..9471ddf 100755
--- a/forms/pom.xml
+++ b/forms/pom.xml
@@ -7,54 +7,19 @@
 		<relativePath>../pom.xml</relativePath>
 	</parent>
 	<modelVersion>4.0.0</modelVersion>
+    <packaging>pom</packaging>
 
 	<artifactId>keycloak-forms</artifactId>
 	<name>Keycloak Forms</name>
 	<description />
 
-	<dependencies>
-		<dependency>
-			<groupId>org.keycloak</groupId>
-			<artifactId>keycloak-core</artifactId>
-			<version>${project.version}</version>
-		</dependency>
-        <dependency>
-            <groupId>org.keycloak</groupId>
-            <artifactId>keycloak-model-api</artifactId>
-            <version>${project.version}</version>
-        </dependency>
-		<dependency>
-			<groupId>org.keycloak</groupId>
-			<artifactId>keycloak-services</artifactId>
-			<version>${project.version}</version>
-		</dependency>
-		<dependency>
-			<groupId>org.keycloak</groupId>
-			<artifactId>keycloak-social-core</artifactId>
-			<version>${project.version}</version>
-		</dependency>
-        <dependency>
-            <groupId>org.jboss.resteasy</groupId>
-            <artifactId>resteasy-jaxrs</artifactId>
-            <scope>provided</scope>
-        </dependency>
-        <dependency>
-			<groupId>org.freemarker</groupId>
-			<artifactId>freemarker</artifactId>
-		</dependency>
-	</dependencies>
-
-	<build>
-		<plugins>
-			<plugin>
-				<groupId>org.apache.maven.plugins</groupId>
-				<artifactId>maven-compiler-plugin</artifactId>
-				<configuration>
-					<source>1.6</source>
-					<target>1.6</target>
-				</configuration>
-			</plugin>
-		</plugins>
-	</build>
+    <modules>
+        <module>common-freemarker</module>
+        <module>common-themes</module>
+        <module>account-api</module>
+        <module>account-freemarker</module>
+        <module>login-api</module>
+        <module>login-freemarker</module>
+    </modules>
 
 </project>
diff --git a/model/api/src/main/java/org/keycloak/models/RealmModel.java b/model/api/src/main/java/org/keycloak/models/RealmModel.java
index ac75c7a..8ec3e0a 100755
--- a/model/api/src/main/java/org/keycloak/models/RealmModel.java
+++ b/model/api/src/main/java/org/keycloak/models/RealmModel.java
@@ -161,4 +161,13 @@ public interface RealmModel extends RoleContainerModel, RoleMapperModel, ScopeMa
     Set<RoleModel> getRealmRoleMappings(UserModel user);
 
     Set<RoleModel> getRealmScopeMappings(UserModel user);
+
+    String getLoginTheme();
+
+    void setLoginTheme(String name);
+
+    String getAccountTheme();
+
+    void setAccountTheme(String name);
+
 }
diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/RealmEntity.java b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/RealmEntity.java
index 58c1f17..4bfbcff 100755
--- a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/RealmEntity.java
+++ b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/RealmEntity.java
@@ -50,6 +50,9 @@ public class RealmEntity {
     @Column(length = 2048)
     protected String privateKeyPem;
 
+    protected String loginTheme;
+    protected String accountTheme;
+
     @OneToMany(cascade ={CascadeType.REMOVE}, orphanRemoval = true)
     @JoinTable(name="USER_REQUIRED_CREDENTIALS")
     Collection<RequiredCredentialEntity> requiredCredentials = new ArrayList<RequiredCredentialEntity>();
@@ -274,5 +277,21 @@ public class RealmEntity {
     public void setPasswordPolicy(String passwordPolicy) {
         this.passwordPolicy = passwordPolicy;
     }
+
+    public String getLoginTheme() {
+        return loginTheme;
+    }
+
+    public void setLoginTheme(String theme) {
+        this.loginTheme = theme;
+    }
+
+    public String getAccountTheme() {
+        return accountTheme;
+    }
+
+    public void setAccountTheme(String theme) {
+        this.accountTheme = theme;
+    }
 }
 
diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java b/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java
index b14fa97..965cbbe 100755
--- a/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java
+++ b/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java
@@ -1096,4 +1096,25 @@ public class RealmAdapter implements RealmModel {
         return r.getId().equals(getId());
     }
 
+    @Override
+    public String getLoginTheme() {
+        return realm.getLoginTheme();
+    }
+
+    @Override
+    public void setLoginTheme(String name) {
+        realm.setLoginTheme(name);
+        em.flush();
+    }
+
+    @Override
+    public String getAccountTheme() {
+        return realm.getAccountTheme();
+    }
+
+    @Override
+    public void setAccountTheme(String name) {
+        realm.setAccountTheme(name);
+        em.flush();
+    }
 }
diff --git a/model/picketlink/src/main/java/org/keycloak/models/picketlink/mappings/RealmData.java b/model/picketlink/src/main/java/org/keycloak/models/picketlink/mappings/RealmData.java
index 03c3466..497add8 100755
--- a/model/picketlink/src/main/java/org/keycloak/models/picketlink/mappings/RealmData.java
+++ b/model/picketlink/src/main/java/org/keycloak/models/picketlink/mappings/RealmData.java
@@ -27,6 +27,8 @@ public class RealmData extends AbstractPartition {
     private Map<String, String> smtpConfig;
     private Map<String, String> socialConfig;
     private String passwordPolicy;
+    private String loginTheme;
+    private String accountTheme;
 
     public RealmData() {
         super(null);
@@ -185,4 +187,22 @@ public class RealmData extends AbstractPartition {
     public void setPasswordPolicy(String passwordPolicy) {
         this.passwordPolicy = passwordPolicy;
     }
+
+    @AttributeProperty
+    public String getLoginTheme() {
+        return loginTheme;
+    }
+
+    public void setLoginTheme(String theme) {
+        this.loginTheme = theme;
+    }
+
+    @AttributeProperty
+    public String getAccountTheme() {
+        return accountTheme;
+    }
+
+    public void setAccountTheme(String theme) {
+        this.accountTheme = theme;
+    }
 }
diff --git a/model/picketlink/src/main/java/org/keycloak/models/picketlink/mappings/RealmEntity.java b/model/picketlink/src/main/java/org/keycloak/models/picketlink/mappings/RealmEntity.java
index fe0a448..4b6abb6 100755
--- a/model/picketlink/src/main/java/org/keycloak/models/picketlink/mappings/RealmEntity.java
+++ b/model/picketlink/src/main/java/org/keycloak/models/picketlink/mappings/RealmEntity.java
@@ -62,6 +62,8 @@ public class RealmEntity implements Serializable {
     @AttributeValue
     @Lob
     private HashMap<String, String> socialConfig;
+    @AttributeValue
+    private String theme;
 
 
     public PartitionTypeEntity getPartitionTypeEntity() {
@@ -191,4 +193,12 @@ public class RealmEntity implements Serializable {
     public void setSocialConfig(HashMap<String, String> socialConfig) {
         this.socialConfig = socialConfig;
     }
+
+    public String getTheme() {
+        return theme;
+    }
+
+    public void setTheme(String theme) {
+        this.theme = theme;
+    }
 }
diff --git a/model/picketlink/src/main/java/org/keycloak/models/picketlink/RealmAdapter.java b/model/picketlink/src/main/java/org/keycloak/models/picketlink/RealmAdapter.java
index 4b0b005..d4c1624 100755
--- a/model/picketlink/src/main/java/org/keycloak/models/picketlink/RealmAdapter.java
+++ b/model/picketlink/src/main/java/org/keycloak/models/picketlink/RealmAdapter.java
@@ -1014,4 +1014,26 @@ public class RealmAdapter implements RealmModel {
         realm.setPasswordPolicy(policy.toString());
         updateRealm();
     }
+
+    @Override
+    public String getLoginTheme() {
+        return realm.getLoginTheme();
+    }
+
+    @Override
+    public void setLoginTheme(String name) {
+        realm.setLoginTheme(name);
+        updateRealm();
+    }
+
+    @Override
+    public String getAccountTheme() {
+        return realm.getAccountTheme();
+    }
+
+    @Override
+    public void setAccountTheme(String name) {
+        realm.setAccountTheme(name);
+        updateRealm();
+    }
 }

server/pom.xml 27(+26 -1)

diff --git a/server/pom.xml b/server/pom.xml
index db1bf9e..f07ec2a 100755
--- a/server/pom.xml
+++ b/server/pom.xml
@@ -66,7 +66,32 @@
         </dependency>
         <dependency>
             <groupId>org.keycloak</groupId>
-            <artifactId>keycloak-forms</artifactId>
+            <artifactId>keycloak-forms-common-freemarker</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.keycloak</groupId>
+            <artifactId>keycloak-forms-common-themes</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.keycloak</groupId>
+            <artifactId>keycloak-account-api</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.keycloak</groupId>
+            <artifactId>keycloak-account-freemarker</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.keycloak</groupId>
+            <artifactId>keycloak-login-api</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.keycloak</groupId>
+            <artifactId>keycloak-login-freemarker</artifactId>
             <version>${project.version}</version>
         </dependency>
         <dependency>
diff --git a/server/src/main/java/org/keycloak/server/KeycloakServerApplication.java b/server/src/main/java/org/keycloak/server/KeycloakServerApplication.java
index 13aa205..fdcdaa4 100755
--- a/server/src/main/java/org/keycloak/server/KeycloakServerApplication.java
+++ b/server/src/main/java/org/keycloak/server/KeycloakServerApplication.java
@@ -10,6 +10,7 @@ import org.keycloak.util.JsonSerialization;
 
 import javax.servlet.ServletContext;
 import javax.ws.rs.core.Context;
+import java.io.File;
 import java.io.FileInputStream;
 import java.io.FileNotFoundException;
 import java.io.IOException;
@@ -31,6 +32,14 @@ public class KeycloakServerApplication extends KeycloakApplication {
             session.getTransaction().commit();
         }
 
+        String themeDir = System.getProperty("keycloak.theme.dir");
+        if (themeDir == null) {
+            String jbossConfigDir = System.getProperty("jboss.server.config.dir");
+            if (jbossConfigDir != null) {
+                themeDir = jbossConfigDir + File.separator + "themes";
+                System.setProperty("keycloak.theme.dir", themeDir);
+            }
+        }
     }
 
     public void importRealm(KeycloakSession session, RealmRepresentation rep) {

services/pom.xml 16(+16 -0)

diff --git a/services/pom.xml b/services/pom.xml
index 7502bbe..d97d6b4 100755
--- a/services/pom.xml
+++ b/services/pom.xml
@@ -32,6 +32,22 @@
         </dependency>
         <dependency>
             <groupId>org.keycloak</groupId>
+            <artifactId>keycloak-forms-common-freemarker</artifactId>
+            <version>${project.version}</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.keycloak</groupId>
+            <artifactId>keycloak-account-api</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.keycloak</groupId>
+            <artifactId>keycloak-login-api</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.keycloak</groupId>
             <artifactId>keycloak-model-api</artifactId>
             <version>${project.version}</version>
         </dependency>
diff --git a/services/src/main/java/org/keycloak/services/email/EmailSender.java b/services/src/main/java/org/keycloak/services/email/EmailSender.java
index ea76c3d..694157a 100755
--- a/services/src/main/java/org/keycloak/services/email/EmailSender.java
+++ b/services/src/main/java/org/keycloak/services/email/EmailSender.java
@@ -103,9 +103,9 @@ public class EmailSender {
         }
     }
 
-    public void sendEmailVerification(UserModel user, RealmModel realm, AccessCodeEntry accessCode, UriInfo uriInfo) throws EmailException {
+    public void sendEmailVerification(UserModel user, RealmModel realm, String accessCodeId, UriInfo uriInfo) throws EmailException {
         UriBuilder builder = Urls.loginActionEmailVerificationBuilder(uriInfo.getBaseUri());
-        builder.queryParam("key", accessCode.getId());
+        builder.queryParam("key", accessCodeId);
 
         URI uri = builder.build(realm.getName());
 
diff --git a/services/src/main/java/org/keycloak/services/managers/ApplianceBootstrap.java b/services/src/main/java/org/keycloak/services/managers/ApplianceBootstrap.java
index f557310..78ee938 100755
--- a/services/src/main/java/org/keycloak/services/managers/ApplianceBootstrap.java
+++ b/services/src/main/java/org/keycloak/services/managers/ApplianceBootstrap.java
@@ -55,6 +55,9 @@ public class ApplianceBootstrap {
         realm.setRegistrationAllowed(false);
         manager.generateRealmKeys(realm);
 
+        realm.setLoginTheme("keycloak");
+        realm.setAccountTheme("keycloak");
+
         ApplicationModel adminConsole = realm.addApplication(Constants.ADMIN_CONSOLE_APPLICATION);
         adminConsole.setEnabled(true);
         UserCredentialModel adminConsolePassword = new UserCredentialModel();
diff --git a/services/src/main/java/org/keycloak/services/managers/ModelToRepresentation.java b/services/src/main/java/org/keycloak/services/managers/ModelToRepresentation.java
index 7b14523..a706360 100755
--- a/services/src/main/java/org/keycloak/services/managers/ModelToRepresentation.java
+++ b/services/src/main/java/org/keycloak/services/managers/ModelToRepresentation.java
@@ -73,6 +73,8 @@ public class ModelToRepresentation {
         rep.setAccessCodeLifespanUserAction(realm.getAccessCodeLifespanUserAction());
         rep.setSmtpServer(realm.getSmtpConfig());
         rep.setSocialProviders(realm.getSocialConfig());
+        rep.setAccountTheme(realm.getAccountTheme());
+        rep.setLoginTheme(realm.getLoginTheme());
         if (realm.getPasswordPolicy() != null) {
             rep.setPasswordPolicy(realm.getPasswordPolicy().toString());
         }
diff --git a/services/src/main/java/org/keycloak/services/managers/RealmManager.java b/services/src/main/java/org/keycloak/services/managers/RealmManager.java
index 8c55586..0789941 100755
--- a/services/src/main/java/org/keycloak/services/managers/RealmManager.java
+++ b/services/src/main/java/org/keycloak/services/managers/RealmManager.java
@@ -121,6 +121,8 @@ public class RealmManager {
         if (rep.getRequiredApplicationCredentials() != null) {
             realm.updateRequiredApplicationCredentials(rep.getRequiredApplicationCredentials());
         }
+        realm.setLoginTheme(rep.getLoginTheme());
+        realm.setAccountTheme(rep.getAccountTheme());
 
         realm.setPasswordPolicy(new PasswordPolicy(rep.getPasswordPolicy()));
 
@@ -198,6 +200,8 @@ public class RealmManager {
             newRealm.setPrivateKeyPem(rep.getPrivateKey());
             newRealm.setPublicKeyPem(rep.getPublicKey());
         }
+        newRealm.setLoginTheme(rep.getLoginTheme());
+        newRealm.setAccountTheme(rep.getAccountTheme());
 
         Map<String, UserModel> userMap = new HashMap<String, UserModel>();
 
diff --git a/services/src/main/java/org/keycloak/services/resources/AccountService.java b/services/src/main/java/org/keycloak/services/resources/AccountService.java
index ef86c57..e63262a 100755
--- a/services/src/main/java/org/keycloak/services/resources/AccountService.java
+++ b/services/src/main/java/org/keycloak/services/resources/AccountService.java
@@ -24,6 +24,9 @@ package org.keycloak.services.resources;
 import org.jboss.resteasy.logging.Logger;
 import org.jboss.resteasy.spi.HttpRequest;
 import org.keycloak.AbstractOAuthClient;
+import org.keycloak.account.Account;
+import org.keycloak.account.AccountLoader;
+import org.keycloak.account.AccountPages;
 import org.keycloak.jaxrs.JaxrsOAuthClient;
 import org.keycloak.jose.jws.JWSInput;
 import org.keycloak.jose.jws.crypto.RSAProvider;
@@ -37,14 +40,11 @@ import org.keycloak.services.managers.ModelToRepresentation;
 import org.keycloak.services.managers.TokenManager;
 import org.keycloak.services.messages.Messages;
 import org.keycloak.services.resources.flows.Flows;
-import org.keycloak.services.resources.flows.FormFlows;
-import org.keycloak.services.resources.flows.Pages;
 import org.keycloak.services.resources.flows.Urls;
 import org.keycloak.services.validation.Validation;
 
 import javax.ws.rs.*;
 import javax.ws.rs.core.*;
-import javax.ws.rs.ext.Providers;
 import java.net.URI;
 import java.util.List;
 
@@ -68,9 +68,6 @@ public class AccountService {
     @Context
     private UriInfo uriInfo;
 
-    @Context
-    private Providers providers;
-
     private AuthenticationManager authManager = new AuthenticationManager();
 
     private ApplicationModel application;
@@ -83,28 +80,28 @@ public class AccountService {
         this.tokenManager = tokenManager;
     }
 
-    private Response forwardToPage(String path, String template) {
+    private Response forwardToPage(String path, AccountPages page) {
         AuthenticationManager.Auth auth = getAuth(false);
         if (auth != null) {
             if (!hasAccess(auth)) {
                 return noAccess();
             }
 
-            FormFlows forms = Flows.forms(realm, request, uriInfo).setUser(auth.getUser());
+            Account account = AccountLoader.load().createAccount(uriInfo).setRealm(realm).setUser(auth.getUser());
 
             String referrer = getReferrer();
             if (referrer != null) {
-                forms.setQueryParam("referrer", referrer);
+                account.setReferrer(referrer);
             }
 
-            return forms.forwardToForm(template);
+            return account.createResponse(page);
         } else {
             return login(path);
         }
     }
 
     private Response noAccess() {
-        return Flows.forms(realm, request, uriInfo).setError("No access").forwardToErrorPage();
+        return Flows.forms(realm, request, uriInfo).setError("No access").createErrorPage();
     }
 
     @Path("/")
@@ -118,7 +115,7 @@ public class AccountService {
     public Response accountPage() {
         List<MediaType> types = headers.getAcceptableMediaTypes();
         if (types.contains(MediaType.WILDCARD_TYPE) || (types.contains(MediaType.TEXT_HTML_TYPE))) {
-            return forwardToPage(null, Pages.ACCOUNT);
+            return forwardToPage(null, AccountPages.ACCOUNT);
         } else if (types.contains(MediaType.APPLICATION_JSON_TYPE)) {
             AuthenticationManager.Auth auth = getAuth(true);
             if (!hasAccess(auth, Constants.ACCOUNT_PROFILE_ROLE)) {
@@ -130,28 +127,16 @@ public class AccountService {
         }
     }
 
-    @Path("social")
-    @GET
-    public Response socialPage() {
-        return forwardToPage("social", Pages.SOCIAL);
-    }
-
     @Path("totp")
     @GET
     public Response totpPage() {
-        return forwardToPage("totp", Pages.TOTP);
+        return forwardToPage("totp", AccountPages.TOTP);
     }
 
     @Path("password")
     @GET
     public Response passwordPage() {
-        return forwardToPage("password", Pages.PASSWORD);
-    }
-
-    @Path("access")
-    @GET
-    public Response accessPage() {
-        return forwardToPage("access", Pages.ACCESS);
+        return forwardToPage("password", AccountPages.PASSWORD);
     }
 
     @Path("/")
@@ -165,16 +150,18 @@ public class AccountService {
 
         UserModel user = auth.getUser();
 
+        Account account = AccountLoader.load().createAccount(uriInfo).setRealm(realm).setUser(auth.getUser());
+
         String error = Validation.validateUpdateProfileForm(formData);
         if (error != null) {
-            return Flows.forms(realm, request, uriInfo).setUser(user).setError(error).forwardToAccount();
+            return account.setError(error).createResponse(AccountPages.ACCOUNT);
         }
 
         user.setFirstName(formData.getFirst("firstName"));
         user.setLastName(formData.getFirst("lastName"));
         user.setEmail(formData.getFirst("email"));
 
-        return Flows.forms(realm, request, uriInfo).setUser(user).setSuccess("accountUpdated").forwardToAccount();
+        return account.setSuccess("accountUpdated").createResponse(AccountPages.ACCOUNT);
     }
 
     @Path("totp-remove")
@@ -186,9 +173,10 @@ public class AccountService {
         }
 
         UserModel user = auth.getUser();
-
         user.setTotp(false);
-        return Flows.forms(realm, request, uriInfo).setSuccess("successTotpRemoved").setUser(user).forwardToTotp();
+
+        Account account = AccountLoader.load().createAccount(uriInfo).setRealm(realm).setUser(auth.getUser());
+        return account.setSuccess("successTotpRemoved").createResponse(AccountPages.TOTP);
     }
 
     @Path("totp")
@@ -205,11 +193,12 @@ public class AccountService {
         String totp = formData.getFirst("totp");
         String totpSecret = formData.getFirst("totpSecret");
 
-        FormFlows forms = Flows.forms(realm, request, uriInfo).setUser(user);
+        Account account = AccountLoader.load().createAccount(uriInfo).setRealm(realm).setUser(auth.getUser());
+
         if (Validation.isEmpty(totp)) {
-            return forms.setError(Messages.MISSING_TOTP).forwardToTotp();
+            return account.setError(Messages.MISSING_TOTP).createResponse(AccountPages.TOTP);
         } else if (!new TimeBasedOTP().validate(totp, totpSecret.getBytes())) {
-            return forms.setError(Messages.INVALID_TOTP).forwardToTotp();
+            return account.setError(Messages.INVALID_TOTP).createResponse(AccountPages.TOTP);
         }
 
         UserCredentialModel credentials = new UserCredentialModel();
@@ -219,7 +208,7 @@ public class AccountService {
 
         user.setTotp(true);
 
-        return Flows.forms(realm, request, uriInfo).setSuccess("successTotp").setUser(user).forwardToTotp();
+        return account.setSuccess("successTotp").createResponse(AccountPages.TOTP);
     }
 
     @Path("password")
@@ -233,27 +222,27 @@ public class AccountService {
 
         UserModel user = auth.getUser();
 
-        FormFlows forms = Flows.forms(realm, request, uriInfo).setUser(user);
+        Account account = AccountLoader.load().createAccount(uriInfo).setRealm(realm).setUser(auth.getUser());
 
         String password = formData.getFirst("password");
         String passwordNew = formData.getFirst("password-new");
         String passwordConfirm = formData.getFirst("password-confirm");
 
         if (Validation.isEmpty(passwordNew)) {
-            return forms.setError(Messages.MISSING_PASSWORD).forwardToPassword();
+            return account.setError(Messages.MISSING_PASSWORD).createResponse(AccountPages.PASSWORD);
         } else if (!passwordNew.equals(passwordConfirm)) {
-            return forms.setError(Messages.INVALID_PASSWORD_CONFIRM).forwardToPassword();
+            return account.setError(Messages.INVALID_PASSWORD_CONFIRM).createResponse(AccountPages.PASSWORD);
         }
 
         if (Validation.isEmpty(password)) {
-            return forms.setError(Messages.MISSING_PASSWORD).forwardToPassword();
+            return account.setError(Messages.MISSING_PASSWORD).createResponse(AccountPages.PASSWORD);
         } else if (!realm.validatePassword(user, password)) {
-            return forms.setError(Messages.INVALID_PASSWORD_EXISTING).forwardToPassword();
+            return account.setError(Messages.INVALID_PASSWORD_EXISTING).createResponse(AccountPages.PASSWORD);
         }
 
         String error = Validation.validatePassword(formData, realm.getPasswordPolicy());
         if (error != null) {
-            return forms.setError(error).forwardToPassword();
+            return account.setError(error).createResponse(AccountPages.PASSWORD);
         }
 
         UserCredentialModel credentials = new UserCredentialModel();
@@ -262,7 +251,7 @@ public class AccountService {
 
         realm.updateCredential(user, credentials);
 
-        return Flows.forms(realm, request, uriInfo).setUser(user).setSuccess("accountPasswordUpdated").forwardToPassword();
+        return account.setSuccess("accountPasswordUpdated").createResponse(AccountPages.PASSWORD);
     }
 
     @Path("login-redirect")
diff --git a/services/src/main/java/org/keycloak/services/resources/admin/AdminService.java b/services/src/main/java/org/keycloak/services/resources/admin/AdminService.java
index a2cdc28..c5ded4d 100755
--- a/services/src/main/java/org/keycloak/services/resources/admin/AdminService.java
+++ b/services/src/main/java/org/keycloak/services/resources/admin/AdminService.java
@@ -24,11 +24,8 @@ import org.keycloak.services.messages.Messages;
 import org.keycloak.services.resources.TokenService;
 import org.keycloak.services.resources.flows.Flows;
 import org.keycloak.services.resources.flows.OAuthFlows;
-import org.keycloak.util.KeycloakUriBuilder;
 
-import javax.ws.rs.BadRequestException;
 import javax.ws.rs.Consumes;
-import javax.ws.rs.ForbiddenException;
 import javax.ws.rs.GET;
 import javax.ws.rs.NotAuthorizedException;
 import javax.ws.rs.NotFoundException;
@@ -200,6 +197,30 @@ public class AdminService {
         return adminResource;
     }
 
+    @Path("serverinfo")
+    public ServerInfoAdminResource getServerInfo(@Context final HttpHeaders headers) {
+        RealmManager realmManager = new RealmManager(session);
+        RealmModel saasRealm = getAdminstrationRealm(realmManager);
+        if (saasRealm == null)
+            throw new NotFoundException();
+        UserModel admin = authManager.authenticateSaasIdentity(saasRealm, uriInfo, headers);
+        if (admin == null) {
+            throw new NotAuthorizedException("Bearer");
+        }
+        ApplicationModel adminConsole = saasRealm.getApplicationNameMap().get(Constants.ADMIN_CONSOLE_APPLICATION);
+        if (adminConsole == null) {
+            throw new NotFoundException();
+        }
+        RoleModel adminRole = adminConsole.getRole(Constants.ADMIN_CONSOLE_ADMIN_ROLE);
+        if (!saasRealm.hasRole(admin, adminRole)) {
+            logger.warn("not a Realm admin");
+            throw new NotAuthorizedException("Bearer");
+        }
+        ServerInfoAdminResource adminResource = new ServerInfoAdminResource();
+        resourceContext.initResource(adminResource);
+        return adminResource;
+    }
+
     @Path("login")
     @GET
     @NoCache
@@ -226,7 +247,7 @@ public class AdminService {
     public Response errorOnLoginRedirect(@QueryParam ("error") String message) {
         RealmManager realmManager = new RealmManager(session);
         RealmModel realm = getAdminstrationRealm(realmManager);
-        return Flows.forms(realm, request, uriInfo).setError(message).forwardToErrorPage();
+        return Flows.forms(realm, request, uriInfo).setError(message).createErrorPage();
     }
 
     protected Response redirectOnLoginError(String message) {
@@ -373,13 +394,11 @@ public class AdminService {
                 NewCookie cookie = authManager.createSaasIdentityCookie(realm, user, uriInfo);
                 return Response.status(302).cookie(cookie).location(contextRoot(uriInfo).path(adminPath).build()).build();
             case ACCOUNT_DISABLED:
-                return Flows.forms(realm, request, uriInfo).setError(Messages.ACCOUNT_DISABLED).setFormData(formData)
-                        .forwardToLogin();
+                return Flows.forms(realm, request, uriInfo).setError(Messages.ACCOUNT_DISABLED).setFormData(formData).createLogin();
             case ACTIONS_REQUIRED:
                 return oauth.processAccessCode(null, "n", contextRoot(uriInfo).path(adminPath).build().toString(), adminConsoleUser, user);
             default:
-                return Flows.forms(realm, request, uriInfo).setError(Messages.INVALID_USER).setFormData(formData)
-                        .forwardToLogin();
+                return Flows.forms(realm, request, uriInfo).setError(Messages.INVALID_USER).setFormData(formData).createLogin();
         }
     }
 
diff --git a/services/src/main/java/org/keycloak/services/resources/admin/ServerInfoAdminResource.java b/services/src/main/java/org/keycloak/services/resources/admin/ServerInfoAdminResource.java
new file mode 100644
index 0000000..04cda21
--- /dev/null
+++ b/services/src/main/java/org/keycloak/services/resources/admin/ServerInfoAdminResource.java
@@ -0,0 +1,68 @@
+package org.keycloak.services.resources.admin;
+
+import org.keycloak.freemarker.Theme;
+import org.keycloak.freemarker.ThemeProvider;
+import org.keycloak.social.SocialProvider;
+import org.keycloak.util.ProviderLoader;
+
+import javax.ws.rs.GET;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+public class ServerInfoAdminResource {
+
+    @GET
+    public ServerInfoRepresentation getInfo() {
+        ServerInfoRepresentation info = new ServerInfoRepresentation();
+        setSocialProviders(info);
+        setThemes(info);
+        return info;
+    }
+
+    private void setThemes(ServerInfoRepresentation info) {
+        Iterable<ThemeProvider> providers = ProviderLoader.load(ThemeProvider.class);
+        info.themes = new HashMap<String, List<String>>();
+        for (Theme.Type type : Theme.Type.values()) {
+            List<String> themes = new LinkedList<String>();
+            for (ThemeProvider p : providers) {
+                themes.addAll(p.nameSet(type));
+            }
+            Collections.sort(themes);
+            info.themes.put(type.toString().toLowerCase(), themes);
+        }
+    }
+
+    private void setSocialProviders(ServerInfoRepresentation info) {
+        info.socialProviders = new LinkedList<String>();
+        for (SocialProvider p : ProviderLoader.load(SocialProvider.class)) {
+            info.socialProviders.add(p.getId());
+        }
+        Collections.sort(info.socialProviders);
+    }
+
+    public static class ServerInfoRepresentation {
+
+        private Map<String, List<String>> themes;
+
+        private List<String> socialProviders;
+
+        public ServerInfoRepresentation() {
+        }
+
+        public Map<String, List<String>> getThemes() {
+            return themes;
+        }
+
+        public List<String> getSocialProviders() {
+            return socialProviders;
+        }
+
+    }
+
+}
diff --git a/services/src/main/java/org/keycloak/services/resources/flows/Flows.java b/services/src/main/java/org/keycloak/services/resources/flows/Flows.java
index e46632a..dd2e26a 100755
--- a/services/src/main/java/org/keycloak/services/resources/flows/Flows.java
+++ b/services/src/main/java/org/keycloak/services/resources/flows/Flows.java
@@ -22,6 +22,8 @@
 package org.keycloak.services.resources.flows;
 
 import org.jboss.resteasy.spi.HttpRequest;
+import org.keycloak.login.LoginForms;
+import org.keycloak.login.FormsLoader;
 import org.keycloak.models.RealmModel;
 import org.keycloak.services.managers.AuthenticationManager;
 import org.keycloak.services.managers.TokenManager;
@@ -36,8 +38,8 @@ public class Flows {
     private Flows() {
     }
 
-    public static FormFlows forms(RealmModel realm, HttpRequest request, UriInfo uriInfo) {
-        return new FormFlows(realm, request, uriInfo);
+    public static LoginForms forms(RealmModel realm, HttpRequest request, UriInfo uriInfo) {
+        return FormsLoader.load().createForms(realm, request, uriInfo);
     }
 
     public static OAuthFlows oauth(RealmModel realm, HttpRequest request, UriInfo uriInfo, AuthenticationManager authManager,
diff --git a/services/src/main/java/org/keycloak/services/resources/flows/OAuthFlows.java b/services/src/main/java/org/keycloak/services/resources/flows/OAuthFlows.java
index 0ed81dc..a18540f 100755
--- a/services/src/main/java/org/keycloak/services/resources/flows/OAuthFlows.java
+++ b/services/src/main/java/org/keycloak/services/resources/flows/OAuthFlows.java
@@ -103,14 +103,16 @@ public class OAuthFlows {
         if (!requiredActions.isEmpty()) {
             accessCode.setRequiredActions(new HashSet<UserModel.RequiredAction>(requiredActions));
             accessCode.setExpiration(System.currentTimeMillis() / 1000 + realm.getAccessCodeLifespanUserAction());
-            return Flows.forms(realm, request, uriInfo).setAccessCode(accessCode).setUser(user)
-                    .forwardToAction(user.getRequiredActions().iterator().next());
+            return Flows.forms(realm, request, uriInfo).setAccessCode(accessCode.getId(), accessCode.getCode()).setUser(user)
+                    .createResponse(user.getRequiredActions().iterator().next());
         }
 
         if (!isResource
                 && (accessCode.getRealmRolesRequested().size() > 0 || accessCode.getResourceRolesRequested().size() > 0)) {
             accessCode.setExpiration(System.currentTimeMillis() / 1000 + realm.getAccessCodeLifespanUserAction());
-            return oauthGrantPage(accessCode, client);
+            return Flows.forms(realm, request, uriInfo).setAccessCode(accessCode.getId(), accessCode.getCode()).
+                    setAccessRequest(accessCode.getRealmRolesRequested(), accessCode.getResourceRolesRequested()).
+                    setClient(client).createOAuthGrant();
         }
 
         if (redirect != null) {
@@ -120,18 +122,8 @@ public class OAuthFlows {
         }
     }
 
-    public Response oauthGrantPage(AccessCodeEntry accessCode, UserModel client) {
-        request.setAttribute("realmRolesRequested", accessCode.getRealmRolesRequested());
-        request.setAttribute("resourceRolesRequested", accessCode.getResourceRolesRequested());
-        request.setAttribute("client", client);
-        request.setAttribute("action", TokenService.processOAuthUrl(uriInfo).build(realm.getName()).toString());
-        request.setAttribute("code", accessCode.getCode());
-
-        return Flows.forms(realm, request, uriInfo).setAccessCode(accessCode).forwardToOAuthGrant();
-    }
-
     public Response forwardToSecurityFailure(String message) {
-        return Flows.forms(realm, request, uriInfo).setError(message).forwardToErrorPage();
+        return Flows.forms(realm, request, uriInfo).setError(message).createErrorPage();
     }
 
 }
diff --git a/services/src/main/java/org/keycloak/services/resources/flows/Urls.java b/services/src/main/java/org/keycloak/services/resources/flows/Urls.java
index 7859c78..23fa68e 100755
--- a/services/src/main/java/org/keycloak/services/resources/flows/Urls.java
+++ b/services/src/main/java/org/keycloak/services/resources/flows/Urls.java
@@ -22,6 +22,7 @@
 package org.keycloak.services.resources.flows;
 
 import org.keycloak.services.resources.AccountService;
+import org.keycloak.services.resources.ThemeResource;
 import org.keycloak.services.resources.admin.AdminService;
 import org.keycloak.services.resources.RealmsResource;
 import org.keycloak.services.resources.RequiredActionsService;
@@ -29,6 +30,7 @@ import org.keycloak.services.resources.SocialResource;
 import org.keycloak.services.resources.TokenService;
 
 import javax.ws.rs.core.UriBuilder;
+import javax.ws.rs.core.UriInfo;
 import java.net.URI;
 
 /**
@@ -128,6 +130,10 @@ public class Urls {
         return tokenBase(baseUri).path(TokenService.class, "registerPage").build(realmId);
     }
 
+    public static URI realmOauthAction(URI baseUri, String realmId) {
+        return tokenBase(baseUri).path(TokenService.class, "processOAuth").build(realmId);
+    }
+
     public static URI realmCode(URI baseUri, String realmId) {
         return tokenBase(baseUri).path(TokenService.class, "accessCodeToToken").build(realmId);
     }
@@ -165,6 +171,10 @@ public class Urls {
                 .build(realmId);
     }
 
+    public static URI themeRoot(URI baseUri) {
+        return themeBase(baseUri).build();
+    }
+
     private static UriBuilder requiredActionsBase(URI baseUri) {
         return tokenBase(baseUri).path(TokenService.class, "getRequiredActionsService");
     }
@@ -172,4 +182,8 @@ public class Urls {
     private static UriBuilder tokenBase(URI baseUri) {
         return realmBase(baseUri).path(RealmsResource.class, "getTokenService");
     }
+
+    private static UriBuilder themeBase(URI baseUri) {
+        return UriBuilder.fromUri(baseUri).path(ThemeResource.class);
+    }
 }
diff --git a/services/src/main/java/org/keycloak/services/resources/KeycloakApplication.java b/services/src/main/java/org/keycloak/services/resources/KeycloakApplication.java
index e41cfc6..6c89c12 100755
--- a/services/src/main/java/org/keycloak/services/resources/KeycloakApplication.java
+++ b/services/src/main/java/org/keycloak/services/resources/KeycloakApplication.java
@@ -48,6 +48,7 @@ public class KeycloakApplication extends Application {
         singletons.add(new SocialResource(tokenManager, new SocialRequestManager()));
         classes.add(SkeletonKeyContextResolver.class);
         classes.add(QRCodeResource.class);
+        classes.add(ThemeResource.class);
 
         setupDefaultRealm();
     }
diff --git a/services/src/main/java/org/keycloak/services/resources/RequiredActionsService.java b/services/src/main/java/org/keycloak/services/resources/RequiredActionsService.java
index 327c02d..b38df73 100755
--- a/services/src/main/java/org/keycloak/services/resources/RequiredActionsService.java
+++ b/services/src/main/java/org/keycloak/services/resources/RequiredActionsService.java
@@ -23,6 +23,7 @@ package org.keycloak.services.resources;
 
 import org.jboss.resteasy.logging.Logger;
 import org.jboss.resteasy.spi.HttpRequest;
+import org.keycloak.login.LoginForms;
 import org.keycloak.jose.jws.JWSInput;
 import org.keycloak.jose.jws.crypto.RSAProvider;
 import org.keycloak.models.RealmModel;
@@ -38,7 +39,6 @@ import org.keycloak.services.managers.AuthenticationManager;
 import org.keycloak.services.managers.TokenManager;
 import org.keycloak.services.messages.Messages;
 import org.keycloak.services.resources.flows.Flows;
-import org.keycloak.services.resources.flows.FormFlows;
 import org.keycloak.services.validation.Validation;
 
 import javax.ws.rs.Consumes;
@@ -97,7 +97,7 @@ public class RequiredActionsService {
 
         String error = Validation.validateUpdateProfileForm(formData);
         if (error != null) {
-            return Flows.forms(realm, request, uriInfo).setError(error).forwardToAction(RequiredAction.UPDATE_PROFILE);
+            return Flows.forms(realm, request, uriInfo).setUser(user).setError(error).createResponse(RequiredAction.UPDATE_PROFILE);
         }
 
         user.setFirstName(formData.getFirst("firstName"));
@@ -124,11 +124,11 @@ public class RequiredActionsService {
         String totp = formData.getFirst("totp");
         String totpSecret = formData.getFirst("totpSecret");
 
-        FormFlows forms = Flows.forms(realm, request, uriInfo).setUser(user);
+        LoginForms loginForms = Flows.forms(realm, request, uriInfo).setUser(user);
         if (Validation.isEmpty(totp)) {
-            return forms.setError(Messages.MISSING_TOTP).forwardToAction(RequiredAction.CONFIGURE_TOTP);
+            return loginForms.setError(Messages.MISSING_TOTP).createResponse(RequiredAction.CONFIGURE_TOTP);
         } else if (!new TimeBasedOTP().validate(totp, totpSecret.getBytes())) {
-            return forms.setError(Messages.INVALID_TOTP).forwardToAction(RequiredAction.CONFIGURE_TOTP);
+            return loginForms.setError(Messages.INVALID_TOTP).createResponse(RequiredAction.CONFIGURE_TOTP);
         }
 
         UserCredentialModel credentials = new UserCredentialModel();
@@ -161,16 +161,16 @@ public class RequiredActionsService {
         String passwordNew = formData.getFirst("password-new");
         String passwordConfirm = formData.getFirst("password-confirm");
 
-        FormFlows forms = Flows.forms(realm, request, uriInfo).setUser(user);
+        LoginForms loginForms = Flows.forms(realm, request, uriInfo).setUser(user);
         if (Validation.isEmpty(passwordNew)) {
-            return forms.setError(Messages.MISSING_PASSWORD).forwardToAction(RequiredAction.UPDATE_PASSWORD);
+            return loginForms.setError(Messages.MISSING_PASSWORD).createResponse(RequiredAction.UPDATE_PASSWORD);
         } else if (!passwordNew.equals(passwordConfirm)) {
-            return forms.setError(Messages.NOTMATCH_PASSWORD).forwardToAction(RequiredAction.UPDATE_PASSWORD);
+            return loginForms.setError(Messages.NOTMATCH_PASSWORD).createResponse(RequiredAction.UPDATE_PASSWORD);
         }
 
         String error = realm.getPasswordPolicy().validate(passwordNew);
         if (error != null) {
-            return forms.setError(error).forwardToAction(RequiredAction.UPDATE_PASSWORD);
+            return loginForms.setError(error).createResponse(RequiredAction.UPDATE_PASSWORD);
         }
 
         UserCredentialModel credentials = new UserCredentialModel();
@@ -186,11 +186,7 @@ public class RequiredActionsService {
             accessCode.getRequiredActions().remove(RequiredAction.UPDATE_PASSWORD);
         }
 
-        if (accessCode != null) {
-            return redirectOauth(user, accessCode);
-        } else {
-            return Flows.forms(realm, request, uriInfo).setUser(user).forwardToPassword();
-        }
+        return redirectOauth(user, accessCode);
     }
 
 
@@ -217,8 +213,8 @@ public class RequiredActionsService {
                 return unauthorized();
             }
 
-            return Flows.forms(realm, request, uriInfo).setAccessCode(accessCode).setUser(accessCode.getUser())
-                    .forwardToAction(RequiredAction.VERIFY_EMAIL);
+            return Flows.forms(realm, request, uriInfo).setAccessCode(accessCode.getId(), accessCode.getCode()).setUser(accessCode.getUser())
+                    .createResponse(RequiredAction.VERIFY_EMAIL);
         }
     }
 
@@ -231,9 +227,9 @@ public class RequiredActionsService {
                     || !accessCode.getRequiredActions().contains(RequiredAction.UPDATE_PASSWORD)) {
                 return unauthorized();
             }
-            return Flows.forms(realm, request, uriInfo).setAccessCode(accessCode).forwardToAction(RequiredAction.UPDATE_PASSWORD);
+            return Flows.forms(realm, request, uriInfo).setAccessCode(accessCode.getId(), accessCode.getCode()).createResponse(RequiredAction.UPDATE_PASSWORD);
         } else {
-            return Flows.forms(realm, request, uriInfo).forwardToPasswordReset();
+            return Flows.forms(realm, request, uriInfo).createPasswordReset();
         }
     }
 
@@ -260,7 +256,7 @@ public class RequiredActionsService {
 
         UserModel user = realm.getUserByEmail(email);
         if (user == null) {
-            return Flows.forms(realm, request, uriInfo).setError("emailError").forwardToPasswordReset();
+            return Flows.forms(realm, request, uriInfo).setError("emailError").createPasswordReset();
         }
 
         Set<RequiredAction> requiredActions = new HashSet<RequiredAction>(user.getRequiredActions());
@@ -274,17 +270,17 @@ public class RequiredActionsService {
             new EmailSender(realm.getSmtpConfig()).sendPasswordReset(user, realm, accessCode, uriInfo);
         } catch (EmailException e) {
             logger.error("Failed to send password reset email", e);
-            return Flows.forms(realm, request, uriInfo).setError("emailSendError").forwardToErrorPage();
+            return Flows.forms(realm, request, uriInfo).setError("emailSendError").createErrorPage();
         }
 
-        return Flows.forms(realm, request, uriInfo).setSuccess("emailSent").forwardToPasswordReset();
+        return Flows.forms(realm, request, uriInfo).setSuccess("emailSent").createPasswordReset();
     }
 
 
     @Path("username-reminder")
     @GET
     public Response usernameReminder() {
-        return Flows.forms(realm, request, uriInfo).forwardToUsernameReminder();
+        return Flows.forms(realm, request, uriInfo).createUsernameReminder();
     }
 
     @Path("username-reminder")
@@ -306,21 +302,21 @@ public class RequiredActionsService {
 
         UserModel user = realm.getUserByEmail(email);
         if (user == null) {
-            return Flows.forms(realm, request, uriInfo).setError("emailError").forwardToUsernameReminder();
+            return Flows.forms(realm, request, uriInfo).setError("emailError").createUsernameReminder();
         }
 
         try {
             new EmailSender(realm.getSmtpConfig()).sendUsernameReminder(user);
         } catch (EmailException e) {
             logger.error("Failed to send username reminder email", e);
-            return Flows.forms(realm, request, uriInfo).setError("emailSendError").forwardToErrorPage();
+            return Flows.forms(realm, request, uriInfo).setError("emailSendError").createErrorPage();
         }
 
-        return Flows.forms(realm, request, uriInfo).setSuccess("emailUsernameSent").forwardToLogin();
+        return Flows.forms(realm, request, uriInfo).setSuccess("emailUsernameSent").createLogin();
     }
 
     private AccessCodeEntry getAccessCodeEntry(RequiredAction requiredAction) {
-        String code = uriInfo.getQueryParameters().getFirst(FormFlows.CODE);
+        String code = uriInfo.getQueryParameters().getFirst("code");
         if (code == null) {
             logger.debug("getAccessCodeEntry code as not in query param");
             return null;
@@ -373,8 +369,8 @@ public class RequiredActionsService {
 
         Set<RequiredAction> requiredActions = user.getRequiredActions();
         if (!requiredActions.isEmpty()) {
-            return Flows.forms(realm, request, uriInfo).setAccessCode(accessCode).setUser(user)
-                    .forwardToAction(requiredActions.iterator().next());
+            return Flows.forms(realm, request, uriInfo).setAccessCode(accessCode.getId(), accessCode.getCode()).setUser(user)
+                    .createResponse(requiredActions.iterator().next());
         } else {
             logger.debug("redirectOauth: redirecting to: {0}", accessCode.getRedirectUri());
             accessCode.setExpiration((System.currentTimeMillis() / 1000) + realm.getAccessCodeLifespan());
@@ -384,7 +380,7 @@ public class RequiredActionsService {
     }
 
     private Response unauthorized() {
-        return Flows.forms(realm, request, uriInfo).setError("Unauthorized request").forwardToErrorPage();
+        return Flows.forms(realm, request, uriInfo).setError("Unauthorized request").createErrorPage();
     }
 
 }
diff --git a/services/src/main/java/org/keycloak/services/resources/SocialResource.java b/services/src/main/java/org/keycloak/services/resources/SocialResource.java
index 770d386..a4efab2 100755
--- a/services/src/main/java/org/keycloak/services/resources/SocialResource.java
+++ b/services/src/main/java/org/keycloak/services/resources/SocialResource.java
@@ -22,7 +22,6 @@
 package org.keycloak.services.resources;
 
 import org.jboss.resteasy.logging.Logger;
-import org.jboss.resteasy.specimpl.MultivaluedMapImpl;
 import org.jboss.resteasy.spi.HttpRequest;
 import org.jboss.resteasy.spi.HttpResponse;
 import org.keycloak.models.KeycloakSession;
@@ -38,7 +37,6 @@ import org.keycloak.services.resources.flows.Urls;
 import org.keycloak.social.AuthCallback;
 import org.keycloak.social.AuthRequest;
 import org.keycloak.social.RequestDetails;
-import org.keycloak.social.SocialConstants;
 import org.keycloak.social.SocialLoader;
 import org.keycloak.social.SocialProvider;
 import org.keycloak.social.SocialProviderConfig;
@@ -46,19 +44,13 @@ import org.keycloak.social.SocialProviderException;
 import org.keycloak.services.managers.SocialRequestManager;
 import org.keycloak.social.SocialUser;
 
-import javax.ws.rs.Consumes;
 import javax.ws.rs.GET;
-import javax.ws.rs.POST;
 import javax.ws.rs.Path;
 import javax.ws.rs.PathParam;
 import javax.ws.rs.QueryParam;
 import javax.ws.rs.container.ResourceContext;
 import javax.ws.rs.core.Context;
-import javax.ws.rs.core.Cookie;
 import javax.ws.rs.core.HttpHeaders;
-import javax.ws.rs.core.MediaType;
-import javax.ws.rs.core.MultivaluedMap;
-import javax.ws.rs.core.NewCookie;
 import javax.ws.rs.core.Response;
 import javax.ws.rs.core.Response.Status;
 import javax.ws.rs.core.UriInfo;
@@ -67,7 +59,6 @@ import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Map.Entry;
-import java.util.UUID;
 
 /**
  * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
@@ -204,7 +195,7 @@ public class SocialResource {
 
         SocialProvider provider = SocialLoader.load(providerId);
         if (provider == null) {
-            return Flows.forms(realm, request, uriInfo).setError("Social provider not found").forwardToErrorPage();
+            return Flows.forms(realm, request, uriInfo).setError("Social provider not found").createErrorPage();
         }
 
         String key = realm.getSocialConfig().get(providerId + ".key");
@@ -216,16 +207,16 @@ public class SocialResource {
         UserModel client = realm.getUser(clientId);
         if (client == null) {
             logger.warn("Unknown login requester: " + clientId);
-            return Flows.forms(realm, request, uriInfo).setError("Unknown login requester.").forwardToErrorPage();
+            return Flows.forms(realm, request, uriInfo).setError("Unknown login requester.").createErrorPage();
         }
 
         if (!client.isEnabled()) {
             logger.warn("Login requester not enabled.");
-            return Flows.forms(realm, request, uriInfo).setError("Login requester not enabled.").forwardToErrorPage();
+            return Flows.forms(realm, request, uriInfo).setError("Login requester not enabled.").createErrorPage();
         }
         redirectUri = TokenService.verifyRedirectUri(redirectUri, client);
         if (redirectUri == null) {
-            return Flows.forms(realm, request, uriInfo).setError("Invalid redirect_uri.").forwardToErrorPage();
+            return Flows.forms(realm, request, uriInfo).setError("Invalid redirect_uri.").createErrorPage();
         }
 
         try {
@@ -240,7 +231,7 @@ public class SocialResource {
 
             return Response.status(Status.FOUND).location(authRequest.getAuthUri()).build();
         } catch (Throwable t) {
-            return Flows.forms(realm, request, uriInfo).setError("Failed to redirect to social auth").forwardToErrorPage();
+            return Flows.forms(realm, request, uriInfo).setError("Failed to redirect to social auth").createErrorPage();
         }
     }
 
diff --git a/services/src/main/java/org/keycloak/services/resources/ThemeResource.java b/services/src/main/java/org/keycloak/services/resources/ThemeResource.java
new file mode 100755
index 0000000..4d06466
--- /dev/null
+++ b/services/src/main/java/org/keycloak/services/resources/ThemeResource.java
@@ -0,0 +1,44 @@
+package org.keycloak.services.resources;
+
+import org.jboss.resteasy.logging.Logger;
+import org.keycloak.freemarker.Theme;
+import org.keycloak.freemarker.ThemeLoader;
+
+import javax.activation.FileTypeMap;
+import javax.activation.MimetypesFileTypeMap;
+import javax.ws.rs.GET;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.core.Response;
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+@Path("/theme")
+public class ThemeResource {
+
+    private static final Logger logger = Logger.getLogger(ThemeResource.class);
+
+    private static FileTypeMap mimeTypes = MimetypesFileTypeMap.getDefaultFileTypeMap();
+
+    @GET
+    @Path("/{themType}/{themeName}/{path:.*}")
+    public Response getResource(@PathParam("themType") String themType, @PathParam("themeName") String themeName, @PathParam("path") String path) {
+        try {
+            Theme theme = ThemeLoader.createTheme(themeName, Theme.Type.valueOf(themType.toUpperCase()));
+            InputStream resource = theme.getResourceAsStream(path);
+            if (resource != null) {
+                return Response.ok(resource).type(mimeTypes.getContentType(path)).build();
+            } else {
+                return Response.status(Response.Status.NOT_FOUND).build();
+            }
+        } catch (Exception e) {
+            logger.warn("Failed to get theme resource", e);
+            return Response.serverError().build();
+        }
+    }
+
+
+}
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 15beb20..79992d6 100755
--- a/services/src/main/java/org/keycloak/services/resources/TokenService.java
+++ b/services/src/main/java/org/keycloak/services/resources/TokenService.java
@@ -7,7 +7,6 @@ import org.jboss.resteasy.spi.HttpResponse;
 import org.keycloak.jose.jws.JWSBuilder;
 import org.keycloak.jose.jws.JWSInput;
 import org.keycloak.jose.jws.crypto.RSAProvider;
-import org.keycloak.models.ApplicationModel;
 import org.keycloak.models.Constants;
 import org.keycloak.models.KeycloakSession;
 import org.keycloak.models.KeycloakTransaction;
@@ -228,8 +227,7 @@ public class TokenService {
         UserModel user = realm.getUser(username);
 
         if (user == null){
-            return Flows.forms(realm, request, uriInfo).setError(Messages.INVALID_USER).setFormData(formData)
-                    .forwardToLogin();
+            return Flows.forms(realm, request, uriInfo).setError(Messages.INVALID_USER).setFormData(formData).createLogin();
         }
 
         isTotpConfigurationRequired(user);
@@ -242,13 +240,11 @@ public class TokenService {
             case ACTIONS_REQUIRED:
                 return oauth.processAccessCode(scopeParam, state, redirect, client, user);
             case ACCOUNT_DISABLED:
-                return Flows.forms(realm, request, uriInfo).setError(Messages.ACCOUNT_DISABLED).setFormData(formData)
-                        .forwardToLogin();
+                return Flows.forms(realm, request, uriInfo).setError(Messages.ACCOUNT_DISABLED).setFormData(formData).createLogin();
             case MISSING_TOTP:
-                return Flows.forms(realm, request, uriInfo).setFormData(formData).forwardToLoginTotp();
+                return Flows.forms(realm, request, uriInfo).setFormData(formData).createLoginTotp();
             default:
-                return Flows.forms(realm, request, uriInfo).setError(Messages.INVALID_USER).setFormData(formData)
-                        .forwardToLogin();
+                return Flows.forms(realm, request, uriInfo).setError(Messages.INVALID_USER).setFormData(formData).createLogin();
         }
     }
 
@@ -319,14 +315,14 @@ public class TokenService {
         }
 
         if (error != null) {
-            return Flows.forms(realm, request, uriInfo).setError(error).setFormData(formData).forwardToRegistration();
+            return Flows.forms(realm, request, uriInfo).setError(error).setFormData(formData).createRegistration();
         }
 
         String username = formData.getFirst("username");
 
         UserModel user = realm.getUser(username);
         if (user != null) {
-            return Flows.forms(realm, request, uriInfo).setError(Messages.USERNAME_EXISTS).setFormData(formData).forwardToRegistration();
+            return Flows.forms(realm, request, uriInfo).setError(Messages.USERNAME_EXISTS).setFormData(formData).createRegistration();
         }
 
         user = realm.addUser(username);
@@ -517,8 +513,8 @@ public class TokenService {
         if (prompt != null && prompt.equals("none")) {
             return oauth.redirectError(client, "access_denied", state, redirect);
         }
-        logger.info("forwardToLogin() now...");
-        return Flows.forms(realm, request, uriInfo).forwardToLogin();
+        logger.info("createLogin() now...");
+        return Flows.forms(realm, request, uriInfo).createLogin();
     }
 
     @Path("registrations")
@@ -560,7 +556,7 @@ public class TokenService {
 
         authManager.expireIdentityCookie(realm, uriInfo);
 
-        return Flows.forms(realm, request, uriInfo).forwardToRegistration();
+        return Flows.forms(realm, request, uriInfo).createRegistration();
     }
 
     @Path("logout")
diff --git a/services/src/main/resources/META-INF/mime.types b/services/src/main/resources/META-INF/mime.types
new file mode 100644
index 0000000..432f4c8
--- /dev/null
+++ b/services/src/main/resources/META-INF/mime.types
@@ -0,0 +1 @@
+text/css css CSS
\ No newline at end of file
diff --git a/social/core/pom.xml b/social/core/pom.xml
index 99ade1e..41a9ec3 100755
--- a/social/core/pom.xml
+++ b/social/core/pom.xml
@@ -14,6 +14,11 @@
 
     <dependencies>
         <dependency>
+            <groupId>org.keycloak</groupId>
+            <artifactId>keycloak-core</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+        <dependency>
             <groupId>org.json</groupId>
             <artifactId>json</artifactId>
         </dependency>
diff --git a/social/core/src/main/java/org/keycloak/social/SocialLoader.java b/social/core/src/main/java/org/keycloak/social/SocialLoader.java
index 5dfb44a..6d24535 100644
--- a/social/core/src/main/java/org/keycloak/social/SocialLoader.java
+++ b/social/core/src/main/java/org/keycloak/social/SocialLoader.java
@@ -1,5 +1,7 @@
 package org.keycloak.social;
 
+import org.keycloak.util.ProviderLoader;
+
 import java.util.ServiceLoader;
 
 /**
@@ -25,7 +27,7 @@ public class SocialLoader {
     }
 
     public static Iterable<SocialProvider> load() {
-        return ServiceLoader.load(SocialProvider.class);
+        return ProviderLoader.load(SocialProvider.class);
     }
 
 }
diff --git a/testsuite/integration/pom.xml b/testsuite/integration/pom.xml
index e3fffc5..e228bbe 100755
--- a/testsuite/integration/pom.xml
+++ b/testsuite/integration/pom.xml
@@ -90,7 +90,32 @@
         </dependency>
         <dependency>
             <groupId>org.keycloak</groupId>
-            <artifactId>keycloak-forms</artifactId>
+            <artifactId>keycloak-forms-common-freemarker</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.keycloak</groupId>
+            <artifactId>keycloak-forms-common-themes</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.keycloak</groupId>
+            <artifactId>keycloak-account-api</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.keycloak</groupId>
+            <artifactId>keycloak-account-freemarker</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.keycloak</groupId>
+            <artifactId>keycloak-login-api</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.keycloak</groupId>
+            <artifactId>keycloak-login-freemarker</artifactId>
             <version>${project.version}</version>
         </dependency>
 
diff --git a/testsuite/integration/src/main/java/org/keycloak/testutils/KeycloakServer.java b/testsuite/integration/src/main/java/org/keycloak/testutils/KeycloakServer.java
index c3edfe9..60433a2 100755
--- a/testsuite/integration/src/main/java/org/keycloak/testutils/KeycloakServer.java
+++ b/testsuite/integration/src/main/java/org/keycloak/testutils/KeycloakServer.java
@@ -37,6 +37,7 @@ import io.undertow.servlet.api.ServletInfo;
 import org.jboss.resteasy.logging.Logger;
 import org.jboss.resteasy.plugins.server.undertow.UndertowJaxrsServer;
 import org.jboss.resteasy.spi.ResteasyDeployment;
+import org.keycloak.theme.DefaultLoginThemeProvider;
 import org.keycloak.services.tmp.TmpAdminRedirectServlet;
 import org.keycloak.util.JsonSerialization;
 import org.keycloak.models.Constants;
@@ -136,6 +137,11 @@ public class KeycloakServer {
                 throw new RuntimeException("Invalid resources directory");
             }
 
+            if (!System.getProperties().containsKey("keycloak.theme.dir")) {
+                System.setProperty(DefaultLoginThemeProvider.class.getName() + ".disabled", "");
+                System.setProperty("keycloak.theme.dir", file(dir.getAbsolutePath(), "forms", "common-themes", "src", "main", "resources", "theme").getAbsolutePath());
+            }
+
             config.setResourcesHome(dir.getAbsolutePath());
         }
 
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/ResetPasswordTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/ResetPasswordTest.java
index 8df0a1e..c49e8a9 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/ResetPasswordTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/ResetPasswordTest.java
@@ -87,7 +87,7 @@ public class ResetPasswordTest {
 
         resetPasswordPage.assertCurrent();
 
-        Assert.assertEquals("Success!", resetPasswordPage.getMessage());
+        Assert.assertEquals("You should receive an email shortly with further instructions.", resetPasswordPage.getMessage());
 
         Assert.assertEquals(1, greenMail.getReceivedMessages().length);
 
@@ -146,7 +146,7 @@ public class ResetPasswordTest {
 
         resetPasswordPage.assertCurrent();
 
-        Assert.assertEquals("Success!", resetPasswordPage.getMessage());
+        Assert.assertEquals("You should receive an email shortly with further instructions.", resetPasswordPage.getMessage());
 
         Assert.assertEquals(1, greenMail.getReceivedMessages().length);
 
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/AccountPasswordPage.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/AccountPasswordPage.java
index 0a94649..080112f 100644
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/AccountPasswordPage.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/AccountPasswordPage.java
@@ -53,7 +53,7 @@ public class AccountPasswordPage extends AbstractAccountPage {
     }
 
     public boolean isCurrent() {
-        return driver.getPageSource().contains("Change Password");
+        return driver.getTitle().contains("Account Management") && driver.getCurrentUrl().endsWith("/account/password");
     }
 
     public void open() {
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/AccountTotpPage.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/AccountTotpPage.java
index 171e6ef..292a99f 100644
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/AccountTotpPage.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/AccountTotpPage.java
@@ -54,7 +54,7 @@ public class AccountTotpPage extends AbstractAccountPage {
     }
 
     public boolean isCurrent() {
-        return driver.getTitle().contains("Edit Account - Google Authenticator");
+        return driver.getTitle().contains("Account Management") && driver.getCurrentUrl().endsWith("/account/totp");
     }
 
     public void open() {
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/AccountUpdateProfilePage.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/AccountUpdateProfilePage.java
index d19ca29..0c88078 100644
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/AccountUpdateProfilePage.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/AccountUpdateProfilePage.java
@@ -75,7 +75,7 @@ public class AccountUpdateProfilePage extends AbstractAccountPage {
     }
 
     public boolean isCurrent() {
-        return driver.getPageSource().contains("Edit Account");
+        return driver.getTitle().contains("Account Management") && driver.getPageSource().contains("Edit Account");
     }
 
     public void open() {
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/LoginPage.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/LoginPage.java
index 3a1f72e..532a7d8 100644
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/LoginPage.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/LoginPage.java
@@ -59,7 +59,7 @@ public class LoginPage extends AbstractPage {
     @FindBy(linkText = "Username")
     private WebElement recoverUsernameLink;
 
-    @FindBy(id = "loginError")
+    @FindBy(className = "error")
     private WebElement loginErrorMessage;
 
     public void login(String username, String password) {
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/LoginTotpPage.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/LoginTotpPage.java
index 2a7ed57..516f2d6 100644
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/LoginTotpPage.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/LoginTotpPage.java
@@ -36,7 +36,7 @@ public class LoginTotpPage extends AbstractPage {
     @FindBy(css = "input[type=\"submit\"]")
     private WebElement submitButton;
 
-    @FindBy(id = "loginError")
+    @FindBy(className = "error")
     private WebElement loginErrorMessage;
 
     public void login(String totp) {
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/LoginUpdateProfilePage.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/LoginUpdateProfilePage.java
index 9fe02ef..17f058d 100644
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/LoginUpdateProfilePage.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/LoginUpdateProfilePage.java
@@ -41,7 +41,7 @@ public class LoginUpdateProfilePage extends AbstractPage {
     @FindBy(css = "input[type=\"submit\"]")
     private WebElement submitButton;
 
-    @FindBy(id = "loginError")
+    @FindBy(className = "error")
     private WebElement loginErrorMessage;
 
     public void update(String firstName, String lastName, String email) {
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/RegisterPage.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/RegisterPage.java
index c67d692..c84f1ac 100644
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/RegisterPage.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/RegisterPage.java
@@ -50,7 +50,7 @@ public class RegisterPage extends AbstractPage {
     @FindBy(css = "input[type=\"submit\"]")
     private WebElement submitButton;
 
-    @FindBy(id = "loginError")
+    @FindBy(className = "error")
     private WebElement loginErrorMessage;
 
     public void register(String firstName, String lastName, String email, String username, String password, String passwordConfirm) {