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 fcd5d13ac1..3371766ee7 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 @@ -256,7 +256,7 @@ module.controller('RealmDetailCtrl', function($scope, Current, Realm, realm, $ht }; }); -module.controller('RealmRequiredCredentialsCtrl', function($scope, Realm, realm, $http, $location, Dialog, Notifications) { +module.controller('RealmRequiredCredentialsCtrl', function($scope, Realm, realm, $http, $location, Dialog, Notifications, PasswordPolicy) { console.log('RealmRequiredCredentialsCtrl'); $scope.realm = { @@ -264,128 +264,25 @@ module.controller('RealmRequiredCredentialsCtrl', function($scope, Realm, realm, requiredCredentials : realm.requiredCredentials, requiredApplicationCredentials : realm.requiredApplicationCredentials, requiredOAuthClientCredentials : realm.requiredOAuthClientCredentials, - registrationAllowed : realm.registrationAllowed + registrationAllowed : realm.registrationAllowed, + passwordPolicy: realm.passwordPolicy }; - if (realm.hasOwnProperty('passwordPolicy')){ - $scope.realm['passwordPolicy'] = realm.passwordPolicy; - } else { - $scope.realm['passwordPolicy'] = ""; - realm['passwordPolicy'] = ""; - } - var oldCopy = angular.copy($scope.realm); - /* Map used in the table when hovering over (i) icon */ - $scope.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." + $scope.allPolicies = PasswordPolicy.allPolicies; + $scope.policyMessages = PasswordPolicy.policyMessages; + + $scope.policy = PasswordPolicy.parse(realm.passwordPolicy); + + $scope.addPolicy = function(policy){ + $scope.policy.push(policy); } - // $scope.policy is an object representing passwordPolicy string - $scope.policy = {}; - // All available policies - $scope.allPolicies = ['length', 'digits', 'lowerCase', 'upperCase', 'specialChars']; - // List of configured policies - $scope.configuredPolicies = []; - // List of not configured policies - $scope.availablePolicies = $scope.allPolicies.slice(0); - - $scope.addPolicy = function(){ - $scope.policy[$scope.newPolicyId] = ""; - updateConfigured(); + $scope.removePolicy = function(index){ + $scope.policy.splice(index, 1); } - $scope.removePolicy = function(pId){ - delete $scope.policy[pId]; - updateConfigured(); - } - - // Updating lists of configured and non-configured policies based on the $scope.policy object - var updateConfigured = function(){ - - for (var i = 0; i < $scope.allPolicies.length; i++){ - - var policy = $scope.allPolicies[i]; - - if($scope.policy.hasOwnProperty(policy)){ - - var ind = $scope.configuredPolicies.indexOf(policy); - - if(ind < 0){ - $scope.configuredPolicies.push(policy); - } - - ind = $scope.availablePolicies.indexOf(policy); - if(ind > -1){ - $scope.availablePolicies.splice(ind, 1); - } - } else { - - var ind = $scope.configuredPolicies.indexOf(policy); - - if(ind > -1){ - $scope.configuredPolicies.splice(ind, 1); - } - - ind = $scope.availablePolicies.indexOf(policy); - if(ind < 0){ - $scope.availablePolicies.push(policy); - } - } - } - - if ($scope.availablePolicies.length > 0){ - $scope.newPolicyId = $scope.availablePolicies[0]; - } - } - - // Creating object from policy string - var evaluatePolicy = function(policyString){ - - var policyObject = {}; - - if (!policyString || policyString.length == 0){ - return policyObject; - } - - 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); - policyObject[policyEntry[1]] = policyEntry[2]; - } - - return policyObject; - } - - // Creating policy string based on policy object - var generatePolicy = function(policyObject){ - var policyString = ""; - - for (var key in policyObject){ - policyString += key; - var value = policyObject[key]; - if ( value != ""){ - policyString += "("+value+")"; - } - policyString += " and "; - } - - policyString = policyString.substring(0, policyString.length - 5); - - return policyString; - } - - $scope.policy = evaluatePolicy(realm.passwordPolicy); - updateConfigured(); - $scope.userCredentialOptions = { 'multiple' : true, 'simple_tags' : true, @@ -400,9 +297,9 @@ module.controller('RealmRequiredCredentialsCtrl', function($scope, Realm, realm, } }, true); - $scope.$watch('policy', function() { - $scope.realm.passwordPolicy = generatePolicy($scope.policy); - if ($scope.realm.passwordPolicy != realm.passwordPolicy){ + $scope.$watch('policy', function(oldVal, newVal) { + if (oldVal != newVal) { + $scope.realm.passwordPolicy = PasswordPolicy.toString($scope.policy); $scope.changed = true; } }, true); @@ -419,11 +316,8 @@ module.controller('RealmRequiredCredentialsCtrl', function($scope, Realm, realm, $scope.reset = function() { $scope.realm = angular.copy(oldCopy); - - $scope.configuredPolicies = []; - $scope.availablePolicies = $scope.allPolicies.slice(0); - $scope.policy = evaluatePolicy(oldCopy.passwordPolicy); - updateConfigured(); + $scope.policy = PasswordPolicy.parse(oldCopy.passwordPolicy); + console.debug(realm.passwordPolicy); $scope.changed = false; }; 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 d1af959a28..193a8d33d0 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 @@ -386,3 +386,68 @@ module.factory('TimeUnit', function() { 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 0d81e0165a..d9e505c387 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 @@ -52,18 +52,16 @@ - + @@ -74,19 +72,19 @@ - + 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 7d41a29f6a..327c02dd5d 100755 --- a/services/src/main/java/org/keycloak/services/resources/RequiredActionsService.java +++ b/services/src/main/java/org/keycloak/services/resources/RequiredActionsService.java @@ -168,6 +168,11 @@ public class RequiredActionsService { return forms.setError(Messages.NOTMATCH_PASSWORD).forwardToAction(RequiredAction.UPDATE_PASSWORD); } + String error = realm.getPasswordPolicy().validate(passwordNew); + if (error != null) { + return forms.setError(error).forwardToAction(RequiredAction.UPDATE_PASSWORD); + } + UserCredentialModel credentials = new UserCredentialModel(); credentials.setType(CredentialRepresentation.PASSWORD); credentials.setValue(passwordNew); 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 4d98653b18..8df0a1e3e7 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 @@ -25,6 +25,9 @@ import org.junit.Assert; import org.junit.ClassRule; import org.junit.Rule; import org.junit.Test; +import org.keycloak.models.PasswordPolicy; +import org.keycloak.models.RealmModel; +import org.keycloak.services.managers.RealmManager; import org.keycloak.testsuite.OAuthClient; import org.keycloak.testsuite.pages.AppPage; import org.keycloak.testsuite.pages.AppPage.RequestType; @@ -125,4 +128,52 @@ public class ResetPasswordTest { Assert.assertEquals("Invalid email.", resetPasswordPage.getMessage()); } + @Test + public void resetPasswordWithPasswordPolicy() throws IOException, MessagingException { + keycloakRule.configure(new KeycloakRule.KeycloakSetup() { + @Override + public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) { + appRealm.setPasswordPolicy(new PasswordPolicy("length")); + } + }); + + loginPage.open(); + loginPage.resetPassword(); + + resetPasswordPage.assertCurrent(); + + resetPasswordPage.changePassword("test-user@localhost"); + + resetPasswordPage.assertCurrent(); + + Assert.assertEquals("Success!", resetPasswordPage.getMessage()); + + Assert.assertEquals(1, greenMail.getReceivedMessages().length); + + MimeMessage message = greenMail.getReceivedMessages()[0]; + + String body = (String) message.getContent(); + String changePasswordUrl = body.split("\n")[3]; + + driver.navigate().to(changePasswordUrl.trim()); + + updatePasswordPage.assertCurrent(); + + updatePasswordPage.changePassword("invalid", "invalid"); + + Assert.assertNotEquals("Success!", resetPasswordPage.getMessage()); + Assert.assertEquals("Invalid password: minimum length 8", resetPasswordPage.getMessage()); + + updatePasswordPage.changePassword("new-password", "new-password"); + + Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType()); + + oauth.openLogout(); + + loginPage.open(); + + loginPage.login("test-user@localhost", "new-password"); + + Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType()); + } }
- +
-
- -
- +
- +
-
+