From 2421d8390e3a718aff42a379721f5181a7eb84cb Mon Sep 17 00:00:00 2001 From: eric Date: Thu, 23 Nov 2023 14:53:33 +0100 Subject: [PATCH] fix: improve default password policy to follow latest OWASP recommendations fixes AM-526 (cherry picked from commit 4dcaf234bbe59de8dbf5e87887451d3af0746ea7) # Conflicts: # gravitee-am-test/specs/gateway/login-flow.jest.spec.ts # gravitee-am-test/specs/gateway/remember-me.jest.spec.ts --- .../src/main/resources/config/gravitee.yml | 22 +++---- .../src/main/resources/config/gravitee.yml | 22 +++---- .../gravitee/am/model/PasswordSettings.java | 2 +- .../impl/DefaultPasswordValidatorImpl.java | 2 +- .../DefaultPasswordValidatorTest.java | 64 +++++++++++++++++++ .../am-confirm-pre-registration.spec.ts | 2 +- ...m-selfaccount-change-password.jest.spec.ts | 4 +- .../specs/gateway/enduser-logout.jest.spec.ts | 2 +- .../specs/gateway/flow-execution.jest.spec.ts | 2 +- .../specs/gateway/login-flow.jest.spec.ts | 8 +-- .../gateway/user-registration.jest.spec.ts | 12 ++-- .../graviteeio-am-openid-ciba-collection.json | 4 +- 12 files changed, 105 insertions(+), 41 deletions(-) create mode 100644 gravitee-am-service/src/test/java/io/gravitee/am/service/validators/DefaultPasswordValidatorTest.java diff --git a/gravitee-am-gateway/gravitee-am-gateway-standalone/gravitee-am-gateway-standalone-distribution/src/main/resources/config/gravitee.yml b/gravitee-am-gateway/gravitee-am-gateway-standalone/gravitee-am-gateway-standalone-distribution/src/main/resources/config/gravitee.yml index 3e26831f951..738b2834d5f 100644 --- a/gravitee-am-gateway/gravitee-am-gateway-standalone/gravitee-am-gateway-standalone-distribution/src/main/resources/config/gravitee.yml +++ b/gravitee-am-gateway/gravitee-am-gateway-standalone/gravitee-am-gateway-standalone-distribution/src/main/resources/config/gravitee.yml @@ -258,17 +258,17 @@ user: password: policy: # Regex pattern for password validation (default to OWASP recommendations). - # 8 to 32 characters, no more than 2 consecutive equal characters, min 1 special characters (@ & # ...), min 1 upper case character. - pattern: ^(?:(?=.*\d)(?=.*[A-Z])(?=.*[a-z])|(?=.*\d)(?=.*[^A-Za-z0-9])(?=.*[a-z])|(?=.*[^A-Za-z0-9])(?=.*[A-Z])(?=.*[a-z])|(?=.*\d)(?=.*[A-Z])(?=.*[^A-Za-z0-9]))(?!.*(.)\1{2,})[A-Za-z0-9!~<>,;:_\-=?*+#."'&§`£€%°()\\\|\[\]\-\$\^\@\/]{8,32}$ - # Example : ^(?=.*[0-9])(?=.*[a-z])(?=.*[A-Z])(?=.*[@#$%^&+=])(?=\S+$).{8,}$ - # ^ # start-of-string - #(?=.*[0-9]) # a digit must occur at least once - #(?=.*[a-z]) # a lower case letter must occur at least once - #(?=.*[A-Z]) # an upper case letter must occur at least once - #(?=.*[@#$%^&+=]) # a special character must occur at least once - #(?=\S+$) # no whitespace allowed in the entire string - #.{8,} # anything, at least eight places though - #$ # end-of-string + # Password must be at least 12 characters long, contain at least one digit, one upper case letter, one lower case letter, one special character, and no more than 2 + pattern: ^(?=.*[0-9])(?=.*[A-Z])(?=.*[a-z])(?=.*[!~<>.,;:_=?/*+\-#\"'&§`£€%°()|\[\]$^@])(?!.*(.)\1{2,}).{12,128}$ + # Example : ^(?=.*[0-9])(?=.*[A-Z])(?=.*[a-z])(?=.*[!~<>.,;:_=?/*+\-#\"'&§`£€%°()|\[\]$^@])(?!.*(.)\1{2,}).{12,128}$ + # ^ # start-of-string + #(?=.*[0-9]) # a digit must occur at least once + #(?=.*[A-Z]) # an upper case letter must occur at least once + #(?=.*[a-z]) # a lower case letter must occur at least once + #(?=.*[!~<>.,;:_=?/*+\-#\"'&§`£€%°()|\[\]$^@]) # a special character must occur at least once + #((?!.*(.)\1{2,}) # no more than 2 consecutive equal characters + #.{12,128} # anything, between 12 and 128 characters + #$ # end-of-string ## Password dictionary to exclude most commons passwords ## You need to enable the feature in the AM management console diff --git a/gravitee-am-management-api/gravitee-am-management-api-standalone/gravitee-am-management-api-standalone-distribution/src/main/resources/config/gravitee.yml b/gravitee-am-management-api/gravitee-am-management-api-standalone/gravitee-am-management-api-standalone-distribution/src/main/resources/config/gravitee.yml index ca3f36583e6..c07df4ce431 100644 --- a/gravitee-am-management-api/gravitee-am-management-api-standalone/gravitee-am-management-api-standalone-distribution/src/main/resources/config/gravitee.yml +++ b/gravitee-am-management-api/gravitee-am-management-api-standalone/gravitee-am-management-api-standalone-distribution/src/main/resources/config/gravitee.yml @@ -311,17 +311,17 @@ user: password: policy: # Regex pattern for password validation (default to OWASP recommendations). - # 8 to 32 characters, no more than 2 consecutive equal characters, min 1 special characters (@ & # ...), min 1 upper case character. - pattern: ^(?:(?=.*\d)(?=.*[A-Z])(?=.*[a-z])|(?=.*\d)(?=.*[^A-Za-z0-9])(?=.*[a-z])|(?=.*[^A-Za-z0-9])(?=.*[A-Z])(?=.*[a-z])|(?=.*\d)(?=.*[A-Z])(?=.*[^A-Za-z0-9]))(?!.*(.)\1{2,})[A-Za-z0-9!~<>,;:_\-=?*+#."'&§`£€%°()\\\|\[\]\-\$\^\@\/]{8,32}$ - # Example : ^(?=.*[0-9])(?=.*[a-z])(?=.*[A-Z])(?=.*[@#$%^&+=])(?=\S+$).{8,}$ - # ^ # start-of-string - #(?=.*[0-9]) # a digit must occur at least once - #(?=.*[a-z]) # a lower case letter must occur at least once - #(?=.*[A-Z]) # an upper case letter must occur at least once - #(?=.*[@#$%^&+=]) # a special character must occur at least once - #(?=\S+$) # no whitespace allowed in the entire string - #.{8,} # anything, at least eight places though - #$ # end-of-string + # Password must be at least 12 characters long, contain at least one digit, one upper case letter, one lower case letter, one special character, and no more than 2 + pattern: ^(?=.*[0-9])(?=.*[A-Z])(?=.*[a-z])(?=.*[!~<>.,;:_=?/*+\-#\"'&§`£€%°()|\[\]$^@])(?!.*(.)\1{2,}).{12,128}$ + # Example : ^(?=.*[0-9])(?=.*[A-Z])(?=.*[a-z])(?=.*[!~<>.,;:_=?/*+\-#\"'&§`£€%°()|\[\]$^@])(?!.*(.)\1{2,}).{12,128}$ + # ^ # start-of-string + #(?=.*[0-9]) # a digit must occur at least once + #(?=.*[A-Z]) # an upper case letter must occur at least once + #(?=.*[a-z]) # a lower case letter must occur at least once + #(?=.*[!~<>.,;:_=?/*+\-#\"'&§`£€%°()|\[\]$^@]) # a special character must occur at least once + #((?!.*(.)\1{2,}) # no more than 2 consecutive equal characters + #.{12,128} # anything, between 12 and 128 characters + #$ # end-of-string ## Password dictionary to exclude most commons passwords ## You need to enable the feature in the AM management console diff --git a/gravitee-am-model/src/main/java/io/gravitee/am/model/PasswordSettings.java b/gravitee-am-model/src/main/java/io/gravitee/am/model/PasswordSettings.java index 31e67dc7394..53df3427625 100644 --- a/gravitee-am-model/src/main/java/io/gravitee/am/model/PasswordSettings.java +++ b/gravitee-am-model/src/main/java/io/gravitee/am/model/PasswordSettings.java @@ -27,7 +27,7 @@ public class PasswordSettings { /** * See https://cheatsheetseries.owasp.org/cheatsheets/Authentication_Cheat_Sheet.html */ - public static final int PASSWORD_MAX_LENGTH = 64; + public static final int PASSWORD_MAX_LENGTH = 128; public static final int PASSWORD_MIN_LENGTH = 8; /** diff --git a/gravitee-am-service/src/main/java/io/gravitee/am/service/validators/password/impl/DefaultPasswordValidatorImpl.java b/gravitee-am-service/src/main/java/io/gravitee/am/service/validators/password/impl/DefaultPasswordValidatorImpl.java index 96329ce6b17..7c67159266e 100644 --- a/gravitee-am-service/src/main/java/io/gravitee/am/service/validators/password/impl/DefaultPasswordValidatorImpl.java +++ b/gravitee-am-service/src/main/java/io/gravitee/am/service/validators/password/impl/DefaultPasswordValidatorImpl.java @@ -32,7 +32,7 @@ @Component("defaultPasswordValidator") public class DefaultPasswordValidatorImpl implements PasswordValidator { - private static final String DEFAULT_PASSWORD_PATTERN_VALUE = "^(?:(?=.*\\d)(?=.*[A-Z])(?=.*[a-z])|(?=.*\\d)(?=.*[^A-Za-z0-9])(?=.*[a-z])|(?=.*[^A-Za-z0-9])(?=.*[A-Z])(?=.*[a-z])|(?=.*\\d)(?=.*[A-Z])(?=.*[^A-Za-z0-9]))(?!.*(.)\\1{2,})[A-Za-z0-9!~<>,;:_\\-=?*+#.\"'&§`£€%°()\\\\\\|\\[\\]\\-\\$\\^\\@\\/]{8,32}$"; + private static final String DEFAULT_PASSWORD_PATTERN_VALUE = "^(?=.*[0-9])(?=.*[A-Z])(?=.*[a-z])(?=.*[!~<>.,;:_=?/*+\\-#\\\"'&§`£€%°()|\\[\\]$^@])(?!.*(.)\\1{2,}).{12,128}$"; private static final String MESSAGE = "Field [password] is invalid"; private static final String ERROR_KEY = "invalid_password_value"; diff --git a/gravitee-am-service/src/test/java/io/gravitee/am/service/validators/DefaultPasswordValidatorTest.java b/gravitee-am-service/src/test/java/io/gravitee/am/service/validators/DefaultPasswordValidatorTest.java new file mode 100644 index 00000000000..e2239f863f7 --- /dev/null +++ b/gravitee-am-service/src/test/java/io/gravitee/am/service/validators/DefaultPasswordValidatorTest.java @@ -0,0 +1,64 @@ +/** + * Copyright (C) 2015 The Gravitee team (http://gravitee.io) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.gravitee.am.service.validators; + +import io.gravitee.am.service.validators.password.PasswordValidator; +import io.gravitee.am.service.validators.password.impl.DefaultPasswordValidatorImpl; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * @author Eric LELEU (eric.leleu at graviteesource.com) + * @author GraviteeSource Team + */ +public class DefaultPasswordValidatorTest { + @ParameterizedTest + @MethodSource("providerValidatePassword") + void validatePassword(String password, boolean expected) { + PasswordValidator validator = new DefaultPasswordValidatorImpl( + "^(?=.*[0-9])(?=.*[A-Z])(?=.*[a-z])(?=.*[!~<>.,;:_=?/*+\\-#\\\"'&§`£€%°()|\\[\\]$^@])(?!.*(.)\\1{2,}).{12,128}$" + ); + assertEquals(expected, validator.validate(password)); + } + + private static Stream providerValidatePassword() { + return Stream.of( + Arguments.of("a1!atjubclzf", false), + Arguments.of("A1!ABVREFAGD", false), + Arguments.of("Aa!AHYaeffSF", false), + Arguments.of("AaBbCcDd1324", false), + Arguments.of("Aa1!", false), + Arguments.of("Password12!", false), + Arguments.of("Passsword123!", false), + Arguments.of("Password123!!!", false), + Arguments.of( + "MA48*xP:344MA48*xP:344MA48*xP:344MA48*xP:344MA48*xP:344MA48*xP:344MA48*xP:344MA48*xP:344MA48*xP:344MA48*xP:344MA48*xP:344MA48*xPz:", + false + ), + Arguments.of("Password1231!", true), + Arguments.of("Password123!2£1", true), + Arguments.of("MA48*xP:344d", true), + Arguments.of("Ab0!~<>,;:_-=?*+#.\"'&§`€%°()|[]$^@", true), + Arguments.of("SomeP@ssw0rd", true) + ); + } +} \ No newline at end of file diff --git a/gravitee-am-test/specs/gateway/am-confirm-pre-registration.spec.ts b/gravitee-am-test/specs/gateway/am-confirm-pre-registration.spec.ts index cc02e9f067e..91508731c0a 100644 --- a/gravitee-am-test/specs/gateway/am-confirm-pre-registration.spec.ts +++ b/gravitee-am-test/specs/gateway/am-confirm-pre-registration.spec.ts @@ -200,7 +200,7 @@ describe('AM - User Pre-Registration - Reset Password to confirm', () => { expect(confirmationLink).toBeDefined(); await clearEmails(); - await resetPassword(confirmationLink, 'Test123!'); + await resetPassword(confirmationLink, 'SomeP@ssw0rd'); }); it('must be enabled', async () => { diff --git a/gravitee-am-test/specs/gateway/am-selfaccount-change-password.jest.spec.ts b/gravitee-am-test/specs/gateway/am-selfaccount-change-password.jest.spec.ts index b8d9b060b1f..679261f1dce 100644 --- a/gravitee-am-test/specs/gateway/am-selfaccount-change-password.jest.spec.ts +++ b/gravitee-am-test/specs/gateway/am-selfaccount-change-password.jest.spec.ts @@ -33,7 +33,7 @@ let openIdConfiguration; let application; let user = { username: 'SelfAccountUser', - password: 'Test123!', + password: 'SomeP@ssw0rd', firstName: 'SelfAccount', lastName: 'User', email: 'selfaccountuser@acme.fr', @@ -135,7 +135,7 @@ describe('SelfAccount - Change Password', () => { describe('With default settings', () => { describe('End User', () => { it('must be able to change his password', async () => { - user.password = 'Test1234!!'; + user.password = 'SomeP@ssw0rd!'; await performPost( `${process.env.AM_GATEWAY_URL}/${domain.hrid}/account/api/changePassword`, diff --git a/gravitee-am-test/specs/gateway/enduser-logout.jest.spec.ts b/gravitee-am-test/specs/gateway/enduser-logout.jest.spec.ts index e2673fd3d24..d4a3bd7bfa5 100644 --- a/gravitee-am-test/specs/gateway/enduser-logout.jest.spec.ts +++ b/gravitee-am-test/specs/gateway/enduser-logout.jest.spec.ts @@ -37,7 +37,7 @@ let openIdConfiguration; let application; let user = { username: 'LogoutUser', - password: 'Test123!', + password: 'SomeP@ssw0rd', firstName: 'Logout', lastName: 'User', email: 'logoutuser@acme.fr', diff --git a/gravitee-am-test/specs/gateway/flow-execution.jest.spec.ts b/gravitee-am-test/specs/gateway/flow-execution.jest.spec.ts index cbd9c7c509e..fece2d0d8ce 100644 --- a/gravitee-am-test/specs/gateway/flow-execution.jest.spec.ts +++ b/gravitee-am-test/specs/gateway/flow-execution.jest.spec.ts @@ -48,7 +48,7 @@ let openIdConfiguration; let application; let user = { username: 'FlowUser', - password: 'Test123!', + password: 'SomeP@ssw0rd', firstName: 'Flow', lastName: 'User', email: 'flowuser@acme.fr', diff --git a/gravitee-am-test/specs/gateway/login-flow.jest.spec.ts b/gravitee-am-test/specs/gateway/login-flow.jest.spec.ts index 48b376a795c..8d04508988e 100644 --- a/gravitee-am-test/specs/gateway/login-flow.jest.spec.ts +++ b/gravitee-am-test/specs/gateway/login-flow.jest.spec.ts @@ -59,16 +59,16 @@ beforeAll(async () => { describe('multiple user', () => { const contractValue = '1234'; let user1; - const user1Password = 'Zxc123!!'; + const user1Password = 'ZxcPrm7123!!'; let user2; - const commonPassword = 'Asd123!!'; + const commonPassword = 'AsdPrm7123!!'; const commonEmail = 'common@test.com'; let user3; //user3 has same password as user2 let user4; - const user4Password = 'Qwe123!!'; + const user4Password = 'QwePrm7123!!'; let user5; let user6; - const secondCommonPassword = 'Phd123!!'; + const secondCommonPassword = 'PhdPrm7123!!'; const secondCommonEmail = 'second.common@test.com'; beforeAll(async () => { diff --git a/gravitee-am-test/specs/gateway/user-registration.jest.spec.ts b/gravitee-am-test/specs/gateway/user-registration.jest.spec.ts index 1468ba39964..42f11b121f6 100644 --- a/gravitee-am-test/specs/gateway/user-registration.jest.spec.ts +++ b/gravitee-am-test/specs/gateway/user-registration.jest.spec.ts @@ -107,7 +107,7 @@ describe('Register User on domain', () => { lastName: faker.name.lastName(), username: '$£êê', email: faker.internet.email(), - password: 'P@ssw0rd!', + password: 'P@ssw0rd!123', }; await register(user, 'warning=invalid_user_information'); @@ -122,7 +122,7 @@ describe('Register User on domain', () => { lastName: faker.name.lastName(), username: faker.name.firstName(), email: faker.random.word(), - password: 'P@ssw0rd!', + password: 'P@ssw0rd!123', }; await register(user, 'warning=invalid_email'); @@ -131,7 +131,7 @@ describe('Register User on domain', () => { expect(usersPage.totalCount).toEqual(0); }); - it('Should not be able to register with invalid email address', async () => { + it('Should not be able to register with invalid password', async () => { const user = { firstName: faker.name.firstName(), lastName: faker.name.lastName(), @@ -152,7 +152,7 @@ describe('Register User on domain', () => { lastName: faker.name.lastName(), username: faker.name.firstName(), email: faker.internet.email(), - password: 'P@ssw0rd!', + password: 'P@ssw0rd!123', }; await register(user, 'success=registration_succeed'); @@ -201,7 +201,7 @@ describe('Register User on domain', () => { lastName: faker.name.lastName(), username: faker.name.firstName(), email: faker.internet.email(), - password: 'P@ssw0rd!', + password: 'P@ssw0rd!123', }; await register(user, 'success=registration_succeed'); @@ -250,7 +250,7 @@ describe('Register User on domain', () => { lastName: faker.name.lastName(), username: faker.name.firstName(), email: faker.internet.email(), - password: 'P@ssw0rd!', + password: 'P@ssw0rd!123', }; await register(user, `https://acustom/web/site?client_id=${clientId}`, true); diff --git a/postman/collections/graviteeio-am-openid-ciba-collection.json b/postman/collections/graviteeio-am-openid-ciba-collection.json index a4a019e4836..ac025123408 100644 --- a/postman/collections/graviteeio-am-openid-ciba-collection.json +++ b/postman/collections/graviteeio-am-openid-ciba-collection.json @@ -369,7 +369,7 @@ ], "body": { "mode": "raw", - "raw": "{\"firstName\":\"Alice\",\"lastName\":\"Wonder\",\"email\":\"alice@acme.fr\",\"username\":\"alice\",\"password\": \"Test123!\"}", + "raw": "{\"firstName\":\"Alice\",\"lastName\":\"Wonder\",\"email\":\"alice@acme.fr\",\"username\":\"alice\",\"password\": \"SomeP@ssw0rd\"}", "options": { "raw": { "language": "json" @@ -1711,7 +1711,7 @@ }, { "key": "password", - "value": "Test123!", + "value": "SomeP@ssw0rd", "type": "text" } ]