From 757c524cc508a662289284b85223a2e3c9a08fa9 Mon Sep 17 00:00:00 2001 From: Gilvan Filho Date: Thu, 7 Mar 2024 14:26:14 -0300 Subject: [PATCH] Password policy for not having username in the password closes #27643 Signed-off-by: Gilvan Filho --- .../release_notes/topics/25_0_0.adoc | 4 ++ ...ontainsUsernamePasswordPolicyProvider.java | 56 +++++++++++++++ ...UsernamePasswordPolicyProviderFactory.java | 70 +++++++++++++++++++ ...cloak.policy.PasswordPolicyProviderFactory | 1 + .../testsuite/forms/RegisterTest.java | 32 +++++++++ .../messages/messages_pt_BR.properties | 1 + .../admin/messages/messages_pt_BR.properties | 1 + .../login/messages/messages_pt_BR.properties | 1 + .../account/messages/messages_en.properties | 1 + .../admin/messages/messages_en.properties | 1 + .../login/messages/messages_en.properties | 1 + 11 files changed, 169 insertions(+) create mode 100644 server-spi-private/src/main/java/org/keycloak/policy/NotContainsUsernamePasswordPolicyProvider.java create mode 100644 server-spi-private/src/main/java/org/keycloak/policy/NotContainsUsernamePasswordPolicyProviderFactory.java diff --git a/docs/documentation/release_notes/topics/25_0_0.adoc b/docs/documentation/release_notes/topics/25_0_0.adoc index 5d6f1ffc8c..0b6773367a 100644 --- a/docs/documentation/release_notes/topics/25_0_0.adoc +++ b/docs/documentation/release_notes/topics/25_0_0.adoc @@ -19,6 +19,10 @@ The following methods for setting custom cookies have been removed: * `HttpCookie` - replaced by `NewCookie.Builder` * `HttpResponse.setCookieIfAbsent(HttpCookie cookie)` - replaced by `HttpResponse.setCookieIfAbsent(NewCookie cookie)` += Password policy for check if password contains Username + +Keycloak supports a new password policy that allows you to deny user passwords which contains the user username. + = Searching by user attribute no longer case insensitive When searching for users by user attribute, {project_name} no longer searches for user attribute names forcing lower case comparisons. The goal of this change was to speed up searches by using {project_name}'s native index on the user attribute table. If your database collation is case-insensitive, your search results will stay the same. If your database collation is case-sensitive, you might see less search results than before. diff --git a/server-spi-private/src/main/java/org/keycloak/policy/NotContainsUsernamePasswordPolicyProvider.java b/server-spi-private/src/main/java/org/keycloak/policy/NotContainsUsernamePasswordPolicyProvider.java new file mode 100644 index 0000000000..c0958c525b --- /dev/null +++ b/server-spi-private/src/main/java/org/keycloak/policy/NotContainsUsernamePasswordPolicyProvider.java @@ -0,0 +1,56 @@ +/* + * Copyright 2024 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * 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 org.keycloak.policy; + +import org.keycloak.models.KeycloakContext; +import org.keycloak.models.RealmModel; +import org.keycloak.models.UserModel; + +public class NotContainsUsernamePasswordPolicyProvider implements PasswordPolicyProvider { + + private static final String ERROR_MESSAGE = "invalidPasswordNotContainsUsernameMessage"; + + private KeycloakContext context; + + public NotContainsUsernamePasswordPolicyProvider(KeycloakContext context) { + this.context = context; + } + + @Override + public PolicyError validate(String username, String password) { + if (username == null) { + return null; + } + return username.contains(password) ? new PolicyError(ERROR_MESSAGE) : null; + } + + @Override + public PolicyError validate(RealmModel realm, UserModel user, String password) { + return validate(user.getUsername(), password); + } + + @Override + public Object parseConfig(String value) { + return null; + } + + @Override + public void close() { + } + +} diff --git a/server-spi-private/src/main/java/org/keycloak/policy/NotContainsUsernamePasswordPolicyProviderFactory.java b/server-spi-private/src/main/java/org/keycloak/policy/NotContainsUsernamePasswordPolicyProviderFactory.java new file mode 100644 index 0000000000..a7bf0c9573 --- /dev/null +++ b/server-spi-private/src/main/java/org/keycloak/policy/NotContainsUsernamePasswordPolicyProviderFactory.java @@ -0,0 +1,70 @@ +/* + * Copyright 2024 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * 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 org.keycloak.policy; + +import org.keycloak.Config; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.KeycloakSessionFactory; + +public class NotContainsUsernamePasswordPolicyProviderFactory implements PasswordPolicyProviderFactory { + + public static final String ID = "notContainsUsername"; + + @Override + public String getId() { + return ID; + } + + @Override + public PasswordPolicyProvider create(KeycloakSession session) { + return new NotContainsUsernamePasswordPolicyProvider(session.getContext()); + } + + @Override + public void init(Config.Scope config) { + } + + @Override + public void postInit(KeycloakSessionFactory factory) { + } + + @Override + public String getDisplayName() { + return "Not Contains Username"; + } + + @Override + public String getConfigType() { + return null; + } + + @Override + public String getDefaultConfigValue() { + return null; + } + + @Override + public boolean isMultiplSupported() { + return false; + } + + @Override + public void close() { + } + +} diff --git a/server-spi-private/src/main/resources/META-INF/services/org.keycloak.policy.PasswordPolicyProviderFactory b/server-spi-private/src/main/resources/META-INF/services/org.keycloak.policy.PasswordPolicyProviderFactory index f9b9167f03..fbcdb93771 100644 --- a/server-spi-private/src/main/resources/META-INF/services/org.keycloak.policy.PasswordPolicyProviderFactory +++ b/server-spi-private/src/main/resources/META-INF/services/org.keycloak.policy.PasswordPolicyProviderFactory @@ -24,6 +24,7 @@ org.keycloak.policy.LengthPasswordPolicyProviderFactory org.keycloak.policy.LowerCasePasswordPolicyProviderFactory org.keycloak.policy.MaximumLengthPasswordPolicyProviderFactory org.keycloak.policy.NotUsernamePasswordPolicyProviderFactory +org.keycloak.policy.NotContainsUsernamePasswordPolicyProviderFactory org.keycloak.policy.RegexPatternsPasswordPolicyProviderFactory org.keycloak.policy.SpecialCharsPasswordPolicyProviderFactory org.keycloak.policy.UpperCasePasswordPolicyProviderFactory diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/RegisterTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/RegisterTest.java index 202c9903d9..cf167b38c6 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/RegisterTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/RegisterTest.java @@ -597,6 +597,38 @@ public class RegisterTest extends AbstractTestRealmKeycloakTest { } } + // KEYCLOAK-27643 + @Test + public void registerUserNotContainsUsernamePasswordPolicy() throws IOException { + try (RealmAttributeUpdater rau = getRealmAttributeUpdater().setPasswordPolicy("notContainsUsername").update()) { + loginPage.open(); + + assertTrue(loginPage.isCurrent()); + + loginPage.clickRegister(); + registerPage.assertCurrent(); + + registerPage.register("firstName", "lastName", "registerUserNotContainsUsername@email", "registerUserNotContainsUsername", "registerUserNotContainsUsername", "registerUserNotContainsUsername"); + + assertTrue(registerPage.isCurrent()); + assertEquals("Invalid password: Can not contains the username.", registerPage.getInputPasswordErrors().getPasswordError()); + + try (Response response = adminClient.realm("test").users().create(UserBuilder.create().username("registerUserNotContainsUsername").build())) { + assertEquals(Response.Status.CREATED.getStatusCode(), response.getStatus()); + } + + registerPage.register("firstName", "lastName", "registerUserNotContainsUsername@email", "registerUserNotContainsUsername", "registerUserNotContainsUsername", "registerUserNotContainsUsername"); + + assertTrue(registerPage.isCurrent()); + assertEquals("Username already exists.", registerPage.getInputAccountErrors().getUsernameError()); + + registerPage.register("firstName", "lastName", "registerUserNotContainsUsername@email", null, "password", "password"); + + assertTrue(registerPage.isCurrent()); + assertEquals("Please specify username.", registerPage.getInputAccountErrors().getUsernameError()); + } + } + // KEYCLOAK-12729 @Test public void registerUserNotEmailPasswordPolicy() throws IOException { diff --git a/themes/src/main/resources-community/theme/base/account/messages/messages_pt_BR.properties b/themes/src/main/resources-community/theme/base/account/messages/messages_pt_BR.properties index 8a0de30115..b7c52538c1 100644 --- a/themes/src/main/resources-community/theme/base/account/messages/messages_pt_BR.properties +++ b/themes/src/main/resources-community/theme/base/account/messages/messages_pt_BR.properties @@ -214,6 +214,7 @@ invalidPasswordMinDigitsMessage=Senha inválida\: deve conter pelo menos {0} nú invalidPasswordMinUpperCaseCharsMessage=Senha inválida\: deve conter pelo menos {0} letra(s) maiúscula(s). invalidPasswordMinSpecialCharsMessage=Senha inválida\: deve conter pelo menos {0} caractere(s) especial(is). invalidPasswordNotUsernameMessage=Senha inválida\: não pode ser igual ao nome de usuário. +invalidPasswordNotContainsUsernameMessage=Senha inválida\: não pode conter o nome de usuário. invalidPasswordNotEmailMessage=Senha inválida: não pode ser igual ao endereço de e-mail. invalidPasswordRegexPatternMessage=Senha inválida\: não corresponde ao(s) padrão(ões) da expressão regular. invalidPasswordHistoryMessage=Senha inválida\: não pode ser igual a qualquer uma da(s) última(s) {0} senha(s). diff --git a/themes/src/main/resources-community/theme/base/admin/messages/messages_pt_BR.properties b/themes/src/main/resources-community/theme/base/admin/messages/messages_pt_BR.properties index cc4a29c7c5..554b87ca7c 100644 --- a/themes/src/main/resources-community/theme/base/admin/messages/messages_pt_BR.properties +++ b/themes/src/main/resources-community/theme/base/admin/messages/messages_pt_BR.properties @@ -4,6 +4,7 @@ invalidPasswordMinDigitsMessage=Senha inválida: deve conter ao menos {0} digito invalidPasswordMinUpperCaseCharsMessage=Senha inválida: deve conter ao menos {0} caracteres maiúsculos. invalidPasswordMinSpecialCharsMessage=Senha inválida: deve conter ao menos {0} caracteres especiais. invalidPasswordNotUsernameMessage=Senha inválida: não deve ser igual ao nome de usuário. +invalidPasswordNotContainsUsernameMessage=Senha inválida\: não pode conter o nome de usuário. invalidPasswordRegexPatternMessage=Senha inválida: falha ao passar por padrões. invalidPasswordHistoryMessage=Senha inválida: não deve ser igual às últimas {0} senhas. diff --git a/themes/src/main/resources-community/theme/base/login/messages/messages_pt_BR.properties b/themes/src/main/resources-community/theme/base/login/messages/messages_pt_BR.properties index 4e90d5e4e1..4ab33fbd50 100755 --- a/themes/src/main/resources-community/theme/base/login/messages/messages_pt_BR.properties +++ b/themes/src/main/resources-community/theme/base/login/messages/messages_pt_BR.properties @@ -241,6 +241,7 @@ invalidPasswordMinLowerCaseCharsMessage=Senha inválida\: deve conter pelo menos invalidPasswordMinUpperCaseCharsMessage=Senha inválida\: deve conter pelo menos {0} letra(s) maiúscula(s). invalidPasswordMinSpecialCharsMessage=Senha inválida\: deve conter pelo menos {0} caractere(s) especial(is). invalidPasswordNotUsernameMessage=Senha inválida\: não pode ser igual ao nome de usuário +invalidPasswordNotContainsUsernameMessage=Senha inválida\: não pode conter o nome de usuário. invalidPasswordNotEmailMessage=Senha inválida: não pode ser igual ao endereço de e-mail. invalidPasswordRegexPatternMessage=Senha inválida\: não corresponde ao(s) padrão(ões) de expressão regular. invalidPasswordHistoryMessage=Senha inválida\: não pode ser igual a qualquer uma da(s) última(s) {0} senha(s). diff --git a/themes/src/main/resources/theme/base/account/messages/messages_en.properties b/themes/src/main/resources/theme/base/account/messages/messages_en.properties index 0b39d2dbcf..1bb5cf3998 100755 --- a/themes/src/main/resources/theme/base/account/messages/messages_en.properties +++ b/themes/src/main/resources/theme/base/account/messages/messages_en.properties @@ -230,6 +230,7 @@ invalidPasswordMinDigitsMessage=Invalid password: must contain at least {0} nume invalidPasswordMinUpperCaseCharsMessage=Invalid password: must contain at least {0} upper case characters. invalidPasswordMinSpecialCharsMessage=Invalid password: must contain at least {0} special characters. invalidPasswordNotUsernameMessage=Invalid password: must not be equal to the username. +invalidPasswordNotContainsUsernameMessage=Invalid password: Can not contains the username. invalidPasswordNotEmailMessage=Invalid password: must not be equal to the email. invalidPasswordRegexPatternMessage=Invalid password: fails to match regex pattern(s). invalidPasswordHistoryMessage=Invalid password: must not be equal to any of last {0} passwords. diff --git a/themes/src/main/resources/theme/base/admin/messages/messages_en.properties b/themes/src/main/resources/theme/base/admin/messages/messages_en.properties index 21cb14c568..87f5cd7f9b 100644 --- a/themes/src/main/resources/theme/base/admin/messages/messages_en.properties +++ b/themes/src/main/resources/theme/base/admin/messages/messages_en.properties @@ -5,6 +5,7 @@ invalidPasswordMinDigitsMessage=Invalid password: must contain at least {0} nume invalidPasswordMinUpperCaseCharsMessage=Invalid password: must contain at least {0} upper case characters. invalidPasswordMinSpecialCharsMessage=Invalid password: must contain at least {0} special characters. invalidPasswordNotUsernameMessage=Invalid password: must not be equal to the username. +invalidPasswordNotContainsUsernameMessage=Invalid password: Can not contains the username. invalidPasswordNotEmailMessage=Invalid password: must not be equal to the email. invalidPasswordRegexPatternMessage=Invalid password: fails to match regex pattern(s). invalidPasswordHistoryMessage=Invalid password: must not be equal to any of last {0} passwords. diff --git a/themes/src/main/resources/theme/base/login/messages/messages_en.properties b/themes/src/main/resources/theme/base/login/messages/messages_en.properties index f04ff18e3d..2767c977c0 100755 --- a/themes/src/main/resources/theme/base/login/messages/messages_en.properties +++ b/themes/src/main/resources/theme/base/login/messages/messages_en.properties @@ -307,6 +307,7 @@ invalidPasswordMinLowerCaseCharsMessage=Invalid password: must contain at least invalidPasswordMinUpperCaseCharsMessage=Invalid password: must contain at least {0} upper case characters. invalidPasswordMinSpecialCharsMessage=Invalid password: must contain at least {0} special characters. invalidPasswordNotUsernameMessage=Invalid password: must not be equal to the username. +invalidPasswordNotContainsUsernameMessage=Invalid password: Can not contains the username. invalidPasswordNotEmailMessage=Invalid password: must not be equal to the email. invalidPasswordRegexPatternMessage=Invalid password: fails to match regex pattern(s). invalidPasswordHistoryMessage=Invalid password: must not be equal to any of last {0} passwords.