keycloak-aplcache
Changes
forms/account-api/pom.xml 49(+49 -0)
forms/account-freemarker/pom.xml 70(+70 -0)
forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/FreeMarkerAccount.java 158(+158 -0)
forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/FreeMarkerAccountProvider.java 18(+18 -0)
forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/model/AccountBean.java 32(+32 -0)
forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/model/MessageBean.java 28(+11 -17)
forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/model/TotpBean.java 25(+9 -16)
forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/model/UrlBean.java 63(+63 -0)
forms/account-freemarker/src/main/resources/META-INF/services/org.keycloak.account.AccountProvider 1(+1 -0)
forms/common-freemarker/pom.xml 49(+49 -0)
forms/common-themes/pom.xml 54(+54 -0)
forms/common-themes/src/main/resources/META-INF/services/org.keycloak.freemarker.ThemeProvider 2(+2 -0)
forms/common-themes/src/main/resources/theme/account/rcue/resources/css/img/customer-login-screen-bg.jpg 0(+0 -0)
forms/common-themes/src/main/resources/theme/account/rcue/resources/css/img/feedback-error-arrow-down.png 0(+0 -0)
forms/common-themes/src/main/resources/theme/account/rcue/resources/css/img/feedback-error-sign.png 0(+0 -0)
forms/common-themes/src/main/resources/theme/account/rcue/resources/css/img/feedback-success-arrow-down.png 0(+0 -0)
forms/common-themes/src/main/resources/theme/account/rcue/resources/css/img/feedback-success-sign.png 0(+0 -0)
forms/common-themes/src/main/resources/theme/account/rcue/resources/css/img/feedback-warning-sign.png 0(+0 -0)
forms/common-themes/src/main/resources/theme/account/rcue/resources/css/img/login-register-separator.png 0(+0 -0)
forms/common-themes/src/main/resources/theme/account/rcue/resources/css/img/login-register-social.png 0(+0 -0)
forms/common-themes/src/main/resources/theme/account/rcue/resources/css/img/login-register-social-separator.png 0(+0 -0)
forms/common-themes/src/main/resources/theme/account/rcue/resources/css/img/register-login-bg.png 0(+0 -0)
forms/common-themes/src/main/resources/theme/account/rcue/resources/css/img/sprites-white.png 0(+0 -0)
forms/common-themes/src/main/resources/theme/account/rcue/resources/img/login-screen-background.jpg 0(+0 -0)
forms/common-themes/src/main/resources/theme/account/rcue/resources/lib/bootstrap/css/bootstrap.css 0(+0 -0)
forms/common-themes/src/main/resources/theme/account/rcue/resources/lib/bootstrap/css/bootstrap.min.css 0(+0 -0)
forms/common-themes/src/main/resources/theme/account/rcue/resources/lib/bootstrap/css/bootstrap-theme.css 0(+0 -0)
forms/common-themes/src/main/resources/theme/account/rcue/resources/lib/bootstrap/css/bootstrap-theme.min.css 0(+0 -0)
forms/common-themes/src/main/resources/theme/account/rcue/resources/lib/bootstrap/fonts/glyphicons-halflings-regular.eot 0(+0 -0)
forms/common-themes/src/main/resources/theme/account/rcue/resources/lib/bootstrap/fonts/glyphicons-halflings-regular.svg 0(+0 -0)
forms/common-themes/src/main/resources/theme/account/rcue/resources/lib/bootstrap/fonts/glyphicons-halflings-regular.ttf 0(+0 -0)
forms/common-themes/src/main/resources/theme/account/rcue/resources/lib/bootstrap/fonts/glyphicons-halflings-regular.woff 0(+0 -0)
forms/common-themes/src/main/resources/theme/account/rcue/resources/lib/bootstrap/js/bootstrap.js 0(+0 -0)
forms/common-themes/src/main/resources/theme/account/rcue/resources/lib/bootstrap/js/bootstrap.min.js 0(+0 -0)
forms/common-themes/src/main/resources/theme/login/rcue/resources/css/login-register.css 436(+436 -0)
forms/common-themes/src/main/resources/theme/login/rcue/resources/img/customer-login-screen-bg.jpg 0(+0 -0)
forms/common-themes/src/main/resources/theme/login/rcue/resources/img/feedback-error-arrow-down.png 0(+0 -0)
forms/common-themes/src/main/resources/theme/login/rcue/resources/img/feedback-error-sign.png 0(+0 -0)
forms/common-themes/src/main/resources/theme/login/rcue/resources/img/feedback-success-arrow-down.png 0(+0 -0)
forms/common-themes/src/main/resources/theme/login/rcue/resources/img/feedback-success-sign.png 0(+0 -0)
forms/common-themes/src/main/resources/theme/login/rcue/resources/img/feedback-warning-arrow-down.png 0(+0 -0)
forms/common-themes/src/main/resources/theme/login/rcue/resources/img/feedback-warning-sign.png 0(+0 -0)
forms/common-themes/src/main/resources/theme/login/rcue/resources/img/login-register-separator.png 0(+0 -0)
forms/common-themes/src/main/resources/theme/login/rcue/resources/img/login-register-social.png 0(+0 -0)
forms/common-themes/src/main/resources/theme/login/rcue/resources/img/login-register-social-separator.png 0(+0 -0)
forms/common-themes/src/main/resources/theme/login/rcue/resources/img/login-screen-background.jpg 0(+0 -0)
forms/common-themes/src/main/resources/theme/login/rcue/resources/img/register-login-bg.png 0(+0 -0)
forms/common-themes/src/main/resources/theme/login/rcue/resources/lib/zocial/zocial-regular-webfont.eot 0(+0 -0)
forms/common-themes/src/main/resources/theme/login/rcue/resources/lib/zocial/zocial-regular-webfont.svg 0(+0 -0)
forms/common-themes/src/main/resources/theme/login/rcue/resources/lib/zocial/zocial-regular-webfont.ttf 0(+0 -0)
forms/common-themes/src/main/resources/theme/login/rcue/resources/lib/zocial/zocial-regular-webfont.woff 0(+0 -0)
forms/login-api/pom.xml 49(+49 -0)
forms/login-freemarker/pom.xml 75(+75 -0)
forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/FreeMarkerLoginForms.java 273(+273 -0)
forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/FreeMarkerLoginFormsProvider.java 20(+20 -0)
forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/model/MessageBean.java 61(+19 -42)
forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/model/OAuthGrantBean.java 45(+12 -33)
forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/model/ProfileBean.java 31(+6 -25)
forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/model/SocialBean.java 78(+39 -39)
forms/login-freemarker/src/main/resources/META-INF/services/org.keycloak.login.FormsProvider 1(+1 -0)
forms/pom.xml 53(+9 -44)
forms/src/main/resources/META-INF/resources/forms/theme/default/template-login-action.ftl 84(+0 -84)
server/pom.xml 27(+26 -1)
services/pom.xml 16(+16 -0)
services/src/main/java/org/keycloak/services/resources/admin/ServerInfoAdminResource.java 68(+68 -0)
social/core/pom.xml 5(+5 -0)
testsuite/integration/pom.xml 27(+26 -1)
testsuite/integration/src/test/java/org/keycloak/testsuite/pages/AccountUpdateProfilePage.java 2(+1 -1)
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
forms/account-api/pom.xml 49(+49 -0)
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);
+
+}
forms/account-freemarker/pom.xml 70(+70 -0)
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
forms/common-freemarker/pom.xml 49(+49 -0)
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);
+
+}
forms/common-themes/pom.xml 54(+54 -0)
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
forms/login-api/pom.xml 49(+49 -0)
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);
+
+}
forms/login-freemarker/pom.xml 75(+75 -0)
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
social/core/pom.xml 5(+5 -0)
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);
}
}
testsuite/integration/pom.xml 27(+26 -1)
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) {