From 7209a95dcecf9f175aaf66513efad02583a8b9e5 Mon Sep 17 00:00:00 2001 From: Bill Burke Date: Thu, 22 Sep 2016 08:34:45 -0400 Subject: [PATCH 1/3] credential refactoring --- .../idm/CredentialRepresentation.java | 13 ++ examples/providers/authenticator/pom.xml | 10 + .../SecretQuestionAuthenticator.java | 16 +- .../SecretQuestionCredentialProvider.java | 117 ++++++++++ ...cretQuestionCredentialProviderFactory.java | 37 +++ .../SecretQuestionRequiredAction.java | 9 +- ...cloak.credential.CredentialProviderFactory | 1 + .../BasePropertiesFederationProvider.java | 44 ++-- ...ClasspathPropertiesFederationProvider.java | 9 + .../FilePropertiesFederationProvider.java | 9 + .../properties/ReadonlyUserModelProxy.java | 15 -- .../properties/WritableUserModelProxy.java | 22 -- .../examples/storage/user/UserAdapter.java | 9 - .../kerberos/KerberosFederationProvider.java | 62 +++-- .../ReadOnlyKerberosUserModelDelegate.java | 8 - .../ldap/LDAPFederationProvider.java | 71 ++++-- .../ldap/ReadonlyLDAPUserModelDelegate.java | 8 - .../ldap/WritableLDAPUserModelDelegate.java | 16 -- .../ldap/mappers/PasswordUpdated.java | 29 +++ .../msad/MSADUserAccountControlMapper.java | 94 ++++---- .../sssd/ReadonlySSSDUserModelDelegate.java | 16 -- .../sssd/SSSDFederationProvider.java | 38 +-- .../models/cache/infinispan/UserAdapter.java | 18 -- .../cache/infinispan/UserCacheSession.java | 10 - .../cache/infinispan/entities/CachedUser.java | 6 - .../keycloak/models/jpa/JpaUserProvider.java | 13 +- .../org/keycloak/models/jpa/UserAdapter.java | 193 ---------------- .../jpa/JpaUserFederatedStorageProvider.java | 54 ++++- .../storage/jpa/entity/FederatedUser.java | 85 +++++++ .../META-INF/jpa-changelog-2.3.0.xml | 39 ++++ .../META-INF/jpa-changelog-master.xml | 1 + .../main/resources/META-INF/persistence.xml | 1 + .../keycloak/adapters/MongoUserProvider.java | 10 - .../mongo/keycloak/adapters/UserAdapter.java | 199 ---------------- .../credential/CredentialProviderFactory.java | 27 +-- .../keycloak/credential/CredentialSpi.java | 50 ++++ .../credential/hash/PasswordHashProvider.java | 35 +++ .../hash/PasswordHashProviderFactory.java | 27 +++ .../credential/hash/PasswordHashSpi.java | 48 ++++ .../keycloak/migration/MigrationModel.java | 2 +- .../models/UserCredentialManager.java | 57 +++++ .../models/UserFederationManager.java | 90 +------- .../models/UserFederationProvider.java | 25 +- .../java/org/keycloak/models/UserModel.java | 6 - .../org/keycloak/models/UserProvider.java | 2 - .../models/utils/CredentialValidation.java | 135 ----------- .../models/utils/RepresentationToModel.java | 15 +- .../models/utils/UserModelDelegate.java | 15 -- .../DefaultPasswordPolicyManagerProvider.java | 14 +- .../policy/DigitsPasswordPolicyProvider.java | 3 +- ...eExpiredPasswordPolicyProviderFactory.java | 3 +- ...lgorithmPasswordPolicyProviderFactory.java | 3 +- ...erationsPasswordPolicyProviderFactory.java | 3 +- .../policy/HistoryPasswordPolicyProvider.java | 58 ++--- .../policy/LengthPasswordPolicyProvider.java | 3 +- .../LowerCasePasswordPolicyProvider.java | 3 +- .../NotUsernamePasswordPolicyProvider.java | 3 +- .../policy/PasswordPolicyManagerProvider.java | 3 +- .../policy/PasswordPolicyProvider.java | 3 +- .../RegexPatternsPasswordPolicyProvider.java | 3 +- .../SpecialCharsPasswordPolicyProvider.java | 3 +- .../UpperCasePasswordPolicyProvider.java | 3 +- .../storage/adapter/AbstractUserAdapter.java | 17 -- .../AbstractUserAdapterFederatedStorage.java | 17 -- .../UserFederatedStorageProvider.java | 5 + .../services/org.keycloak.provider.Spi | 2 + .../AbstractUsernameFormAuthenticator.java | 5 +- .../browser/OTPFormAuthenticator.java | 7 +- .../directgrant/ValidateOTP.java | 5 +- .../directgrant/ValidatePassword.java | 3 +- .../authenticators/resetcred/ResetOTP.java | 2 +- .../resetcred/ResetPassword.java | 2 +- .../forms/RegistrationPassword.java | 2 +- .../requiredactions/UpdatePassword.java | 30 +-- .../requiredactions/UpdateTotp.java | 4 +- ...anager.java => OTPCredentialProvider.java} | 14 +- .../OTPCredentialProviderFactory.java | 37 +++ .../PasswordCredentialProvider.java | 216 ++++++++++++++++++ .../PasswordCredentialProviderFactory.java | 37 +++ .../UserCredentialStoreManager.java | 131 ++++++----- .../hash/Pbkdf2PasswordHashProvider.java | 138 +++++++++++ .../exportimport/util/ExportUtils.java | 8 +- .../account/freemarker/model/TotpBean.java | 2 +- .../authenticator/HttpBasicAuthenticator.java | 2 +- .../org/keycloak/services/ServicesLogger.java | 2 +- .../services/managers/ApplianceBootstrap.java | 2 +- .../services/resources/AccountService.java | 40 +--- .../resources/KeycloakApplication.java | 2 +- .../resources/admin/UsersResource.java | 5 +- .../keycloak/storage/UserStorageManager.java | 49 ---- ...cloak.credential.CredentialProviderFactory | 2 + ...redential.hash.PasswordHashProviderFactory | 1 + .../DummyUserFederationProvider.java | 52 +++-- .../rest/TestingResourceProvider.java | 2 +- .../testsuite/forms/BruteForceTest.java | 13 +- .../testsuite/forms/LoginHotpTest.java | 10 +- .../keycloak/testsuite/forms/LoginTest.java | 4 +- .../federation/KerberosLdapTest.java | 5 + .../federation/ldap/FederationTestUtils.java | 2 +- .../FederationProvidersIntegrationTest.java | 9 +- .../federation/storage/UserMapStorage.java | 72 +++--- .../storage/UserPropertyFileStorage.java | 29 ++- .../federation/storage/UserStorageTest.java | 5 +- .../keycloak/testsuite/model/AdapterTest.java | 17 +- .../testsuite/model/MultipleRealmsTest.java | 12 +- .../testsuite/util/cli/UserCommands.java | 2 +- 106 files changed, 1579 insertions(+), 1368 deletions(-) create mode 100644 examples/providers/authenticator/src/main/java/org/keycloak/examples/authenticator/SecretQuestionCredentialProvider.java create mode 100644 examples/providers/authenticator/src/main/java/org/keycloak/examples/authenticator/SecretQuestionCredentialProviderFactory.java create mode 100644 examples/providers/authenticator/src/main/resources/META-INF/services/org.keycloak.credential.CredentialProviderFactory create mode 100644 federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/PasswordUpdated.java create mode 100644 model/jpa/src/main/java/org/keycloak/storage/jpa/entity/FederatedUser.java create mode 100755 model/jpa/src/main/resources/META-INF/jpa-changelog-2.3.0.xml create mode 100644 server-spi/src/main/java/org/keycloak/credential/CredentialSpi.java create mode 100644 server-spi/src/main/java/org/keycloak/credential/hash/PasswordHashProvider.java create mode 100644 server-spi/src/main/java/org/keycloak/credential/hash/PasswordHashProviderFactory.java create mode 100644 server-spi/src/main/java/org/keycloak/credential/hash/PasswordHashSpi.java rename services/src/main/java/org/keycloak/credential/{LocalOTPCredentialManager.java => OTPCredentialProvider.java} (93%) create mode 100644 services/src/main/java/org/keycloak/credential/OTPCredentialProviderFactory.java create mode 100644 services/src/main/java/org/keycloak/credential/PasswordCredentialProvider.java create mode 100644 services/src/main/java/org/keycloak/credential/PasswordCredentialProviderFactory.java create mode 100644 services/src/main/java/org/keycloak/credential/hash/Pbkdf2PasswordHashProvider.java create mode 100644 services/src/main/resources/META-INF/services/org.keycloak.credential.CredentialProviderFactory create mode 100644 services/src/main/resources/META-INF/services/org.keycloak.credential.hash.PasswordHashProviderFactory diff --git a/core/src/main/java/org/keycloak/representations/idm/CredentialRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/CredentialRepresentation.java index 568dfd157ded..f48d726dd77f 100755 --- a/core/src/main/java/org/keycloak/representations/idm/CredentialRepresentation.java +++ b/core/src/main/java/org/keycloak/representations/idm/CredentialRepresentation.java @@ -17,6 +17,10 @@ package org.keycloak.representations.idm; +import org.keycloak.common.util.MultivaluedHashMap; + +import java.util.Map; + /** * @author Bill Burke * @version $Revision: 1 $ @@ -45,6 +49,7 @@ public class CredentialRepresentation { private Integer digits; private Integer period; private Long createdDate; + private MultivaluedHashMap config; // only used when updating a credential. Might set required action protected Boolean temporary; @@ -144,4 +149,12 @@ public Long getCreatedDate() { public void setCreatedDate(Long createdDate) { this.createdDate = createdDate; } + + public MultivaluedHashMap getConfig() { + return config; + } + + public void setConfig(MultivaluedHashMap config) { + this.config = config; + } } diff --git a/examples/providers/authenticator/pom.xml b/examples/providers/authenticator/pom.xml index 5a1b100d1546..0123a91977bb 100755 --- a/examples/providers/authenticator/pom.xml +++ b/examples/providers/authenticator/pom.xml @@ -55,5 +55,15 @@ authenticator-required-action-example + + + org.apache.maven.plugins + maven-compiler-plugin + + 1.8 + 1.8 + + + diff --git a/examples/providers/authenticator/src/main/java/org/keycloak/examples/authenticator/SecretQuestionAuthenticator.java b/examples/providers/authenticator/src/main/java/org/keycloak/examples/authenticator/SecretQuestionAuthenticator.java index 3fdd4ff6e41a..e25ff812b671 100755 --- a/examples/providers/authenticator/src/main/java/org/keycloak/examples/authenticator/SecretQuestionAuthenticator.java +++ b/examples/providers/authenticator/src/main/java/org/keycloak/examples/authenticator/SecretQuestionAuthenticator.java @@ -23,6 +23,7 @@ import org.keycloak.models.AuthenticatorConfigModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; +import org.keycloak.models.UserCredentialModel; import org.keycloak.models.UserCredentialValueModel; import org.keycloak.models.UserModel; import org.keycloak.services.util.CookieHelper; @@ -96,15 +97,10 @@ protected void setCookie(AuthenticationFlowContext context) { protected boolean validateAnswer(AuthenticationFlowContext context) { MultivaluedMap formData = context.getHttpRequest().getDecodedFormParameters(); String secret = formData.getFirst("secret_answer"); - UserCredentialValueModel cred = null; - for (UserCredentialValueModel model : context.getUser().getCredentialsDirectly()) { - if (model.getType().equals(CREDENTIAL_TYPE)) { - cred = model; - break; - } - } - - return cred.getValue().equals(secret); + UserCredentialModel input = new UserCredentialModel(); + input.setType(SecretQuestionCredentialProvider.SECRET_QUESTION); + input.setValue(secret); + return context.getSession().userCredentialManager().isValid(context.getRealm(), context.getUser(), input); } @Override @@ -114,7 +110,7 @@ public boolean requiresUser() { @Override public boolean configuredFor(KeycloakSession session, RealmModel realm, UserModel user) { - return session.users().configuredForCredentialType(CREDENTIAL_TYPE, realm, user); + return session.userCredentialManager().isConfiguredFor(realm, user, SecretQuestionCredentialProvider.SECRET_QUESTION); } @Override diff --git a/examples/providers/authenticator/src/main/java/org/keycloak/examples/authenticator/SecretQuestionCredentialProvider.java b/examples/providers/authenticator/src/main/java/org/keycloak/examples/authenticator/SecretQuestionCredentialProvider.java new file mode 100644 index 000000000000..61e07a8cf422 --- /dev/null +++ b/examples/providers/authenticator/src/main/java/org/keycloak/examples/authenticator/SecretQuestionCredentialProvider.java @@ -0,0 +1,117 @@ +/* + * Copyright 2016 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.examples.authenticator; + +import org.keycloak.common.util.Time; +import org.keycloak.credential.CredentialInput; +import org.keycloak.credential.CredentialInputUpdater; +import org.keycloak.credential.CredentialInputValidator; +import org.keycloak.credential.CredentialModel; +import org.keycloak.credential.CredentialProvider; +import org.keycloak.credential.PasswordCredentialProvider; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.RealmModel; +import org.keycloak.models.UserCredentialModel; +import org.keycloak.models.UserModel; +import org.keycloak.models.cache.CachedUserModel; +import org.keycloak.models.cache.OnUserCache; + +import java.util.List; + +/** + * @author Bill Burke + * @version $Revision: 1 $ + */ +public class SecretQuestionCredentialProvider implements CredentialProvider, CredentialInputValidator, CredentialInputUpdater, OnUserCache { + public static final String SECRET_QUESTION = "SECRET_QUESTION"; + public static final String CACHE_KEY = SecretQuestionCredentialProvider.class.getName() + "." + SECRET_QUESTION; + + protected KeycloakSession session; + + public SecretQuestionCredentialProvider(KeycloakSession session) { + this.session = session; + } + + public CredentialModel getSecret(RealmModel realm, UserModel user) { + CredentialModel secret = null; + if (user instanceof CachedUserModel) { + CachedUserModel cached = (CachedUserModel)user; + secret = (CredentialModel)cached.getCachedWith().get(CACHE_KEY); + + } else { + List creds = session.userCredentialManager().getStoredCredentialsByType(realm, user, SECRET_QUESTION); + if (!creds.isEmpty()) secret = creds.get(0); + } + return secret; + } + + + @Override + public boolean updateCredential(RealmModel realm, UserModel user, CredentialInput input) { + if (!SECRET_QUESTION.equals(input.getType())) return false; + if (!(input instanceof UserCredentialModel)) return false; + UserCredentialModel credInput = (UserCredentialModel) input; + List creds = session.userCredentialManager().getStoredCredentialsByType(realm, user, SECRET_QUESTION); + if (creds.isEmpty()) { + CredentialModel secret = new CredentialModel(); + secret.setType(SECRET_QUESTION); + secret.setValue(credInput.getValue()); + secret.setCreatedDate(Time.toMillis(Time.currentTime())); + session.userCredentialManager().createCredential(realm ,user, secret); + } else { + creds.get(0).setValue(credInput.getValue()); + session.userCredentialManager().updateCredential(realm, user, creds.get(0)); + } + session.getUserCache().evict(realm, user); + return true; + } + + @Override + public void disableCredentialType(RealmModel realm, UserModel user, String credentialType) { + if (!SECRET_QUESTION.equals(credentialType)) return; + session.userCredentialManager().disableCredential(realm, user, credentialType); + session.getUserCache().evict(realm, user); + + } + + @Override + public boolean supportsCredentialType(String credentialType) { + return SECRET_QUESTION.equals(credentialType); + } + + @Override + public boolean isConfiguredFor(RealmModel realm, UserModel user, String credentialType) { + if (!SECRET_QUESTION.equals(credentialType)) return false; + return getSecret(realm, user) != null; + } + + @Override + public boolean isValid(RealmModel realm, UserModel user, CredentialInput input) { + if (!SECRET_QUESTION.equals(input.getType())) return false; + if (!(input instanceof UserCredentialModel)) return false; + + String secret = getSecret(realm, user).getValue(); + + return secret != null && ((UserCredentialModel)input).getValue().equals(secret); + } + + @Override + public void onCache(RealmModel realm, CachedUserModel user) { + List creds = session.userCredentialManager().getStoredCredentialsByType(realm, user, SECRET_QUESTION); + if (!creds.isEmpty()) user.getCachedWith().put(CACHE_KEY, creds.get(0)); + } +} diff --git a/examples/providers/authenticator/src/main/java/org/keycloak/examples/authenticator/SecretQuestionCredentialProviderFactory.java b/examples/providers/authenticator/src/main/java/org/keycloak/examples/authenticator/SecretQuestionCredentialProviderFactory.java new file mode 100644 index 000000000000..98b65ae9df94 --- /dev/null +++ b/examples/providers/authenticator/src/main/java/org/keycloak/examples/authenticator/SecretQuestionCredentialProviderFactory.java @@ -0,0 +1,37 @@ +/* + * Copyright 2016 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.examples.authenticator; + +import org.keycloak.credential.CredentialProvider; +import org.keycloak.credential.CredentialProviderFactory; +import org.keycloak.models.KeycloakSession; + +/** + * @author Bill Burke + * @version $Revision: 1 $ + */ +public class SecretQuestionCredentialProviderFactory implements CredentialProviderFactory { + @Override + public String getId() { + return "secret-question"; + } + + @Override + public CredentialProvider create(KeycloakSession session) { + return new SecretQuestionCredentialProvider(session); + } +} diff --git a/examples/providers/authenticator/src/main/java/org/keycloak/examples/authenticator/SecretQuestionRequiredAction.java b/examples/providers/authenticator/src/main/java/org/keycloak/examples/authenticator/SecretQuestionRequiredAction.java index f196205b70b3..a1a65459e74a 100755 --- a/examples/providers/authenticator/src/main/java/org/keycloak/examples/authenticator/SecretQuestionRequiredAction.java +++ b/examples/providers/authenticator/src/main/java/org/keycloak/examples/authenticator/SecretQuestionRequiredAction.java @@ -19,6 +19,7 @@ import org.keycloak.authentication.RequiredActionContext; import org.keycloak.authentication.RequiredActionProvider; +import org.keycloak.models.UserCredentialModel; import org.keycloak.models.UserCredentialValueModel; import javax.ws.rs.core.Response; @@ -45,10 +46,10 @@ public void requiredActionChallenge(RequiredActionContext context) { @Override public void processAction(RequiredActionContext context) { String answer = (context.getHttpRequest().getDecodedFormParameters().getFirst("secret_answer")); - UserCredentialValueModel model = new UserCredentialValueModel(); - model.setValue(answer); - model.setType(SecretQuestionAuthenticator.CREDENTIAL_TYPE); - context.getUser().updateCredentialDirectly(model); + UserCredentialModel input = new UserCredentialModel(); + input.setType(SecretQuestionCredentialProvider.SECRET_QUESTION); + input.setValue(answer); + context.getSession().userCredentialManager().updateCredential(context.getRealm(), context.getUser(), input); context.success(); } diff --git a/examples/providers/authenticator/src/main/resources/META-INF/services/org.keycloak.credential.CredentialProviderFactory b/examples/providers/authenticator/src/main/resources/META-INF/services/org.keycloak.credential.CredentialProviderFactory new file mode 100644 index 000000000000..a221e371cc55 --- /dev/null +++ b/examples/providers/authenticator/src/main/resources/META-INF/services/org.keycloak.credential.CredentialProviderFactory @@ -0,0 +1 @@ + org.keycloak.examples.authenticator.SecretQuestionCredentialProviderFactory \ No newline at end of file diff --git a/examples/providers/federation-provider/src/main/java/org/keycloak/examples/federation/properties/BasePropertiesFederationProvider.java b/examples/providers/federation-provider/src/main/java/org/keycloak/examples/federation/properties/BasePropertiesFederationProvider.java index ddf54404d308..3ee3a0b77926 100755 --- a/examples/providers/federation-provider/src/main/java/org/keycloak/examples/federation/properties/BasePropertiesFederationProvider.java +++ b/examples/providers/federation-provider/src/main/java/org/keycloak/examples/federation/properties/BasePropertiesFederationProvider.java @@ -17,6 +17,7 @@ package org.keycloak.examples.federation.properties; +import org.keycloak.credential.CredentialInput; import org.keycloak.models.CredentialValidationOutput; import org.keycloak.models.GroupModel; import org.keycloak.models.KeycloakSession; @@ -146,48 +147,29 @@ public boolean isValid(RealmModel realm, UserModel local) { return properties.containsKey(local.getUsername()); } - /** - * hardcoded to only return PASSWORD - * - * @param user - * @return - */ @Override - public Set getSupportedCredentialTypes(UserModel user) { + public Set getSupportedCredentialTypes() { return supportedCredentialTypes; } @Override - public Set getSupportedCredentialTypes() { - return supportedCredentialTypes; + public boolean isValid(RealmModel realm, UserModel user, CredentialInput input) { + if (!supportsCredentialType(input.getType()) || !(input instanceof UserCredentialModel)) return false; + + UserCredentialModel cred = (UserCredentialModel)input; + String password = properties.getProperty(user.getUsername()); + if (password == null) return false; + return password.equals(cred.getValue()); } @Override - public boolean validCredentials(RealmModel realm, UserModel user, List input) { - for (UserCredentialModel cred : input) { - if (cred.getType().equals(UserCredentialModel.PASSWORD)) { - String password = properties.getProperty(user.getUsername()); - if (password == null) return false; - return password.equals(cred.getValue()); - } else { - return false; // invalid cred type - } - } - return false; + public boolean isConfiguredFor(RealmModel realm, UserModel user, String credentialType) { + return getSupportedCredentialTypes().contains(credentialType); } @Override - public boolean validCredentials(RealmModel realm, UserModel user, UserCredentialModel... input) { - for (UserCredentialModel cred : input) { - if (cred.getType().equals(UserCredentialModel.PASSWORD)) { - String password = properties.getProperty(user.getUsername()); - if (password == null) return false; - return password.equals(cred.getValue()); - } else { - return false; // invalid cred type - } - } - return true; + public boolean supportsCredentialType(String credentialType) { + return getSupportedCredentialTypes().contains(credentialType); } @Override diff --git a/examples/providers/federation-provider/src/main/java/org/keycloak/examples/federation/properties/ClasspathPropertiesFederationProvider.java b/examples/providers/federation-provider/src/main/java/org/keycloak/examples/federation/properties/ClasspathPropertiesFederationProvider.java index 2776b292adce..ee57104793a1 100755 --- a/examples/providers/federation-provider/src/main/java/org/keycloak/examples/federation/properties/ClasspathPropertiesFederationProvider.java +++ b/examples/providers/federation-provider/src/main/java/org/keycloak/examples/federation/properties/ClasspathPropertiesFederationProvider.java @@ -17,8 +17,10 @@ package org.keycloak.examples.federation.properties; +import org.keycloak.credential.CredentialInput; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; +import org.keycloak.models.UserCredentialModel; import org.keycloak.models.UserFederationProviderModel; import org.keycloak.models.UserModel; @@ -80,6 +82,13 @@ public boolean removeUser(RealmModel realm, UserModel user) { throw new IllegalStateException("Remove not supported"); } + @Override + public boolean updateCredential(RealmModel realm, UserModel user, CredentialInput input) { + return false; + } + @Override + public void disableCredentialType(RealmModel realm, UserModel user, String credentialType) { + } } diff --git a/examples/providers/federation-provider/src/main/java/org/keycloak/examples/federation/properties/FilePropertiesFederationProvider.java b/examples/providers/federation-provider/src/main/java/org/keycloak/examples/federation/properties/FilePropertiesFederationProvider.java index 2bfa93267452..bc506843f883 100755 --- a/examples/providers/federation-provider/src/main/java/org/keycloak/examples/federation/properties/FilePropertiesFederationProvider.java +++ b/examples/providers/federation-provider/src/main/java/org/keycloak/examples/federation/properties/FilePropertiesFederationProvider.java @@ -17,8 +17,10 @@ package org.keycloak.examples.federation.properties; +import org.keycloak.credential.CredentialInput; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; +import org.keycloak.models.UserCredentialModel; import org.keycloak.models.UserFederationProviderModel; import org.keycloak.models.UserModel; @@ -98,6 +100,13 @@ public boolean removeUser(RealmModel realm, UserModel user) { } } + @Override + public boolean updateCredential(RealmModel realm, UserModel user, CredentialInput input) { + return false; + } + @Override + public void disableCredentialType(RealmModel realm, UserModel user, String credentialType) { + } } diff --git a/examples/providers/federation-provider/src/main/java/org/keycloak/examples/federation/properties/ReadonlyUserModelProxy.java b/examples/providers/federation-provider/src/main/java/org/keycloak/examples/federation/properties/ReadonlyUserModelProxy.java index d1cec50d6953..5a1e0df059dd 100755 --- a/examples/providers/federation-provider/src/main/java/org/keycloak/examples/federation/properties/ReadonlyUserModelProxy.java +++ b/examples/providers/federation-provider/src/main/java/org/keycloak/examples/federation/properties/ReadonlyUserModelProxy.java @@ -39,19 +39,4 @@ public void setUsername(String username) { throw new IllegalStateException("Username is readonly"); } - @Override - public void updateCredentialDirectly(UserCredentialValueModel cred) { - if (cred.getType().equals(UserCredentialModel.PASSWORD)) { - throw new IllegalStateException("Passwords are readonly"); - } - super.updateCredentialDirectly(cred); - } - - @Override - public void updateCredential(UserCredentialModel cred) { - if (cred.getType().equals(UserCredentialModel.PASSWORD)) { - throw new IllegalStateException("Passwords are readonly"); - } - super.updateCredential(cred); - } } diff --git a/examples/providers/federation-provider/src/main/java/org/keycloak/examples/federation/properties/WritableUserModelProxy.java b/examples/providers/federation-provider/src/main/java/org/keycloak/examples/federation/properties/WritableUserModelProxy.java index 93f0b3ddf023..e294f82dd323 100755 --- a/examples/providers/federation-provider/src/main/java/org/keycloak/examples/federation/properties/WritableUserModelProxy.java +++ b/examples/providers/federation-provider/src/main/java/org/keycloak/examples/federation/properties/WritableUserModelProxy.java @@ -64,26 +64,4 @@ public void setUsername(String username) { } - @Override - public void updateCredentialDirectly(UserCredentialValueModel cred) { - if (cred.getType().equals(UserCredentialModel.PASSWORD)) { - throw new IllegalStateException("Shouldn't be using this method"); - } - super.updateCredentialDirectly(cred); - } - - @Override - public void updateCredential(UserCredentialModel cred) { - if (cred.getType().equals(UserCredentialModel.PASSWORD)) { - synchronized (provider.getProperties()) { - if (!provider.getProperties().containsKey(delegate.getUsername())) { - throw new IllegalStateException("no user of that in properties file"); - } - provider.getProperties().setProperty(delegate.getUsername(), cred.getValue()); - provider.save(); - } - } else { - super.updateCredential(cred); - } - } } diff --git a/examples/providers/user-storage-jpa/src/main/java/org/keycloak/examples/storage/user/UserAdapter.java b/examples/providers/user-storage-jpa/src/main/java/org/keycloak/examples/storage/user/UserAdapter.java index 6276f7f84298..84f4084d8b7a 100644 --- a/examples/providers/user-storage-jpa/src/main/java/org/keycloak/examples/storage/user/UserAdapter.java +++ b/examples/providers/user-storage-jpa/src/main/java/org/keycloak/examples/storage/user/UserAdapter.java @@ -70,15 +70,6 @@ public String getId() { return keycloakId; } - @Override - public void updateCredential(UserCredentialModel cred) { - if (cred.getType().equals(UserCredentialModel.PASSWORD)) { - entity.setPassword(cred.getValue()); - } else { - super.updateCredential(cred); - } - } - @Override public void setSingleAttribute(String name, String value) { if (name.equals("phone")) { diff --git a/federation/kerberos/src/main/java/org/keycloak/federation/kerberos/KerberosFederationProvider.java b/federation/kerberos/src/main/java/org/keycloak/federation/kerberos/KerberosFederationProvider.java index e6cfae249573..2d0b4b2f20cc 100755 --- a/federation/kerberos/src/main/java/org/keycloak/federation/kerberos/KerberosFederationProvider.java +++ b/federation/kerberos/src/main/java/org/keycloak/federation/kerberos/KerberosFederationProvider.java @@ -26,11 +26,14 @@ import java.util.Set; import org.jboss.logging.Logger; +import org.keycloak.credential.CredentialInput; +import org.keycloak.credential.CredentialModel; import org.keycloak.federation.kerberos.impl.KerberosUsernamePasswordAuthenticator; import org.keycloak.federation.kerberos.impl.SPNEGOAuthenticator; import org.keycloak.models.CredentialValidationOutput; import org.keycloak.models.GroupModel; import org.keycloak.models.KeycloakSession; +import org.keycloak.models.ModelReadOnlyException; import org.keycloak.models.RealmModel; import org.keycloak.models.RoleModel; import org.keycloak.models.UserCredentialModel; @@ -40,6 +43,7 @@ import org.keycloak.models.UserModel; import org.keycloak.common.constants.KerberosConstants; import org.keycloak.services.managers.UserManager; +import org.keycloak.storage.adapter.AbstractUserAdapter; /** * @author Marek Posolda @@ -143,47 +147,44 @@ public boolean isValid(RealmModel realm, UserModel local) { } @Override - public Set getSupportedCredentialTypes(UserModel local) { + public Set getSupportedCredentialTypes() { Set supportedCredTypes = new HashSet(); supportedCredTypes.add(UserCredentialModel.KERBEROS); + return supportedCredTypes; + } - if (kerberosConfig.isAllowPasswordAuthentication()) { - boolean passwordSupported = true; - if (kerberosConfig.getEditMode() == EditMode.UNSYNCED ) { + @Override + public boolean updateCredential(RealmModel realm, UserModel user, CredentialInput input) { + if (!(input instanceof UserCredentialModel) || !CredentialModel.PASSWORD.equals(input.getType())) return false; + if (kerberosConfig.getEditMode() == EditMode.READ_ONLY) { + throw new ModelReadOnlyException("Can't change password in Keycloak database. Change password with your Kerberos server"); + } + return false; + } - // Password from KC database has preference over kerberos password - for (UserCredentialValueModel cred : local.getCredentialsDirectly()) { - if (cred.getType().equals(UserCredentialModel.PASSWORD)) { - passwordSupported = false; - } - } - } + @Override + public void disableCredentialType(RealmModel realm, UserModel user, String credentialType) { - if (passwordSupported) { - supportedCredTypes.add(UserCredentialModel.PASSWORD); - } - } + } - return supportedCredTypes; + @Override + public boolean supportsCredentialType(String credentialType) { + return credentialType.equals(CredentialModel.KERBEROS) || (kerberosConfig.isAllowPasswordAuthentication() && credentialType.equals(CredentialModel.PASSWORD)); } @Override - public Set getSupportedCredentialTypes() { - Set supportedCredTypes = new HashSet(); - supportedCredTypes.add(UserCredentialModel.KERBEROS); - return supportedCredTypes; + public boolean isConfiguredFor(RealmModel realm, UserModel user, String credentialType) { + return supportsCredentialType(credentialType); } @Override - public boolean validCredentials(RealmModel realm, UserModel user, List input) { - for (UserCredentialModel cred : input) { - if (cred.getType().equals(UserCredentialModel.PASSWORD)) { - return validPassword(user.getUsername(), cred.getValue()); - } else { - return false; // invalid cred type - } + public boolean isValid(RealmModel realm, UserModel user, CredentialInput input) { + if (!(input instanceof UserCredentialModel)) return false; + if (input.getType().equals(UserCredentialModel.PASSWORD) && !session.userCredentialManager().isConfiguredLocally(realm, user, UserCredentialModel.PASSWORD)) { + return validPassword(user.getUsername(), ((UserCredentialModel)input).getValue()); + } else { + return false; // invalid cred type } - return true; } protected boolean validPassword(String username, String password) { @@ -195,11 +196,6 @@ protected boolean validPassword(String username, String password) { } } - @Override - public boolean validCredentials(RealmModel realm, UserModel user, UserCredentialModel... input) { - return validCredentials(realm, user, Arrays.asList(input)); - } - @Override public CredentialValidationOutput validCredentials(RealmModel realm, UserCredentialModel credential) { if (credential.getType().equals(UserCredentialModel.KERBEROS)) { diff --git a/federation/kerberos/src/main/java/org/keycloak/federation/kerberos/ReadOnlyKerberosUserModelDelegate.java b/federation/kerberos/src/main/java/org/keycloak/federation/kerberos/ReadOnlyKerberosUserModelDelegate.java index 4cc4824b375a..2a4e3b161f91 100644 --- a/federation/kerberos/src/main/java/org/keycloak/federation/kerberos/ReadOnlyKerberosUserModelDelegate.java +++ b/federation/kerberos/src/main/java/org/keycloak/federation/kerberos/ReadOnlyKerberosUserModelDelegate.java @@ -34,12 +34,4 @@ public ReadOnlyKerberosUserModelDelegate(UserModel delegate, KerberosFederationP this.provider = provider; } - @Override - public void updateCredential(UserCredentialModel cred) { - if (provider.getSupportedCredentialTypes(delegate).contains(cred.getType())) { - throw new ModelReadOnlyException("Can't change password in Keycloak database. Change password with your Kerberos server"); - } - - delegate.updateCredential(cred); - } } diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/LDAPFederationProvider.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/LDAPFederationProvider.java index be103c3f2b13..d16deaf5b6e5 100755 --- a/federation/ldap/src/main/java/org/keycloak/federation/ldap/LDAPFederationProvider.java +++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/LDAPFederationProvider.java @@ -18,6 +18,9 @@ package org.keycloak.federation.ldap; import org.jboss.logging.Logger; +import org.keycloak.credential.CredentialInput; +import org.keycloak.credential.CredentialInputUpdater; +import org.keycloak.credential.CredentialModel; import org.keycloak.federation.kerberos.impl.KerberosUsernamePasswordAuthenticator; import org.keycloak.federation.kerberos.impl.SPNEGOAuthenticator; import org.keycloak.federation.ldap.idm.model.LDAPObject; @@ -28,12 +31,14 @@ import org.keycloak.federation.ldap.kerberos.LDAPProviderKerberosConfig; import org.keycloak.federation.ldap.mappers.LDAPFederationMapper; import org.keycloak.federation.ldap.mappers.LDAPMappersComparator; +import org.keycloak.federation.ldap.mappers.PasswordUpdated; import org.keycloak.models.CredentialValidationOutput; import org.keycloak.models.GroupModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.LDAPConstants; import org.keycloak.models.ModelDuplicateException; import org.keycloak.models.ModelException; +import org.keycloak.models.ModelReadOnlyException; import org.keycloak.models.RealmModel; import org.keycloak.models.RoleModel; import org.keycloak.models.UserCredentialModel; @@ -73,6 +78,7 @@ public class LDAPFederationProvider implements UserFederationProvider { protected LDAPIdentityStore ldapIdentityStore; protected EditMode editMode; protected LDAPProviderKerberosConfig kerberosConfig; + protected PasswordUpdated updater; protected final Set supportedCredentialTypes = new HashSet<>(); @@ -90,6 +96,10 @@ public LDAPFederationProvider(LDAPFederationProviderFactory factory, KeycloakSes } } + public void setUpdater(PasswordUpdated updater) { + this.updater = updater; + } + public KeycloakSession getSession() { return session; } @@ -139,20 +149,6 @@ protected UserModel proxy(RealmModel realm, UserModel local, LDAPObject ldapObje return proxied; } - @Override - public Set getSupportedCredentialTypes(UserModel local) { - Set supportedCredentialTypes = new HashSet(this.supportedCredentialTypes); - if (editMode == EditMode.UNSYNCED ) { - for (UserCredentialValueModel cred : local.getCredentialsDirectly()) { - if (cred.getType().equals(UserCredentialModel.PASSWORD)) { - // User has changed password in KC local database. Use KC password instead of LDAP password - supportedCredentialTypes.remove(UserCredentialModel.PASSWORD); - } - } - } - return supportedCredentialTypes; - } - @Override public Set getSupportedCredentialTypes() { return new HashSet(this.supportedCredentialTypes); @@ -409,20 +405,47 @@ public boolean validPassword(RealmModel realm, UserModel user, String password) @Override - public boolean validCredentials(RealmModel realm, UserModel user, List input) { - for (UserCredentialModel cred : input) { - if (cred.getType().equals(UserCredentialModel.PASSWORD)) { - return validPassword(realm, user, cred.getValue()); - } else { - return false; // invalid cred type - } + public boolean updateCredential(RealmModel realm, UserModel user, CredentialInput input) { + if (!CredentialModel.PASSWORD.equals(input.getType()) || ! (input instanceof UserCredentialModel)) return false; + if (editMode == EditMode.READ_ONLY) { + throw new ModelReadOnlyException("Federated storage is not writable"); + + } else if (editMode == EditMode.WRITABLE) { + LDAPIdentityStore ldapIdentityStore = getLdapIdentityStore(); + UserCredentialModel cred = (UserCredentialModel)input; + String password = cred.getValue(); + LDAPObject ldapUser = loadAndValidateUser(realm, user); + ldapIdentityStore.updatePassword(ldapUser, password); + if (updater != null) updater.passwordUpdated(user, ldapUser, input); + return true; + } else { + return false; } - return true; } @Override - public boolean validCredentials(RealmModel realm, UserModel user, UserCredentialModel... input) { - return validCredentials(realm, user, Arrays.asList(input)); + public void disableCredentialType(RealmModel realm, UserModel user, String credentialType) { + + } + + @Override + public boolean supportsCredentialType(String credentialType) { + return getSupportedCredentialTypes().contains(credentialType); + } + + @Override + public boolean isConfiguredFor(RealmModel realm, UserModel user, String credentialType) { + return getSupportedCredentialTypes().contains(credentialType); + } + + @Override + public boolean isValid(RealmModel realm, UserModel user, CredentialInput input) { + if (!(input instanceof UserCredentialModel)) return false; + if (input.getType().equals(UserCredentialModel.PASSWORD) && !session.userCredentialManager().isConfiguredLocally(realm, user, UserCredentialModel.PASSWORD)) { + return validPassword(realm, user, ((UserCredentialModel)input).getValue()); + } else { + return false; // invalid cred type + } } @Override diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/ReadonlyLDAPUserModelDelegate.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/ReadonlyLDAPUserModelDelegate.java index bfe3bcc59714..c71b26aef66b 100755 --- a/federation/ldap/src/main/java/org/keycloak/federation/ldap/ReadonlyLDAPUserModelDelegate.java +++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/ReadonlyLDAPUserModelDelegate.java @@ -50,14 +50,6 @@ public void setFirstName(String first) { throw new ModelReadOnlyException("Federated storage is not writable"); } - @Override - public void updateCredential(UserCredentialModel cred) { - if (provider.getSupportedCredentialTypes(delegate).contains(cred.getType())) { - throw new ModelReadOnlyException("Federated storage is not writable"); - } - delegate.updateCredential(cred); - } - @Override public void setEmail(String email) { throw new ModelReadOnlyException("Federated storage is not writable"); diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/WritableLDAPUserModelDelegate.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/WritableLDAPUserModelDelegate.java index 5a235f5fdeb1..4644cd9f9b59 100755 --- a/federation/ldap/src/main/java/org/keycloak/federation/ldap/WritableLDAPUserModelDelegate.java +++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/WritableLDAPUserModelDelegate.java @@ -40,20 +40,4 @@ public WritableLDAPUserModelDelegate(UserModel delegate, LDAPFederationProvider this.ldapObject = ldapObject; } - @Override - public void updateCredential(UserCredentialModel cred) { - if (!provider.getSupportedCredentialTypes(delegate).contains(cred.getType())) { - delegate.updateCredential(cred); - return; - } - - if (cred.getType().equals(UserCredentialModel.PASSWORD)) { - LDAPIdentityStore ldapIdentityStore = provider.getLdapIdentityStore(); - String password = cred.getValue(); - ldapIdentityStore.updatePassword(ldapObject, password); - } else { - logger.warnf("Don't know how to update credential of type [%s] for user [%s]", cred.getType(), delegate.getUsername()); - } - } - } diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/PasswordUpdated.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/PasswordUpdated.java new file mode 100644 index 000000000000..1ec5ad8e3c85 --- /dev/null +++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/PasswordUpdated.java @@ -0,0 +1,29 @@ +/* + * Copyright 2016 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.federation.ldap.mappers; + +import org.keycloak.credential.CredentialInput; +import org.keycloak.federation.ldap.idm.model.LDAPObject; +import org.keycloak.models.UserModel; + +/** + * @author Bill Burke + * @version $Revision: 1 $ + */ +public interface PasswordUpdated { + void passwordUpdated(UserModel user, LDAPObject ldapUser, CredentialInput input); +} diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/msad/MSADUserAccountControlMapper.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/msad/MSADUserAccountControlMapper.java index 229ac87dd6ff..ba6139d23b38 100644 --- a/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/msad/MSADUserAccountControlMapper.java +++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/msad/MSADUserAccountControlMapper.java @@ -25,10 +25,12 @@ import javax.naming.AuthenticationException; import org.jboss.logging.Logger; +import org.keycloak.credential.CredentialInput; import org.keycloak.federation.ldap.LDAPFederationProvider; import org.keycloak.federation.ldap.idm.model.LDAPObject; import org.keycloak.federation.ldap.idm.query.internal.LDAPQuery; import org.keycloak.federation.ldap.mappers.AbstractLDAPFederationMapper; +import org.keycloak.federation.ldap.mappers.PasswordUpdated; import org.keycloak.models.LDAPConstants; import org.keycloak.models.ModelException; import org.keycloak.models.RealmModel; @@ -44,7 +46,7 @@ * * @author Marek Posolda */ -public class MSADUserAccountControlMapper extends AbstractLDAPFederationMapper { +public class MSADUserAccountControlMapper extends AbstractLDAPFederationMapper implements PasswordUpdated { private static final Logger logger = Logger.getLogger(MSADUserAccountControlMapper.class); @@ -53,6 +55,7 @@ public class MSADUserAccountControlMapper extends AbstractLDAPFederationMapper { public MSADUserAccountControlMapper(UserFederationMapperModel mapperModel, LDAPFederationProvider ldapProvider, RealmModel realm) { super(mapperModel, ldapProvider, realm); + ldapProvider.setUpdater(this); } @Override @@ -68,6 +71,26 @@ public void beforeLDAPQuery(LDAPQuery query) { } } + @Override + public void passwordUpdated(UserModel user, LDAPObject ldapUser, CredentialInput input) { + logger.debugf("Going to update userAccountControl for ldap user '%s' after successful password update", ldapUser.getDn().toString()); + + // Normally it's read-only + ldapUser.removeReadOnlyAttributeName(LDAPConstants.PWD_LAST_SET); + + ldapUser.setSingleAttribute(LDAPConstants.PWD_LAST_SET, "-1"); + + UserAccountControl control = getUserAccountControl(ldapUser); + control.remove(UserAccountControl.PASSWD_NOTREQD); + control.remove(UserAccountControl.PASSWORD_EXPIRED); + + if (user.isEnabled()) { + control.remove(UserAccountControl.ACCOUNTDISABLE); + } + + updateUserAccountControl(ldapUser, control); + } + @Override public UserModel proxy(LDAPObject ldapUser, UserModel delegate) { return new MSADUserModelDelegate(delegate, ldapUser); @@ -135,6 +158,22 @@ protected ModelException processFailedPasswordUpdateException(ModelException e) return e; } + protected UserAccountControl getUserAccountControl(LDAPObject ldapUser) { + String userAccountControl = ldapUser.getAttributeAsString(LDAPConstants.USER_ACCOUNT_CONTROL); + long longValue = userAccountControl == null ? 0 : Long.parseLong(userAccountControl); + return new UserAccountControl(longValue); + } + + // Update user in LDAP + protected void updateUserAccountControl(LDAPObject ldapUser, UserAccountControl accountControl) { + String userAccountControlValue = String.valueOf(accountControl.getValue()); + logger.debugf("Updating userAccountControl of user '%s' to value '%s'", ldapUser.getDn().toString(), userAccountControlValue); + + ldapUser.setSingleAttribute(LDAPConstants.USER_ACCOUNT_CONTROL, userAccountControlValue); + ldapProvider.getLdapIdentityStore().update(ldapUser); + } + + public class MSADUserModelDelegate extends UserModelDelegate { @@ -151,7 +190,7 @@ public boolean isEnabled() { if (getPwdLastSet() > 0) { // Merge KC and MSAD - return kcEnabled && !getUserAccountControl().has(UserAccountControl.ACCOUNTDISABLE); + return kcEnabled && !getUserAccountControl(ldapUser).has(UserAccountControl.ACCOUNTDISABLE); } else { // If new MSAD user is created and pwdLastSet is still 0, MSAD account is in disabled state. So read just from Keycloak DB. User is not able to login via MSAD anyway return kcEnabled; @@ -166,44 +205,14 @@ public void setEnabled(boolean enabled) { if (ldapProvider.getEditMode() == UserFederationProvider.EditMode.WRITABLE && getPwdLastSet() > 0) { logger.debugf("Going to propagate enabled=%s for ldapUser '%s' to MSAD", enabled, ldapUser.getDn().toString()); - UserAccountControl control = getUserAccountControl(); + UserAccountControl control = getUserAccountControl(ldapUser); if (enabled) { control.remove(UserAccountControl.ACCOUNTDISABLE); } else { control.add(UserAccountControl.ACCOUNTDISABLE); } - updateUserAccountControl(control); - } - } - - @Override - public void updateCredential(UserCredentialModel cred) { - // Update LDAP password first - try { - super.updateCredential(cred); - } catch (ModelException me) { - me = processFailedPasswordUpdateException(me); - throw me; - } - - if (ldapProvider.getEditMode() == UserFederationProvider.EditMode.WRITABLE && cred.getType().equals(UserCredentialModel.PASSWORD)) { - logger.debugf("Going to update userAccountControl for ldap user '%s' after successful password update", ldapUser.getDn().toString()); - - // Normally it's read-only - ldapUser.removeReadOnlyAttributeName(LDAPConstants.PWD_LAST_SET); - - ldapUser.setSingleAttribute(LDAPConstants.PWD_LAST_SET, "-1"); - - UserAccountControl control = getUserAccountControl(); - control.remove(UserAccountControl.PASSWD_NOTREQD); - control.remove(UserAccountControl.PASSWORD_EXPIRED); - - if (super.isEnabled()) { - control.remove(UserAccountControl.ACCOUNTDISABLE); - } - - updateUserAccountControl(control); + updateUserAccountControl(ldapUser, control); } } @@ -243,7 +252,7 @@ public void removeRequiredAction(String action) { if (ldapProvider.getEditMode() == UserFederationProvider.EditMode.WRITABLE && RequiredAction.UPDATE_PASSWORD.toString().equals(action)) { // Don't set pwdLastSet in MSAD when it is new user - UserAccountControl accountControl = getUserAccountControl(); + UserAccountControl accountControl = getUserAccountControl(ldapUser); if (accountControl.getValue() != 0 && !accountControl.has(UserAccountControl.PASSWD_NOTREQD)) { logger.debugf("Going to remove required action UPDATE_PASSWORD from MSAD for ldap user '%s' ", ldapUser.getDn().toString()); @@ -261,7 +270,7 @@ public Set getRequiredActions() { Set requiredActions = super.getRequiredActions(); if (ldapProvider.getEditMode() == UserFederationProvider.EditMode.WRITABLE) { - if (getPwdLastSet() == 0 || getUserAccountControl().has(UserAccountControl.PASSWORD_EXPIRED)) { + if (getPwdLastSet() == 0 || getUserAccountControl(ldapUser).has(UserAccountControl.PASSWORD_EXPIRED)) { requiredActions = new HashSet<>(requiredActions); requiredActions.add(RequiredAction.UPDATE_PASSWORD.toString()); return requiredActions; @@ -276,20 +285,7 @@ protected long getPwdLastSet() { return pwdLastSet == null ? 0 : Long.parseLong(pwdLastSet); } - protected UserAccountControl getUserAccountControl() { - String userAccountControl = ldapUser.getAttributeAsString(LDAPConstants.USER_ACCOUNT_CONTROL); - long longValue = userAccountControl == null ? 0 : Long.parseLong(userAccountControl); - return new UserAccountControl(longValue); - } - - // Update user in LDAP - protected void updateUserAccountControl(UserAccountControl accountControl) { - String userAccountControlValue = String.valueOf(accountControl.getValue()); - logger.debugf("Updating userAccountControl of user '%s' to value '%s'", ldapUser.getDn().toString(), userAccountControlValue); - ldapUser.setSingleAttribute(LDAPConstants.USER_ACCOUNT_CONTROL, userAccountControlValue); - ldapProvider.getLdapIdentityStore().update(ldapUser); - } } } diff --git a/federation/sssd/src/main/java/org/keycloak/federation/sssd/ReadonlySSSDUserModelDelegate.java b/federation/sssd/src/main/java/org/keycloak/federation/sssd/ReadonlySSSDUserModelDelegate.java index 52061c9d185e..75b6fd8f569f 100755 --- a/federation/sssd/src/main/java/org/keycloak/federation/sssd/ReadonlySSSDUserModelDelegate.java +++ b/federation/sssd/src/main/java/org/keycloak/federation/sssd/ReadonlySSSDUserModelDelegate.java @@ -54,22 +54,6 @@ public void setFirstName(String first) { throw new ModelReadOnlyException("Federated storage is not writable"); } - @Override - public void updateCredentialDirectly(UserCredentialValueModel cred) { - if (cred.getType().equals(UserCredentialModel.PASSWORD)) { - throw new IllegalStateException("Federated storage is not writable"); - } - super.updateCredentialDirectly(cred); - } - - @Override - public void updateCredential(UserCredentialModel cred) { - if (provider.getSupportedCredentialTypes(delegate).contains(cred.getType())) { - throw new ModelReadOnlyException("Federated storage is not writable"); - } - delegate.updateCredential(cred); - } - @Override public void setEmail(String email) { throw new ModelReadOnlyException("Federated storage is not writable"); diff --git a/federation/sssd/src/main/java/org/keycloak/federation/sssd/SSSDFederationProvider.java b/federation/sssd/src/main/java/org/keycloak/federation/sssd/SSSDFederationProvider.java index a9089ec0b187..ba47871b5192 100755 --- a/federation/sssd/src/main/java/org/keycloak/federation/sssd/SSSDFederationProvider.java +++ b/federation/sssd/src/main/java/org/keycloak/federation/sssd/SSSDFederationProvider.java @@ -19,11 +19,14 @@ import org.freedesktop.dbus.Variant; import org.jboss.logging.Logger; +import org.keycloak.credential.CredentialInput; +import org.keycloak.credential.CredentialModel; import org.keycloak.federation.sssd.api.Sssd; import org.keycloak.federation.sssd.impl.PAMAuthenticator; import org.keycloak.models.CredentialValidationOutput; import org.keycloak.models.GroupModel; import org.keycloak.models.KeycloakSession; +import org.keycloak.models.ModelReadOnlyException; import org.keycloak.models.RealmModel; import org.keycloak.models.RoleModel; import org.keycloak.models.UserCredentialModel; @@ -163,29 +166,38 @@ public boolean isValid(RealmModel realm, UserModel local) { } @Override - public Set getSupportedCredentialTypes(UserModel user) { + public Set getSupportedCredentialTypes() { return supportedCredentialTypes; } @Override - public Set getSupportedCredentialTypes() { - return supportedCredentialTypes; + public boolean updateCredential(RealmModel realm, UserModel user, CredentialInput input) { + if (!(input instanceof UserCredentialModel) || !CredentialModel.PASSWORD.equals(input.getType())) return false; + throw new ModelReadOnlyException("Federated storage is not writable"); } @Override - public boolean validCredentials(RealmModel realm, UserModel user, List input) { - for (UserCredentialModel cred : input) { - if (cred.getType().equals(UserCredentialModel.PASSWORD)) { - PAMAuthenticator pam = factory.createPAMAuthenticator(user.getUsername(), cred.getValue()); - return (pam.authenticate() != null); - } - } - return false; + public void disableCredentialType(RealmModel realm, UserModel user, String credentialType) { + } @Override - public boolean validCredentials(RealmModel realm, UserModel user, UserCredentialModel... input) { - return validCredentials(realm, user, Arrays.asList(input)); + public boolean supportsCredentialType(String credentialType) { + return CredentialModel.PASSWORD.equals(credentialType); + } + + @Override + public boolean isConfiguredFor(RealmModel realm, UserModel user, String credentialType) { + return CredentialModel.PASSWORD.equals(credentialType); + } + + @Override + public boolean isValid(RealmModel realm, UserModel user, CredentialInput input) { + if (!supportsCredentialType(input.getType()) || !(input instanceof UserCredentialModel)) return false; + + UserCredentialModel cred = (UserCredentialModel)input; + PAMAuthenticator pam = factory.createPAMAuthenticator(user.getUsername(), cred.getValue()); + return (pam.authenticate() != null); } @Override diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UserAdapter.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UserAdapter.java index ea6698cf8346..0ef768917fc6 100755 --- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UserAdapter.java +++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UserAdapter.java @@ -253,24 +253,6 @@ public void setOtpEnabled(boolean totp) { updated.setOtpEnabled(totp); } - @Override - public void updateCredential(UserCredentialModel cred) { - getDelegateForUpdate(); - updated.updateCredential(cred); - } - - @Override - public List getCredentialsDirectly() { - if (updated != null) return updated.getCredentialsDirectly(); - return cached.getCredentials(); - } - - @Override - public void updateCredentialDirectly(UserCredentialValueModel cred) { - getDelegateForUpdate(); - updated.updateCredentialDirectly(cred); - } - @Override public String getFederationLink() { if (updated != null) return updated.getFederationLink(); diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UserCacheSession.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UserCacheSession.java index b38d9aab1b03..9143052ef5e9 100755 --- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UserCacheSession.java +++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UserCacheSession.java @@ -627,16 +627,6 @@ public boolean removeFederatedIdentity(RealmModel realm, UserModel user, String return getDelegate().removeFederatedIdentity(realm, user, socialProvider); } - @Override - public boolean validCredentials(KeycloakSession session, RealmModel realm, UserModel user, List input) { - return getDelegate().validCredentials(session, realm, user, input); - } - - @Override - public boolean validCredentials(KeycloakSession session, RealmModel realm, UserModel user, UserCredentialModel... input) { - return getDelegate().validCredentials(session, realm, user, input); - } - @Override public CredentialValidationOutput validCredentials(KeycloakSession session, RealmModel realm, UserCredentialModel... input) { return getDelegate().validCredentials(session, realm, input); diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/entities/CachedUser.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/entities/CachedUser.java index 7c24594885dd..2eec04a37ab5 100755 --- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/entities/CachedUser.java +++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/entities/CachedUser.java @@ -44,7 +44,6 @@ public class CachedUser extends AbstractExtendableRevisioned implements InRealm private String lastName; private String email; private boolean emailVerified; - private List credentials = new LinkedList<>(); private boolean enabled; private boolean totp; private String federationLink; @@ -66,7 +65,6 @@ public CachedUser(Long revision, RealmModel realm, UserModel user) { this.attributes.putAll(user.getAttributes()); this.email = user.getEmail(); this.emailVerified = user.isEmailVerified(); - this.credentials.addAll(user.getCredentialsDirectly()); this.enabled = user.isEnabled(); this.totp = user.isOtpEnabled(); this.federationLink = user.getFederationLink(); @@ -111,10 +109,6 @@ public boolean isEmailVerified() { return emailVerified; } - public List getCredentials() { - return credentials; - } - public boolean isEnabled() { return enabled; } diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/JpaUserProvider.java b/model/jpa/src/main/java/org/keycloak/models/jpa/JpaUserProvider.java index d44c6fe171d1..51b41530d106 100755 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/JpaUserProvider.java +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/JpaUserProvider.java @@ -710,16 +710,6 @@ public FederatedIdentityModel getFederatedIdentity(UserModel user, String identi return (entity != null) ? new FederatedIdentityModel(entity.getIdentityProvider(), entity.getUserId(), entity.getUserName(), entity.getToken()) : null; } - @Override - public boolean validCredentials(KeycloakSession session, RealmModel realm, UserModel user, List input) { - return CredentialValidation.validCredentials(session, realm, user, input); - } - - @Override - public boolean validCredentials(KeycloakSession session, RealmModel realm, UserModel user, UserCredentialModel... input) { - return CredentialValidation.validCredentials(session, realm, user, input); - } - @Override public CredentialValidationOutput validCredentials(KeycloakSession session, RealmModel realm, UserCredentialModel... input) { // Not supported yet @@ -803,7 +793,7 @@ public CredentialModel createCredential(RealmModel realm, UserModel user, Creden entity.setUser(userRef); em.persist(entity); MultivaluedHashMap config = cred.getConfig(); - if (config != null || !config.isEmpty()) { + if (config != null && !config.isEmpty()) { for (String key : config.keySet()) { List values = config.getList(key); @@ -850,6 +840,7 @@ protected CredentialModel toModel(CredentialEntity entity) { model.setCreatedDate(entity.getCreatedDate()); model.setDevice(entity.getDevice()); model.setDigits(entity.getDigits()); + model.setHashIterations(entity.getHashIterations()); MultivaluedHashMap config = new MultivaluedHashMap<>(); model.setConfig(config); for (CredentialAttributeEntity attr : entity.getCredentialAttributes()) { diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/UserAdapter.java b/model/jpa/src/main/java/org/keycloak/models/jpa/UserAdapter.java index 49bcac633b3c..5d4bc911c5cf 100755 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/UserAdapter.java +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/UserAdapter.java @@ -315,199 +315,6 @@ public void setOtpEnabled(boolean totp) { user.setTotp(totp); } - @Override - public void updateCredential(UserCredentialModel cred) { - - if (cred.getType().equals(UserCredentialModel.PASSWORD)) { - updatePasswordCredential(cred); - } else if (UserCredentialModel.isOtp(cred.getType())){ - updateOtpCredential(cred); - - } else { - CredentialEntity credentialEntity = getCredentialEntity(user, cred.getType()); - - if (credentialEntity == null) { - credentialEntity = setCredentials(user, cred); - credentialEntity.setValue(cred.getValue()); - em.persist(credentialEntity); - user.getCredentials().add(credentialEntity); - } else { - credentialEntity.setValue(cred.getValue()); - } - } - em.flush(); - } - - private void updateOtpCredential(UserCredentialModel cred) { - CredentialEntity credentialEntity = getCredentialEntity(user, cred.getType()); - - if (credentialEntity == null) { - credentialEntity = setCredentials(user, cred); - - credentialEntity.setValue(cred.getValue()); - OTPPolicy otpPolicy = realm.getOTPPolicy(); - credentialEntity.setAlgorithm(otpPolicy.getAlgorithm()); - credentialEntity.setDigits(otpPolicy.getDigits()); - credentialEntity.setCounter(otpPolicy.getInitialCounter()); - credentialEntity.setPeriod(otpPolicy.getPeriod()); - em.persist(credentialEntity); - user.getCredentials().add(credentialEntity); - } else { - OTPPolicy policy = realm.getOTPPolicy(); - credentialEntity.setDigits(policy.getDigits()); - credentialEntity.setCounter(policy.getInitialCounter()); - credentialEntity.setAlgorithm(policy.getAlgorithm()); - credentialEntity.setValue(cred.getValue()); - credentialEntity.setPeriod(policy.getPeriod()); - } - } - - - - - private void updatePasswordCredential(UserCredentialModel cred) { - CredentialEntity credentialEntity = getCredentialEntity(user, cred.getType()); - - if (credentialEntity == null) { - credentialEntity = setCredentials(user, cred); - setValue(credentialEntity, cred); - em.persist(credentialEntity); - user.getCredentials().add(credentialEntity); - } else { - - int expiredPasswordsPolicyValue = -1; - PasswordPolicy policy = realm.getPasswordPolicy(); - if(policy != null) { - expiredPasswordsPolicyValue = policy.getExpiredPasswords(); - } - - if (expiredPasswordsPolicyValue != -1) { - user.getCredentials().remove(credentialEntity); - credentialEntity.setType(UserCredentialModel.PASSWORD_HISTORY); - user.getCredentials().add(credentialEntity); - - List credentialEntities = getCredentialEntities(user, UserCredentialModel.PASSWORD_HISTORY); - if (credentialEntities.size() > expiredPasswordsPolicyValue - 1) { - user.getCredentials().removeAll(credentialEntities.subList(expiredPasswordsPolicyValue - 1, credentialEntities.size())); - } - - credentialEntity = setCredentials(user, cred); - setValue(credentialEntity, cred); - em.persist(credentialEntity); - user.getCredentials().add(credentialEntity); - } else { - List credentialEntities = getCredentialEntities(user, UserCredentialModel.PASSWORD_HISTORY); - if (credentialEntities != null && credentialEntities.size() > 0) { - user.getCredentials().removeAll(credentialEntities); - } - setValue(credentialEntity, cred); - } - } - } - - private CredentialEntity setCredentials(UserEntity user, UserCredentialModel cred) { - CredentialEntity credentialEntity = new CredentialEntity(); - credentialEntity.setId(KeycloakModelUtils.generateId()); - credentialEntity.setType(cred.getType()); - credentialEntity.setDevice(cred.getDevice()); - credentialEntity.setUser(user); - return credentialEntity; - } - - private void setValue(CredentialEntity credentialEntity, UserCredentialModel cred) { - UserCredentialValueModel encoded = PasswordHashManager.encode(session, realm, cred.getValue()); - credentialEntity.setCreatedDate(Time.toMillis(Time.currentTime())); - credentialEntity.setAlgorithm(encoded.getAlgorithm()); - credentialEntity.setValue(encoded.getValue()); - credentialEntity.setSalt(encoded.getSalt()); - credentialEntity.setHashIterations(encoded.getHashIterations()); - } - - private CredentialEntity getCredentialEntity(UserEntity userEntity, String credType) { - for (CredentialEntity entity : userEntity.getCredentials()) { - if (entity.getType().equals(credType)) { - return entity; - } - } - - return null; - } - - private List getCredentialEntities(UserEntity userEntity, String credType) { - List credentialEntities = new ArrayList(); - for (CredentialEntity entity : userEntity.getCredentials()) { - if (entity.getType().equals(credType)) { - credentialEntities.add(entity); - } - } - - // Avoiding direct use of credSecond.getCreatedDate() - credFirst.getCreatedDate() to prevent Integer Overflow - // Orders from most recent to least recent - Collections.sort(credentialEntities, new Comparator() { - public int compare(CredentialEntity credFirst, CredentialEntity credSecond) { - if (credFirst.getCreatedDate() > credSecond.getCreatedDate()) { - return -1; - } else if (credFirst.getCreatedDate() < credSecond.getCreatedDate()) { - return 1; - } else { - return 0; - } - } - }); - return credentialEntities; - } - - @Override - public List getCredentialsDirectly() { - List credentials = new ArrayList<>(user.getCredentials()); - List result = new ArrayList<>(); - - for (CredentialEntity credEntity : credentials) { - UserCredentialValueModel credModel = new UserCredentialValueModel(); - credModel.setType(credEntity.getType()); - credModel.setDevice(credEntity.getDevice()); - credModel.setValue(credEntity.getValue()); - credModel.setCreatedDate(credEntity.getCreatedDate()); - credModel.setSalt(credEntity.getSalt()); - credModel.setHashIterations(credEntity.getHashIterations()); - credModel.setCounter(credEntity.getCounter()); - credModel.setAlgorithm(credEntity.getAlgorithm()); - credModel.setDigits(credEntity.getDigits()); - credModel.setPeriod(credEntity.getPeriod()); - - result.add(credModel); - } - - return result; - } - - @Override - public void updateCredentialDirectly(UserCredentialValueModel credModel) { - CredentialEntity credentialEntity = getCredentialEntity(user, credModel.getType()); - - if (credentialEntity == null) { - credentialEntity = new CredentialEntity(); - credentialEntity.setId(KeycloakModelUtils.generateId()); - credentialEntity.setType(credModel.getType()); - credentialEntity.setCreatedDate(credModel.getCreatedDate()); - credentialEntity.setUser(user); - em.persist(credentialEntity); - user.getCredentials().add(credentialEntity); - } - - credentialEntity.setValue(credModel.getValue()); - credentialEntity.setSalt(credModel.getSalt()); - credentialEntity.setDevice(credModel.getDevice()); - credentialEntity.setHashIterations(credModel.getHashIterations()); - credentialEntity.setCounter(credModel.getCounter()); - credentialEntity.setAlgorithm(credModel.getAlgorithm()); - credentialEntity.setDigits(credModel.getDigits()); - credentialEntity.setPeriod(credModel.getPeriod()); - - em.flush(); - } - - @Override public Set getGroups() { // we query ids only as the group might be cached and following the @ManyToOne will result in a load diff --git a/model/jpa/src/main/java/org/keycloak/storage/jpa/JpaUserFederatedStorageProvider.java b/model/jpa/src/main/java/org/keycloak/storage/jpa/JpaUserFederatedStorageProvider.java index 1c72da074467..090674b4b226 100644 --- a/model/jpa/src/main/java/org/keycloak/storage/jpa/JpaUserFederatedStorageProvider.java +++ b/model/jpa/src/main/java/org/keycloak/storage/jpa/JpaUserFederatedStorageProvider.java @@ -47,6 +47,7 @@ import org.keycloak.storage.federated.UserRequiredActionsFederatedStorage; import org.keycloak.storage.federated.UserRoleMappingsFederatedStorage; import org.keycloak.storage.jpa.entity.BrokerLinkEntity; +import org.keycloak.storage.jpa.entity.FederatedUser; import org.keycloak.storage.jpa.entity.FederatedUserAttributeEntity; import org.keycloak.storage.jpa.entity.FederatedUserConsentEntity; import org.keycloak.storage.jpa.entity.FederatedUserConsentProtocolMapperEntity; @@ -95,9 +96,24 @@ public void close() { } + /** + * We create an entry so that its easy to iterate over all things in the database. Specifically useful for export + * + */ + protected void createIndex(RealmModel realm, UserModel user) { + if (em.find(FederatedUser.class, user.getId()) == null) { + FederatedUser fedUser = new FederatedUser(); + fedUser.setId(user.getId()); + fedUser.setRealmId(realm.getId()); + fedUser.setStorageProviderId(StorageId.resolveProviderId(user)); + em.persist(fedUser); + } + } + @Override public void setAttribute(RealmModel realm, UserModel user, String name, List values) { + createIndex(realm, user); deleteAttribute(realm, user, name); em.flush(); for (String value : values) { @@ -126,6 +142,7 @@ private void persistAttributeValue(RealmModel realm, UserModel user, String name @Override public void setSingleAttribute(RealmModel realm, UserModel user, String name, String value) { + createIndex(realm, user); deleteAttribute(realm, user, name); em.flush(); persistAttributeValue(realm, user, name, value); @@ -133,6 +150,7 @@ public void setSingleAttribute(RealmModel realm, UserModel user, String name, St @Override public void removeAttribute(RealmModel realm, UserModel user, String name) { + // createIndex(realm, user); don't need to create an index for removal deleteAttribute(realm, user, name); em.flush(); } @@ -180,6 +198,7 @@ public String getUserByFederatedIdentity(FederatedIdentityModel link, RealmModel @Override public void addFederatedIdentity(RealmModel realm, UserModel user, FederatedIdentityModel link) { + createIndex(realm, user); BrokerLinkEntity entity = new BrokerLinkEntity(); entity.setRealmId(realm.getId()); entity.setUserId(user.getId()); @@ -211,6 +230,7 @@ private BrokerLinkEntity getBrokerLinkEntity(RealmModel realm, UserModel user, S @Override public void updateFederatedIdentity(RealmModel realm, UserModel user, FederatedIdentityModel model) { + createIndex(realm, user); BrokerLinkEntity entity = getBrokerLinkEntity(realm, user, model.getIdentityProvider()); if (entity == null) return; entity.setBrokerUserName(model.getUserName()); @@ -243,6 +263,7 @@ public FederatedIdentityModel getFederatedIdentity(UserModel user, String social @Override public void addConsent(RealmModel realm, UserModel user, UserConsentModel consent) { + createIndex(realm, user); String clientId = consent.getClient().getId(); FederatedUserConsentEntity consentEntity = getGrantedConsentEntity(user, clientId); @@ -285,6 +306,7 @@ public List getConsents(RealmModel realm, UserModel user) { @Override public void updateConsent(RealmModel realm, UserModel user, UserConsentModel consent) { + createIndex(realm, user); String clientId = consent.getClient().getId(); FederatedUserConsentEntity consentEntity = getGrantedConsentEntity(user, clientId); @@ -432,12 +454,14 @@ public List getCredentials(RealmModel realm, UserModel @Override public void updateCredential(RealmModel realm, UserModel user, UserCredentialModel cred) { + createIndex(realm, user); FederatedCredentials.updateCredential(session, this, realm, user, cred); } @Override public void updateCredential(RealmModel realm, UserModel user, UserCredentialValueModel cred) { + createIndex(realm, user); FederatedUserCredentialEntity entity = null; if (cred.getId() != null) entity = em.find(FederatedUserCredentialEntity.class, cred.getId()); boolean newEntity = false; @@ -490,6 +514,7 @@ public Set getGroups(RealmModel realm, UserModel user) { @Override public void joinGroup(RealmModel realm, UserModel user, GroupModel group) { if (isMemberOf(realm, user, group)) return; + createIndex(realm, user); FederatedUserGroupMembershipEntity entity = new FederatedUserGroupMembershipEntity(); entity.setUserId(user.getId()); entity.setStorageProviderId(StorageId.resolveProviderId(user)); @@ -553,6 +578,7 @@ private List getRequiredActionEntities(RealmM @Override public void addRequiredAction(RealmModel realm, UserModel user, String action) { + createIndex(realm, user); if (user.getRequiredActions().contains(action)) return; FederatedUserRequiredActionEntity entity = new FederatedUserRequiredActionEntity(); entity.setUserId(user.getId()); @@ -576,6 +602,7 @@ public void removeRequiredAction(RealmModel realm, UserModel user, String action @Override public void grantRole(RealmModel realm, UserModel user, RoleModel role) { if (user.hasRole(role)) return; + createIndex(realm, user); FederatedUserRoleMappingEntity entity = new FederatedUserRoleMappingEntity(); entity.setUserId(user.getId()); entity.setStorageProviderId(StorageId.resolveProviderId(user)); @@ -645,6 +672,7 @@ public CredentialModel getCredentialByNameAndType(RealmModel realm, UserModel us public void updateCredential(RealmModel realm, UserModel user, CredentialModel cred) { FederatedUserCredentialEntity entity = em.find(FederatedUserCredentialEntity.class, cred.getId()); if (entity == null) return; + createIndex(realm, user); entity.setAlgorithm(cred.getAlgorithm()); entity.setCounter(cred.getCounter()); entity.setCreatedDate(cred.getCreatedDate()); @@ -696,6 +724,7 @@ public void updateCredential(RealmModel realm, UserModel user, CredentialModel c @Override public CredentialModel createCredential(RealmModel realm, UserModel user, CredentialModel cred) { + createIndex(realm, user); FederatedUserCredentialEntity entity = new FederatedUserCredentialEntity(); String id = cred.getId() == null ? KeycloakModelUtils.generateId() : cred.getId(); entity.setId(id); @@ -714,7 +743,7 @@ public CredentialModel createCredential(RealmModel realm, UserModel user, Creden entity.setStorageProviderId(StorageId.resolveProviderId(user)); em.persist(entity); MultivaluedHashMap config = cred.getConfig(); - if (config != null || !config.isEmpty()) { + if (config != null && !config.isEmpty()) { for (String key : config.keySet()) { List values = config.getList(key); @@ -761,6 +790,7 @@ protected CredentialModel toModel(FederatedUserCredentialEntity entity) { model.setCreatedDate(entity.getCreatedDate()); model.setDevice(entity.getDevice()); model.setDigits(entity.getDigits()); + model.setHashIterations(entity.getHashIterations()); MultivaluedHashMap config = new MultivaluedHashMap<>(); model.setConfig(config); for (FederatedUserCredentialAttributeEntity attr : entity.getCredentialAttributes()) { @@ -805,6 +835,15 @@ public CredentialModel getStoredCredentialByNameAndType(RealmModel realm, UserMo return toModel(results.get(0)); } + @Override + public List getStoredUsers(RealmModel realm, int first, int max) { + TypedQuery query = em.createNamedQuery("getFederatedUserIds", String.class) + .setParameter("realmId", realm.getId()) + .setFirstResult(first) + .setMaxResults(max); + return query.getResultList(); + } + @Override public void preRemove(RealmModel realm) { int num = em.createNamedQuery("deleteFederatedUserConsentRolesByRealm") @@ -827,6 +866,8 @@ public void preRemove(RealmModel realm) { .setParameter("realmId", realm.getId()).executeUpdate(); num = em.createNamedQuery("deleteFederatedUserGroupMembershipByRealm") .setParameter("realmId", realm.getId()).executeUpdate(); + num = em.createNamedQuery("deleteFederatedUsersByRealm") + .setParameter("realmId", realm.getId()).executeUpdate(); } @Override @@ -855,6 +896,10 @@ public void preRemove(RealmModel realm, UserFederationProviderModel link) { .setParameter("realmId", realm.getId()) .setParameter("link", link.getId()) .executeUpdate(); + num = em.createNamedQuery("deleteFederatedUsersByRealmAndLink") + .setParameter("realmId", realm.getId()) + .setParameter("link", link.getId()) + .executeUpdate(); } @Override @@ -924,6 +969,10 @@ public void preRemove(RealmModel realm, UserModel user) { .setParameter("userId", user.getId()) .setParameter("realmId", realm.getId()) .executeUpdate(); + em.createNamedQuery("deleteFederatedUserByUser") + .setParameter("userId", user.getId()) + .setParameter("realmId", realm.getId()) + .executeUpdate(); } @@ -961,6 +1010,9 @@ public void preRemove(RealmModel realm, ComponentModel model) { em.createNamedQuery("deleteFederatedUserRoleMappingsByStorageProvider") .setParameter("storageProviderId", model.getId()) .executeUpdate(); + em.createNamedQuery("deleteFederatedUsersByStorageProvider") + .setParameter("storageProviderId", model.getId()) + .executeUpdate(); } } diff --git a/model/jpa/src/main/java/org/keycloak/storage/jpa/entity/FederatedUser.java b/model/jpa/src/main/java/org/keycloak/storage/jpa/entity/FederatedUser.java new file mode 100644 index 000000000000..c4ace99fedf7 --- /dev/null +++ b/model/jpa/src/main/java/org/keycloak/storage/jpa/entity/FederatedUser.java @@ -0,0 +1,85 @@ +/* + * Copyright 2016 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.storage.jpa.entity; + +import org.keycloak.models.jpa.entities.UserAttributeEntity; + +import javax.persistence.Access; +import javax.persistence.AccessType; +import javax.persistence.CascadeType; +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.Id; +import javax.persistence.NamedQueries; +import javax.persistence.NamedQuery; +import javax.persistence.OneToMany; +import javax.persistence.Table; +import java.util.ArrayList; +import java.util.Collection; +import java.util.LinkedList; + +/** + * @author Bill Burke + * @version $Revision: 1 $ + */ +@NamedQueries({ + @NamedQuery(name="getFederatedUserIds", query="select f.id from FederatedUser f where f.realmId=:realmId"), + @NamedQuery(name="deleteFederatedUserByUser", query="delete from FederatedUser f where f.id = :userId and f.realmId=:realmId"), + @NamedQuery(name="deleteFederatedUsersByRealm", query="delete from FederatedUser f where f.realmId=:realmId"), + @NamedQuery(name="deleteFederatedUsersByStorageProvider", query="delete from FederatedUser f where f.storageProviderId=:storageProviderId"), + @NamedQuery(name="deleteFederatedUsersByRealmAndLink", query="delete from FederatedUser f where f.id IN (select u.id from UserEntity u where u.realmId=:realmId and u.federationLink=:link)") +}) +@Entity +@Table(name="FEDERATED_USER") +public class FederatedUser { + + @Id + @Column(name="ID") + @Access(AccessType.PROPERTY) // we do this because relationships often fetch id, but not entity. This avoids an extra SQL + protected String id; + + @Column(name = "REALM_ID") + protected String realmId; + + @Column(name = "STORAGE_PROVIDER_ID") + protected String storageProviderId; + + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getRealmId() { + return realmId; + } + + public void setRealmId(String realmId) { + this.realmId = realmId; + } + + public String getStorageProviderId() { + return storageProviderId; + } + + public void setStorageProviderId(String storageProviderId) { + this.storageProviderId = storageProviderId; + } +} diff --git a/model/jpa/src/main/resources/META-INF/jpa-changelog-2.3.0.xml b/model/jpa/src/main/resources/META-INF/jpa-changelog-2.3.0.xml new file mode 100755 index 000000000000..d6041e03b137 --- /dev/null +++ b/model/jpa/src/main/resources/META-INF/jpa-changelog-2.3.0.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/model/jpa/src/main/resources/META-INF/jpa-changelog-master.xml b/model/jpa/src/main/resources/META-INF/jpa-changelog-master.xml index 55a52b60cb03..8990fc438ca7 100755 --- a/model/jpa/src/main/resources/META-INF/jpa-changelog-master.xml +++ b/model/jpa/src/main/resources/META-INF/jpa-changelog-master.xml @@ -36,4 +36,5 @@ + diff --git a/model/jpa/src/main/resources/META-INF/persistence.xml b/model/jpa/src/main/resources/META-INF/persistence.xml index 6288fe5e2ae9..c93a440421ab 100755 --- a/model/jpa/src/main/resources/META-INF/persistence.xml +++ b/model/jpa/src/main/resources/META-INF/persistence.xml @@ -70,6 +70,7 @@ org.keycloak.storage.jpa.entity.BrokerLinkEntity + org.keycloak.storage.jpa.entity.FederatedUser org.keycloak.storage.jpa.entity.FederatedUserAttributeEntity org.keycloak.storage.jpa.entity.FederatedUserConsentEntity org.keycloak.storage.jpa.entity.FederatedUserConsentRoleEntity diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/MongoUserProvider.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/MongoUserProvider.java index 9ad2c1ea36aa..87c64873e015 100755 --- a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/MongoUserProvider.java +++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/MongoUserProvider.java @@ -513,16 +513,6 @@ public void preRemove(RealmModel realm, RoleModel role) { getMongoStore().updateEntities(MongoUserConsentEntity.class, query, pull, invocationContext); } - @Override - public boolean validCredentials(KeycloakSession session, RealmModel realm, UserModel user, List input) { - return CredentialValidation.validCredentials(session, realm, user, input); - } - - @Override - public boolean validCredentials(KeycloakSession session, RealmModel realm, UserModel user, UserCredentialModel... input) { - return CredentialValidation.validCredentials(session, realm, user, input); - } - @Override public CredentialValidationOutput validCredentials(KeycloakSession session, RealmModel realm, UserCredentialModel... input) { // Not supported yet diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/UserAdapter.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/UserAdapter.java index b89ab0102a6f..caa1f71cb0b4 100755 --- a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/UserAdapter.java +++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/UserAdapter.java @@ -254,205 +254,6 @@ public void setOtpEnabled(boolean totp) { updateUser(); } - @Override - public void updateCredential(UserCredentialModel cred) { - - if (cred.getType().equals(UserCredentialModel.PASSWORD)) { - updatePasswordCredential(cred); - } else if (UserCredentialModel.isOtp(cred.getType())){ - updateOtpCredential(cred); - } else { - CredentialEntity credentialEntity = getCredentialEntity(user, cred.getType()); - - if (credentialEntity == null) { - credentialEntity = setCredentials(user, cred); - credentialEntity.setValue(cred.getValue()); - user.getCredentials().add(credentialEntity); - } else { - credentialEntity.setValue(cred.getValue()); - } - } - getMongoStore().updateEntity(user, invocationContext); - } - - private void updateOtpCredential(UserCredentialModel cred) { - CredentialEntity credentialEntity = getCredentialEntity(user, cred.getType()); - - if (credentialEntity == null) { - credentialEntity = setCredentials(user, cred); - credentialEntity.setValue(cred.getValue()); - OTPPolicy otpPolicy = realm.getOTPPolicy(); - credentialEntity.setAlgorithm(otpPolicy.getAlgorithm()); - credentialEntity.setDigits(otpPolicy.getDigits()); - credentialEntity.setCounter(otpPolicy.getInitialCounter()); - credentialEntity.setPeriod(otpPolicy.getPeriod()); - user.getCredentials().add(credentialEntity); - } else { - credentialEntity.setValue(cred.getValue()); - OTPPolicy policy = realm.getOTPPolicy(); - credentialEntity.setDigits(policy.getDigits()); - credentialEntity.setCounter(policy.getInitialCounter()); - credentialEntity.setAlgorithm(policy.getAlgorithm()); - credentialEntity.setPeriod(policy.getPeriod()); - } - } - - - private void updatePasswordCredential(UserCredentialModel cred) { - CredentialEntity credentialEntity = getCredentialEntity(user, cred.getType()); - - if (credentialEntity == null) { - credentialEntity = setCredentials(user, cred); - setValue(credentialEntity, cred); - user.getCredentials().add(credentialEntity); - } else { - - int expiredPasswordsPolicyValue = -1; - PasswordPolicy policy = realm.getPasswordPolicy(); - if(policy != null) { - expiredPasswordsPolicyValue = policy.getExpiredPasswords(); - } - - if (expiredPasswordsPolicyValue != -1) { - user.getCredentials().remove(credentialEntity); - credentialEntity.setType(UserCredentialModel.PASSWORD_HISTORY); - user.getCredentials().add(credentialEntity); - - List credentialEntities = getCredentialEntities(user, UserCredentialModel.PASSWORD_HISTORY); - if (credentialEntities.size() > expiredPasswordsPolicyValue - 1) { - user.getCredentials().removeAll(credentialEntities.subList(expiredPasswordsPolicyValue - 1, credentialEntities.size())); - } - - credentialEntity = setCredentials(user, cred); - setValue(credentialEntity, cred); - user.getCredentials().add(credentialEntity); - } else { - List credentialEntities = getCredentialEntities(user, UserCredentialModel.PASSWORD_HISTORY); - if (credentialEntities != null && credentialEntities.size() > 0) { - user.getCredentials().removeAll(credentialEntities); - } - setValue(credentialEntity, cred); - } - } - } - - private CredentialEntity setCredentials(MongoUserEntity user, UserCredentialModel cred) { - CredentialEntity credentialEntity = new CredentialEntity(); - credentialEntity.setType(cred.getType()); - credentialEntity.setDevice(cred.getDevice()); - return credentialEntity; - } - - private void setValue(CredentialEntity credentialEntity, UserCredentialModel cred) { - UserCredentialValueModel encoded = PasswordHashManager.encode(session, realm, cred.getValue()); - credentialEntity.setCreatedDate(Time.toMillis(Time.currentTime())); - credentialEntity.setAlgorithm(encoded.getAlgorithm()); - credentialEntity.setValue(encoded.getValue()); - credentialEntity.setSalt(encoded.getSalt()); - credentialEntity.setHashIterations(encoded.getHashIterations()); - } - - private CredentialEntity getCredentialEntity(MongoUserEntity userEntity, String credType) { - for (CredentialEntity entity : userEntity.getCredentials()) { - if (entity.getType().equals(credType)) { - return entity; - } - } - - return null; - } - - private List getCredentialEntities(MongoUserEntity userEntity, String credType) { - List credentialEntities = new ArrayList(); - for (CredentialEntity entity : userEntity.getCredentials()) { - if (entity.getType().equals(credType)) { - credentialEntities.add(entity); - } - } - - // Avoiding direct use of credSecond.getCreatedDate() - credFirst.getCreatedDate() to prevent Integer Overflow - // Orders from most recent to least recent - Collections.sort(credentialEntities, new Comparator() { - public int compare(CredentialEntity credFirst, CredentialEntity credSecond) { - if (credFirst.getCreatedDate() > credSecond.getCreatedDate()) { - return -1; - } else if (credFirst.getCreatedDate() < credSecond.getCreatedDate()) { - return 1; - } else { - return 0; - } - } - }); - return credentialEntities; - } - - @Override - public List getCredentialsDirectly() { - List credentials = user.getCredentials(); - List result = new ArrayList(); - for (CredentialEntity credEntity : credentials) { - UserCredentialValueModel credModel = new UserCredentialValueModel(); - credModel.setType(credEntity.getType()); - credModel.setDevice(credEntity.getDevice()); - credModel.setCreatedDate(credEntity.getCreatedDate()); - credModel.setValue(credEntity.getValue()); - credModel.setSalt(credEntity.getSalt()); - credModel.setHashIterations(credEntity.getHashIterations()); - credModel.setAlgorithm(credEntity.getAlgorithm()); - - if (UserCredentialModel.isOtp(credEntity.getType())) { - credModel.setCounter(credEntity.getCounter()); - if (credEntity.getAlgorithm() == null) { - // for migration where these values would be null - credModel.setAlgorithm(realm.getOTPPolicy().getAlgorithm()); - } else { - credModel.setAlgorithm(credEntity.getAlgorithm()); - } - if (credEntity.getDigits() == 0) { - // for migration where these values would be 0 - credModel.setDigits(realm.getOTPPolicy().getDigits()); - } else { - credModel.setDigits(credEntity.getDigits()); - } - - if (credEntity.getPeriod() == 0) { - // for migration where these values would be 0 - credModel.setPeriod(realm.getOTPPolicy().getPeriod()); - } else { - credModel.setPeriod(credEntity.getPeriod()); - } - } - - result.add(credModel); - } - - return result; - } - - @Override - public void updateCredentialDirectly(UserCredentialValueModel credModel) { - CredentialEntity credentialEntity = getCredentialEntity(user, credModel.getType()); - - if (credentialEntity == null) { - credentialEntity = new CredentialEntity(); - credentialEntity.setType(credModel.getType()); - credModel.setCreatedDate(credModel.getCreatedDate()); - user.getCredentials().add(credentialEntity); - } - - credentialEntity.setValue(credModel.getValue()); - credentialEntity.setSalt(credModel.getSalt()); - credentialEntity.setDevice(credModel.getDevice()); - credentialEntity.setHashIterations(credModel.getHashIterations()); - credentialEntity.setCounter(credModel.getCounter()); - credentialEntity.setAlgorithm(credModel.getAlgorithm()); - credentialEntity.setDigits(credModel.getDigits()); - credentialEntity.setPeriod(credModel.getPeriod()); - - - getMongoStore().updateEntity(user, invocationContext); - } - protected void updateUser() { super.updateMongoEntity(); } diff --git a/server-spi/src/main/java/org/keycloak/credential/CredentialProviderFactory.java b/server-spi/src/main/java/org/keycloak/credential/CredentialProviderFactory.java index 480dd1c5c2d5..9ad2bc8c8732 100755 --- a/server-spi/src/main/java/org/keycloak/credential/CredentialProviderFactory.java +++ b/server-spi/src/main/java/org/keycloak/credential/CredentialProviderFactory.java @@ -24,6 +24,7 @@ import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSessionFactory; import org.keycloak.provider.ProviderConfigProperty; +import org.keycloak.provider.ProviderFactory; import org.keycloak.storage.UserStorageProvider; import java.util.Collections; @@ -34,16 +35,7 @@ * @author Bill Burke * @version $Revision: 1 $ */ -public interface CredentialProviderFactory extends ComponentFactory { - /** - * called per Keycloak transaction. - * - * @param session - * @param model - * @return - */ - T create(KeycloakSession session, ComponentModel model); - +public interface CredentialProviderFactory extends ProviderFactory { /** * This is the name of the provider and will be showed in the admin console as an option. * @@ -67,19 +59,4 @@ default void close() { } - @Override - default String getHelpText() { - return ""; - } - - @Override - default List getConfigProperties() { - return Collections.EMPTY_LIST; - } - - @Override - default void validateConfiguration(KeycloakSession session, ComponentModel config) throws ComponentValidationException { - - } - } diff --git a/server-spi/src/main/java/org/keycloak/credential/CredentialSpi.java b/server-spi/src/main/java/org/keycloak/credential/CredentialSpi.java new file mode 100644 index 000000000000..34bd43f1b183 --- /dev/null +++ b/server-spi/src/main/java/org/keycloak/credential/CredentialSpi.java @@ -0,0 +1,50 @@ +/* + * Copyright 2016 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.credential; + +import org.keycloak.credential.hash.PasswordHashProvider; +import org.keycloak.credential.hash.PasswordHashProviderFactory; +import org.keycloak.provider.Provider; +import org.keycloak.provider.ProviderFactory; +import org.keycloak.provider.Spi; + +/** + * @author Kunal Kerkar + */ +public class CredentialSpi implements Spi { + + @Override + public boolean isInternal() { + return false; + } + + @Override + public String getName() { + return "credential"; + } + + @Override + public Class getProviderClass() { + return CredentialProvider.class; + } + + @Override + public Class getProviderFactoryClass() { + return CredentialProviderFactory.class; + } +} diff --git a/server-spi/src/main/java/org/keycloak/credential/hash/PasswordHashProvider.java b/server-spi/src/main/java/org/keycloak/credential/hash/PasswordHashProvider.java new file mode 100644 index 000000000000..74fe7cd00cfb --- /dev/null +++ b/server-spi/src/main/java/org/keycloak/credential/hash/PasswordHashProvider.java @@ -0,0 +1,35 @@ +/* + * Copyright 2016 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.credential.hash; + +import org.keycloak.credential.CredentialModel; +import org.keycloak.models.PasswordPolicy; +import org.keycloak.models.UserCredentialValueModel; +import org.keycloak.provider.Provider; + +/** + * @author Kunal Kerkar + */ +public interface PasswordHashProvider extends Provider { + boolean policyCheck(PasswordPolicy policy, CredentialModel credentia); + + void encode(String rawPassword, PasswordPolicy policy, CredentialModel credential); + + boolean verify(String rawPassword, CredentialModel credential); + +} diff --git a/server-spi/src/main/java/org/keycloak/credential/hash/PasswordHashProviderFactory.java b/server-spi/src/main/java/org/keycloak/credential/hash/PasswordHashProviderFactory.java new file mode 100644 index 000000000000..795bf789f4ae --- /dev/null +++ b/server-spi/src/main/java/org/keycloak/credential/hash/PasswordHashProviderFactory.java @@ -0,0 +1,27 @@ +/* + * Copyright 2016 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.credential.hash; + +import org.keycloak.provider.ProviderFactory; + +/** + * @author Kunal Kerkar + */ +public interface PasswordHashProviderFactory extends ProviderFactory { + +} diff --git a/server-spi/src/main/java/org/keycloak/credential/hash/PasswordHashSpi.java b/server-spi/src/main/java/org/keycloak/credential/hash/PasswordHashSpi.java new file mode 100644 index 000000000000..bdee135d6f4a --- /dev/null +++ b/server-spi/src/main/java/org/keycloak/credential/hash/PasswordHashSpi.java @@ -0,0 +1,48 @@ +/* + * Copyright 2016 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.credential.hash; + +import org.keycloak.provider.Provider; +import org.keycloak.provider.ProviderFactory; +import org.keycloak.provider.Spi; + +/** + * @author Kunal Kerkar + */ +public class PasswordHashSpi implements Spi { + + @Override + public boolean isInternal() { + return false; + } + + @Override + public String getName() { + return "password-hashing"; + } + + @Override + public Class getProviderClass() { + return PasswordHashProvider.class; + } + + @Override + public Class getProviderFactoryClass() { + return PasswordHashProviderFactory.class; + } +} diff --git a/server-spi/src/main/java/org/keycloak/migration/MigrationModel.java b/server-spi/src/main/java/org/keycloak/migration/MigrationModel.java index c2e6dab0ecee..59fcee832a53 100755 --- a/server-spi/src/main/java/org/keycloak/migration/MigrationModel.java +++ b/server-spi/src/main/java/org/keycloak/migration/MigrationModel.java @@ -26,7 +26,7 @@ public interface MigrationModel { /** * Must have the form of major.minor.micro as the version is parsed and numbers are compared */ - String LATEST_VERSION = "2.1.0"; + String LATEST_VERSION = "2.3.0"; String getStoredVersion(); void setStoredVersion(String version); diff --git a/server-spi/src/main/java/org/keycloak/models/UserCredentialManager.java b/server-spi/src/main/java/org/keycloak/models/UserCredentialManager.java index 2da5716f9255..0dd1aae40eca 100644 --- a/server-spi/src/main/java/org/keycloak/models/UserCredentialManager.java +++ b/server-spi/src/main/java/org/keycloak/models/UserCredentialManager.java @@ -27,11 +27,68 @@ * @version $Revision: 1 $ */ public interface UserCredentialManager extends UserCredentialStore { + + /** + * Validates list of credentials. Will call UserStorageProvider and UserFederationProviders first, then loop through + * each CredentialProvider. + * + * @param realm + * @param user + * @param inputs + * @return + */ boolean isValid(RealmModel realm, UserModel user, List inputs); + /** + * Validates list of credentials. Will call UserStorageProvider and UserFederationProviders first, then loop through + * each CredentialProvider. + * + * @param realm + * @param user + * @param inputs + * @return + */ + boolean isValid(RealmModel realm, UserModel user, CredentialInput... inputs); + + /** + * Updates a credential. Will call UserStorageProvider and UserFederationProviders first, then loop through + * each CredentialProvider. Update is finished whenever any one provider returns true. + * + * @param realm + * @param user + * @return + */ void updateCredential(RealmModel realm, UserModel user, CredentialInput input); + + /** + * Calls disableCredential on UserStorageProvider and UserFederationProviders first, then loop through + * each CredentialProvider. + * + * @param realm + * @param user + * @param credentialType + */ void disableCredential(RealmModel realm, UserModel user, String credentialType); + /** + * Checks to see if user has credential type configured. Looks in UserStorageProvider or UserFederationProvider first, + * then loops through each CredentialProvider. + * + * @param realm + * @param user + * @param type + * @return + */ boolean isConfiguredFor(RealmModel realm, UserModel user, String type); + /** + * Only loops through each CredentialProvider to see if credential type is configured for the user. + * This allows UserStorageProvider and UserFederationProvider to look to abort isValid + * + * @param realm + * @param user + * @param type + * @return + */ + boolean isConfiguredLocally(RealmModel realm, UserModel user, String type); } diff --git a/server-spi/src/main/java/org/keycloak/models/UserFederationManager.java b/server-spi/src/main/java/org/keycloak/models/UserFederationManager.java index e1037053fcfb..42f112d15b4e 100755 --- a/server-spi/src/main/java/org/keycloak/models/UserFederationManager.java +++ b/server-spi/src/main/java/org/keycloak/models/UserFederationManager.java @@ -58,10 +58,6 @@ public UserModel addUser(RealmModel realm, String id, String username, boolean a return registerWithFederation(realm, user); } - protected UserFederationProvider getFederationProvider(UserFederationProviderModel model) { - return KeycloakModelUtils.getFederationProviderInstance(session, model); - } - @Override public UserModel addUser(RealmModel realm, String username) { UserModel user = session.userStorage().addUser(realm, username.toLowerCase()); @@ -81,7 +77,11 @@ protected UserModel registerWithFederation(RealmModel realm, UserModel user) { return user; } - protected UserFederationProvider getFederationLink(RealmModel realm, UserModel user) { + protected UserFederationProvider getFederationProvider(UserFederationProviderModel model) { + return KeycloakModelUtils.getFederationProviderInstance(session, model); + } + + public UserFederationProvider getFederationLink(RealmModel realm, UserModel user) { if (user.getFederationLink() == null) return null; for (UserFederationProviderModel fed : realm.getUserFederationProviders()) { if (fed.getId().equals(user.getFederationLink())) { @@ -112,7 +112,7 @@ public boolean removeUser(RealmModel realm, UserModel user) { } - protected void validateUser(RealmModel realm, UserModel user) { + public void validateUser(RealmModel realm, UserModel user) { if (managedUsers.containsKey(user.getId())) { return; } @@ -488,84 +488,6 @@ public void preRemove(ProtocolMapperModel protocolMapper) { session.userStorage().preRemove(protocolMapper); } - public void updateCredential(RealmModel realm, UserModel user, UserCredentialModel credential) { - if (credential.getType().equals(UserCredentialModel.PASSWORD)) { - if (realm.getPasswordPolicy() != null) { - PolicyError error = session.getProvider(PasswordPolicyManagerProvider.class).validate(user, credential.getValue()); - if (error != null) throw new ModelException(error.getMessage(), error.getParameters()); - } - } - user.updateCredential(credential); - } - - @Override - public boolean validCredentials(KeycloakSession session, RealmModel realm, UserModel user, List input) { - UserFederationProvider link = getFederationLink(realm, user); - if (link != null) { - validateUser(realm, user); - Set supportedCredentialTypes = link.getSupportedCredentialTypes(user); - if (supportedCredentialTypes.size() > 0) { - List fedCreds = new ArrayList(); - List localCreds = new ArrayList(); - for (UserCredentialModel cred : input) { - if (supportedCredentialTypes.contains(cred.getType())) { - fedCreds.add(cred); - } else { - localCreds.add(cred); - } - } - if (!link.validCredentials(realm, user, fedCreds)) { - return false; - } - return session.userStorage().validCredentials(session, realm, user, localCreds); - } - } - return session.userStorage().validCredentials(session, realm, user, input); - } - - /** - * Is the user configured to use this credential type - * - * @return - */ - public boolean configuredForCredentialType(String type, RealmModel realm, UserModel user) { - UserFederationProvider link = getFederationLink(realm, user); - if (link != null) { - Set supportedCredentialTypes = link.getSupportedCredentialTypes(user); - if (supportedCredentialTypes.contains(type)) return true; - } - if (UserCredentialModel.isOtp(type)) { - if (!user.isOtpEnabled()) return false; - } - - List creds = user.getCredentialsDirectly(); - for (UserCredentialValueModel cred : creds) { - if (cred.getType().equals(type)) { - if (UserCredentialModel.isOtp(type)) { - OTPPolicy otpPolicy = realm.getOTPPolicy(); - if (!cred.getAlgorithm().equals(otpPolicy.getAlgorithm()) - || cred.getDigits() != otpPolicy.getDigits()) { - return false; - } - if (type.equals(UserCredentialModel.TOTP) && cred.getPeriod() != otpPolicy.getPeriod()) { - return false; - } - } - return true; - } - } - return false; - } - - - - - - @Override - public boolean validCredentials(KeycloakSession session, RealmModel realm, UserModel user, UserCredentialModel... input) { - return validCredentials(session, realm, user, Arrays.asList(input)); - } - @Override public CredentialValidationOutput validCredentials(KeycloakSession session, RealmModel realm, UserCredentialModel... input) { List fedProviderModels = realm.getUserFederationProviders(); diff --git a/server-spi/src/main/java/org/keycloak/models/UserFederationProvider.java b/server-spi/src/main/java/org/keycloak/models/UserFederationProvider.java index 66683176d5cc..69d17ec174c0 100755 --- a/server-spi/src/main/java/org/keycloak/models/UserFederationProvider.java +++ b/server-spi/src/main/java/org/keycloak/models/UserFederationProvider.java @@ -17,6 +17,8 @@ package org.keycloak.models; +import org.keycloak.credential.CredentialInputUpdater; +import org.keycloak.credential.CredentialInputValidator; import org.keycloak.provider.Provider; import java.util.List; @@ -30,7 +32,8 @@ * @author Bill Burke * @version $Revision: 1 $ */ -public interface UserFederationProvider extends Provider { +@Deprecated +public interface UserFederationProvider extends Provider, CredentialInputValidator, CredentialInputUpdater { public static final String USERNAME = UserModel.USERNAME; public static final String EMAIL = UserModel.EMAIL; @@ -166,14 +169,6 @@ enum EditMode { */ boolean isValid(RealmModel realm, UserModel local); - /** - * What UserCredentialModel types should be handled by this provider for this user? Keycloak will only call - * validCredentials() with the credential types specified in this method. - * - * @return - */ - Set getSupportedCredentialTypes(UserModel user); - /** * What UserCredentialModel types should be handled by this provider? This is called in scenarios when we don't know user, * who is going to authenticate (For example Kerberos authentication). @@ -182,18 +177,6 @@ enum EditMode { */ Set getSupportedCredentialTypes(); - /** - * Validate credentials for this user. This method will only be called with credential parameters supported - * by this provider - * - * @param realm - * @param user - * @param input - * @return - */ - boolean validCredentials(RealmModel realm, UserModel user, List input); - boolean validCredentials(RealmModel realm, UserModel user, UserCredentialModel... input); - /** * Validate credentials of unknown user. The authenticated user is recognized based on provided credentials and returned back in CredentialValidationOutput * @param realm diff --git a/server-spi/src/main/java/org/keycloak/models/UserModel.java b/server-spi/src/main/java/org/keycloak/models/UserModel.java index 28c35ab4266a..c75633002d95 100755 --- a/server-spi/src/main/java/org/keycloak/models/UserModel.java +++ b/server-spi/src/main/java/org/keycloak/models/UserModel.java @@ -112,12 +112,6 @@ interface UserRemovedEvent extends ProviderEvent { void setOtpEnabled(boolean totp); - void updateCredential(UserCredentialModel cred); - - List getCredentialsDirectly(); - - void updateCredentialDirectly(UserCredentialValueModel cred); - Set getGroups(); void joinGroup(GroupModel group); void leaveGroup(GroupModel group); diff --git a/server-spi/src/main/java/org/keycloak/models/UserProvider.java b/server-spi/src/main/java/org/keycloak/models/UserProvider.java index 642131b51853..d6267c906467 100755 --- a/server-spi/src/main/java/org/keycloak/models/UserProvider.java +++ b/server-spi/src/main/java/org/keycloak/models/UserProvider.java @@ -35,7 +35,6 @@ public interface UserProvider extends Provider, UserLookupProvider, UserQueryProvider, - UserCredentialValidatorProvider, UserRegistrationProvider { // Note: The reason there are so many query methods here is for layering a cache on top of an persistent KeycloakSession @@ -79,7 +78,6 @@ public interface UserProvider extends Provider, void preRemove(ProtocolMapperModel protocolMapper); - boolean validCredentials(KeycloakSession session, RealmModel realm, UserModel user, UserCredentialModel... input); CredentialValidationOutput validCredentials(KeycloakSession session, RealmModel realm, UserCredentialModel... input); diff --git a/server-spi/src/main/java/org/keycloak/models/utils/CredentialValidation.java b/server-spi/src/main/java/org/keycloak/models/utils/CredentialValidation.java index 85d52c002c27..4bbb2ab192f6 100755 --- a/server-spi/src/main/java/org/keycloak/models/utils/CredentialValidation.java +++ b/server-spi/src/main/java/org/keycloak/models/utils/CredentialValidation.java @@ -38,45 +38,6 @@ */ public class CredentialValidation { - /** - * Will update password if hash iteration policy has changed - * - * @param realm - * @param user - * @param password - * @return - */ - public static boolean validPassword(KeycloakSession session, RealmModel realm, UserModel user, String password) { - UserCredentialValueModel passwordCred = null; - for (UserCredentialValueModel cred : user.getCredentialsDirectly()) { - if (cred.getType().equals(UserCredentialModel.PASSWORD)) { - passwordCred = cred; - } - } - if (passwordCred == null) return false; - - return validateHashedCredential(session, realm, user, password, passwordCred); - - } - - - public static boolean validateHashedCredential(KeycloakSession session, RealmModel realm, UserModel user, String unhashedCredValue, UserCredentialValueModel credential) { - if (unhashedCredValue == null || unhashedCredValue.isEmpty()) { - return false; - } - - boolean validated = PasswordHashManager.verify(session, realm, unhashedCredValue, credential); - - if (validated) { - if (realm.getPasswordPolicy().getHashIterations() != credential.getHashIterations()) { - - UserCredentialValueModel newCred = PasswordHashManager.encode(session, realm, unhashedCredValue); - user.updateCredentialDirectly(newCred); - } - - } - return validated; - } public static boolean validPasswordToken(RealmModel realm, UserModel user, String encodedPasswordToken) { try { @@ -100,23 +61,6 @@ public static boolean validPasswordToken(RealmModel realm, UserModel user, Strin } } - public static boolean validHOTP(RealmModel realm, UserModel user, String otp) { - UserCredentialValueModel passwordCred = null; - OTPPolicy policy = realm.getOTPPolicy(); - HmacOTP validator = new HmacOTP(policy.getDigits(), policy.getAlgorithm(), policy.getLookAheadWindow()); - for (UserCredentialValueModel cred : user.getCredentialsDirectly()) { - if (cred.getType().equals(UserCredentialModel.HOTP)) { - int counter = validator.validateHOTP(otp, cred.getValue(), cred.getCounter()); - if (counter < 0) return false; - cred.setCounter(counter); - user.updateCredentialDirectly(cred); - return true; - } - } - return false; - - } - public static boolean validOTP(RealmModel realm, String token, String secret) { OTPPolicy policy = realm.getOTPPolicy(); if (policy.getType().equals(UserCredentialModel.TOTP)) { @@ -130,84 +74,5 @@ public static boolean validOTP(RealmModel realm, String token, String secret) { } - public static boolean validTOTP(RealmModel realm, UserModel user, String otp) { - UserCredentialValueModel passwordCred = null; - OTPPolicy policy = realm.getOTPPolicy(); - TimeBasedOTP validator = new TimeBasedOTP(policy.getAlgorithm(), policy.getDigits(), policy.getPeriod(), policy.getLookAheadWindow()); - for (UserCredentialValueModel cred : user.getCredentialsDirectly()) { - if (cred.getType().equals(UserCredentialModel.TOTP)) { - if (validator.validateTOTP(otp, cred.getValue().getBytes())) { - return true; - } - } - } - return false; - } - public static boolean validSecret(RealmModel realm, UserModel user, String secret) { - for (UserCredentialValueModel cred : user.getCredentialsDirectly()) { - if (cred.getType().equals(UserCredentialModel.SECRET)) { - if (cred.getValue().equals(secret)) return true; - } - } - return false; - - } - - /** - * Must validate all credentials. FYI, password hashes may be rehashed and updated based on realm hash password policies. - * - * @param realm - * @param user - * @param credentials - * @return - */ - public static boolean validCredentials(KeycloakSession session, RealmModel realm, UserModel user, List credentials) { - for (UserCredentialModel credential : credentials) { - if (!validCredential(session, realm, user, credential)) return false; - } - return true; - } - - /** - * Must validate all credentials. FYI, password hashes may be rehashed and updated based on realm hash password policies. - * - * @param realm - * @param user - * @param credentials - * @return - */ - public static boolean validCredentials(KeycloakSession session, RealmModel realm, UserModel user, UserCredentialModel... credentials) { - for (UserCredentialModel credential : credentials) { - if (!validCredential(session, realm, user, credential)) return false; - } - return true; - } - - public static boolean validCredential(KeycloakSession session, RealmModel realm, UserModel user, UserCredentialModel credential) { - if (credential.getType().equals(UserCredentialModel.PASSWORD)) { - if (!validPassword(session, realm, user, credential.getValue())) { - return false; - } - } else if (credential.getType().equals(UserCredentialModel.PASSWORD_TOKEN)) { - if (!validPasswordToken(realm, user, credential.getValue())) { - return false; - } - } else if (credential.getType().equals(UserCredentialModel.TOTP)) { - if (!validTOTP(realm, user, credential.getValue())) { - return false; - } - } else if (credential.getType().equals(UserCredentialModel.HOTP)) { - if (!validHOTP(realm, user, credential.getValue())) { - return false; - } - } else if (credential.getType().equals(UserCredentialModel.SECRET)) { - if (!validSecret(realm, user, credential.getValue())) { - return false; - } - } else { - return false; - } - return true; - } } diff --git a/server-spi/src/main/java/org/keycloak/models/utils/RepresentationToModel.java b/server-spi/src/main/java/org/keycloak/models/utils/RepresentationToModel.java index d1d5b059d79a..f78f1cfad337 100755 --- a/server-spi/src/main/java/org/keycloak/models/utils/RepresentationToModel.java +++ b/server-spi/src/main/java/org/keycloak/models/utils/RepresentationToModel.java @@ -30,6 +30,7 @@ import org.keycloak.authorization.store.StoreFactory; import org.keycloak.common.util.MultivaluedHashMap; import org.keycloak.component.ComponentModel; +import org.keycloak.credential.CredentialModel; import org.keycloak.hash.Pbkdf2PasswordHashProvider; import org.keycloak.migration.migrators.MigrationUtils; import org.keycloak.models.ClientTemplateModel; @@ -1326,7 +1327,7 @@ public static UserModel createUser(KeycloakSession session, RealmModel newRealm, user.addRequiredAction(UserModel.RequiredAction.valueOf(requiredAction)); } } - createCredentials(userRep, user); + createCredentials(userRep, session, newRealm, user); if (userRep.getFederatedIdentities() != null) { for (FederatedIdentityRepresentation identity : userRep.getFederatedIdentities()) { FederatedIdentityModel mappingModel = new FederatedIdentityModel(identity.getIdentityProvider(), identity.getUserId(), identity.getUserName()); @@ -1361,21 +1362,21 @@ public static UserModel createUser(KeycloakSession session, RealmModel newRealm, return user; } - public static void createCredentials(UserRepresentation userRep, UserModel user) { + public static void createCredentials(UserRepresentation userRep, KeycloakSession session, RealmModel realm,UserModel user) { if (userRep.getCredentials() != null) { for (CredentialRepresentation cred : userRep.getCredentials()) { - updateCredential(user, cred); + updateCredential(session, realm, user, cred); } } } // Detect if it is "plain-text" or "hashed" representation and update model according to it - private static void updateCredential(UserModel user, CredentialRepresentation cred) { + private static void updateCredential(KeycloakSession session, RealmModel realm, UserModel user, CredentialRepresentation cred) { if (cred.getValue() != null) { UserCredentialModel plainTextCred = convertCredential(cred); - user.updateCredential(plainTextCred); + session.userCredentialManager().updateCredential(realm, user, plainTextCred); } else { - UserCredentialValueModel hashedCred = new UserCredentialValueModel(); + CredentialModel hashedCred = new CredentialModel(); hashedCred.setType(cred.getType()); hashedCred.setDevice(cred.getDevice()); if (cred.getHashIterations() != null) hashedCred.setHashIterations(cred.getHashIterations()); @@ -1414,7 +1415,7 @@ private static void updateCredential(UserModel user, CredentialRepresentation cr hashedCred.setPeriod(30); } hashedCred.setCreatedDate(cred.getCreatedDate()); - user.updateCredentialDirectly(hashedCred); + session.userCredentialManager().createCredential(realm, user, hashedCred); } } diff --git a/server-spi/src/main/java/org/keycloak/models/utils/UserModelDelegate.java b/server-spi/src/main/java/org/keycloak/models/utils/UserModelDelegate.java index 9a8e4abcc714..4596ff4907ca 100755 --- a/server-spi/src/main/java/org/keycloak/models/utils/UserModelDelegate.java +++ b/server-spi/src/main/java/org/keycloak/models/utils/UserModelDelegate.java @@ -170,21 +170,6 @@ public void setOtpEnabled(boolean totp) { delegate.setOtpEnabled(totp); } - @Override - public void updateCredential(UserCredentialModel cred) { - delegate.updateCredential(cred); - } - - @Override - public List getCredentialsDirectly() { - return delegate.getCredentialsDirectly(); - } - - @Override - public void updateCredentialDirectly(UserCredentialValueModel cred) { - delegate.updateCredentialDirectly(cred); - } - @Override public Set getRealmRoleMappings() { return delegate.getRealmRoleMappings(); diff --git a/server-spi/src/main/java/org/keycloak/policy/DefaultPasswordPolicyManagerProvider.java b/server-spi/src/main/java/org/keycloak/policy/DefaultPasswordPolicyManagerProvider.java index c380f804d378..b1bfc0c1ab8c 100644 --- a/server-spi/src/main/java/org/keycloak/policy/DefaultPasswordPolicyManagerProvider.java +++ b/server-spi/src/main/java/org/keycloak/policy/DefaultPasswordPolicyManagerProvider.java @@ -19,6 +19,7 @@ import org.keycloak.models.KeycloakSession; import org.keycloak.models.PasswordPolicy; +import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; import java.util.LinkedList; @@ -36,9 +37,9 @@ public DefaultPasswordPolicyManagerProvider(KeycloakSession session) { } @Override - public PolicyError validate(UserModel user, String password) { - for (PasswordPolicyProvider p : getProviders(session)) { - PolicyError policyError = p.validate(user, password); + public PolicyError validate(RealmModel realm, UserModel user, String password) { + for (PasswordPolicyProvider p : getProviders(realm, session)) { + PolicyError policyError = p.validate(realm, user, password); if (policyError != null) { return policyError; } @@ -62,8 +63,13 @@ public void close() { } private List getProviders(KeycloakSession session) { + return getProviders(session.getContext().getRealm(), session); + + } + + private List getProviders(RealmModel realm, KeycloakSession session) { LinkedList list = new LinkedList<>(); - PasswordPolicy policy = session.getContext().getRealm().getPasswordPolicy(); + PasswordPolicy policy = realm.getPasswordPolicy(); for (String id : policy.getPolicies()) { PasswordPolicyProvider provider = session.getProvider(PasswordPolicyProvider.class, id); list.add(provider); diff --git a/server-spi/src/main/java/org/keycloak/policy/DigitsPasswordPolicyProvider.java b/server-spi/src/main/java/org/keycloak/policy/DigitsPasswordPolicyProvider.java index d3ca22dea7bd..391317ed9b36 100644 --- a/server-spi/src/main/java/org/keycloak/policy/DigitsPasswordPolicyProvider.java +++ b/server-spi/src/main/java/org/keycloak/policy/DigitsPasswordPolicyProvider.java @@ -18,6 +18,7 @@ package org.keycloak.policy; import org.keycloak.models.KeycloakContext; +import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; /** @@ -46,7 +47,7 @@ public PolicyError validate(String username, String password) { } @Override - public PolicyError validate(UserModel user, String password) { + public PolicyError validate(RealmModel realm, UserModel user, String password) { return validate(user.getUsername(), password); } diff --git a/server-spi/src/main/java/org/keycloak/policy/ForceExpiredPasswordPolicyProviderFactory.java b/server-spi/src/main/java/org/keycloak/policy/ForceExpiredPasswordPolicyProviderFactory.java index ecefffb3be01..114447834d01 100644 --- a/server-spi/src/main/java/org/keycloak/policy/ForceExpiredPasswordPolicyProviderFactory.java +++ b/server-spi/src/main/java/org/keycloak/policy/ForceExpiredPasswordPolicyProviderFactory.java @@ -20,6 +20,7 @@ import org.keycloak.Config; import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSessionFactory; +import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; /** @@ -53,7 +54,7 @@ public String getId() { } @Override - public PolicyError validate(UserModel user, String password) { + public PolicyError validate(RealmModel realm, UserModel user, String password) { return null; } diff --git a/server-spi/src/main/java/org/keycloak/policy/HashAlgorithmPasswordPolicyProviderFactory.java b/server-spi/src/main/java/org/keycloak/policy/HashAlgorithmPasswordPolicyProviderFactory.java index add3b03cf2df..e7daaabba0a6 100644 --- a/server-spi/src/main/java/org/keycloak/policy/HashAlgorithmPasswordPolicyProviderFactory.java +++ b/server-spi/src/main/java/org/keycloak/policy/HashAlgorithmPasswordPolicyProviderFactory.java @@ -20,6 +20,7 @@ import org.keycloak.Config; import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSessionFactory; +import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; /** @@ -54,7 +55,7 @@ public String getId() { } @Override - public PolicyError validate(UserModel user, String password) { + public PolicyError validate(RealmModel realm, UserModel user, String password) { return null; } diff --git a/server-spi/src/main/java/org/keycloak/policy/HashIterationsPasswordPolicyProviderFactory.java b/server-spi/src/main/java/org/keycloak/policy/HashIterationsPasswordPolicyProviderFactory.java index 64a77d544039..ae9d497e2385 100644 --- a/server-spi/src/main/java/org/keycloak/policy/HashIterationsPasswordPolicyProviderFactory.java +++ b/server-spi/src/main/java/org/keycloak/policy/HashIterationsPasswordPolicyProviderFactory.java @@ -20,6 +20,7 @@ import org.keycloak.Config; import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSessionFactory; +import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; /** @@ -50,7 +51,7 @@ public String getId() { } @Override - public PolicyError validate(UserModel user, String password) { + public PolicyError validate(RealmModel realm, UserModel user, String password) { return null; } diff --git a/server-spi/src/main/java/org/keycloak/policy/HistoryPasswordPolicyProvider.java b/server-spi/src/main/java/org/keycloak/policy/HistoryPasswordPolicyProvider.java index 3cfe5fb6ce8f..b65aff216cb1 100644 --- a/server-spi/src/main/java/org/keycloak/policy/HistoryPasswordPolicyProvider.java +++ b/server-spi/src/main/java/org/keycloak/policy/HistoryPasswordPolicyProvider.java @@ -17,9 +17,13 @@ package org.keycloak.policy; +import org.jboss.logging.Logger; +import org.keycloak.credential.CredentialModel; +import org.keycloak.credential.hash.PasswordHashProvider; import org.keycloak.hash.PasswordHashManager; import org.keycloak.models.KeycloakSession; import org.keycloak.models.PasswordPolicy; +import org.keycloak.models.RealmModel; import org.keycloak.models.UserCredentialModel; import org.keycloak.models.UserCredentialValueModel; import org.keycloak.models.UserModel; @@ -34,6 +38,7 @@ */ public class HistoryPasswordPolicyProvider implements PasswordPolicyProvider { + private static final Logger logger = Logger.getLogger(HistoryPasswordPolicyProvider.class); private static final String ERROR_MESSAGE = "invalidPasswordHistoryMessage"; private KeycloakSession session; @@ -48,63 +53,30 @@ public PolicyError validate(String username, String password) { } @Override - public PolicyError validate(UserModel user, String password) { + public PolicyError validate(RealmModel realm, UserModel user, String password) { PasswordPolicy policy = session.getContext().getRealm().getPasswordPolicy(); int passwordHistoryPolicyValue = policy.getPolicyConfig(HistoryPasswordPolicyProviderFactory.ID); if (passwordHistoryPolicyValue != -1) { - UserCredentialValueModel cred = getCredentialValueModel(user, UserCredentialModel.PASSWORD); - if (cred != null) { - if(PasswordHashManager.verify(session, policy, password, cred)) { + List storedPasswords = session.userCredentialManager().getStoredCredentialsByType(realm, user, CredentialModel.PASSWORD); + for (CredentialModel cred : storedPasswords) { + PasswordHashProvider hash = session.getProvider(PasswordHashProvider.class, cred.getAlgorithm()); + if (hash == null) continue; + if (hash.verify(password, cred)) { return new PolicyError(ERROR_MESSAGE, passwordHistoryPolicyValue); } } - - List passwordExpiredCredentials = getCredentialValueModels(user, passwordHistoryPolicyValue - 1, - UserCredentialModel.PASSWORD_HISTORY); - for (UserCredentialValueModel credential : passwordExpiredCredentials) { - if (PasswordHashManager.verify(session, policy, password, credential)) { + List passwordHistory = session.userCredentialManager().getStoredCredentialsByType(realm, user, CredentialModel.PASSWORD_HISTORY); + for (CredentialModel cred : passwordHistory) { + PasswordHashProvider hash = session.getProvider(PasswordHashProvider.class, cred.getAlgorithm()); + if (hash.verify(password, cred)) { return new PolicyError(ERROR_MESSAGE, passwordHistoryPolicyValue); } - } - } - return null; - } - private UserCredentialValueModel getCredentialValueModel(UserModel user, String credType) { - for (UserCredentialValueModel model : user.getCredentialsDirectly()) { - if (model.getType().equals(credType)) { - return model; } } return null; } - private List getCredentialValueModels(UserModel user, int expiredPasswordsPolicyValue, String credType) { - List credentialModels = new ArrayList(); - for (UserCredentialValueModel model : user.getCredentialsDirectly()) { - if (model.getType().equals(credType)) { - credentialModels.add(model); - } - } - - Collections.sort(credentialModels, new Comparator() { - public int compare(UserCredentialValueModel credFirst, UserCredentialValueModel credSecond) { - if (credFirst.getCreatedDate() > credSecond.getCreatedDate()) { - return -1; - } else if (credFirst.getCreatedDate() < credSecond.getCreatedDate()) { - return 1; - } else { - return 0; - } - } - }); - - if (credentialModels.size() > expiredPasswordsPolicyValue) { - return credentialModels.subList(0, expiredPasswordsPolicyValue); - } - return credentialModels; - } - @Override public Object parseConfig(String value) { return value != null ? Integer.parseInt(value) : HistoryPasswordPolicyProviderFactory.DEFAULT_VALUE; diff --git a/server-spi/src/main/java/org/keycloak/policy/LengthPasswordPolicyProvider.java b/server-spi/src/main/java/org/keycloak/policy/LengthPasswordPolicyProvider.java index bdafdc929345..5ba71fee45a5 100644 --- a/server-spi/src/main/java/org/keycloak/policy/LengthPasswordPolicyProvider.java +++ b/server-spi/src/main/java/org/keycloak/policy/LengthPasswordPolicyProvider.java @@ -18,6 +18,7 @@ package org.keycloak.policy; import org.keycloak.models.KeycloakContext; +import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; /** @@ -40,7 +41,7 @@ public PolicyError validate(String username, String password) { } @Override - public PolicyError validate(UserModel user, String password) { + public PolicyError validate(RealmModel realm, UserModel user, String password) { return validate(user.getUsername(), password); } diff --git a/server-spi/src/main/java/org/keycloak/policy/LowerCasePasswordPolicyProvider.java b/server-spi/src/main/java/org/keycloak/policy/LowerCasePasswordPolicyProvider.java index 3312e2d2c0e6..f080d005948e 100644 --- a/server-spi/src/main/java/org/keycloak/policy/LowerCasePasswordPolicyProvider.java +++ b/server-spi/src/main/java/org/keycloak/policy/LowerCasePasswordPolicyProvider.java @@ -18,6 +18,7 @@ package org.keycloak.policy; import org.keycloak.models.KeycloakContext; +import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; /** @@ -46,7 +47,7 @@ public PolicyError validate(String username, String password) { } @Override - public PolicyError validate(UserModel user, String password) { + public PolicyError validate(RealmModel realm, UserModel user, String password) { return validate(user.getUsername(), password); } diff --git a/server-spi/src/main/java/org/keycloak/policy/NotUsernamePasswordPolicyProvider.java b/server-spi/src/main/java/org/keycloak/policy/NotUsernamePasswordPolicyProvider.java index 54634e670c1c..f08edab0d6d3 100644 --- a/server-spi/src/main/java/org/keycloak/policy/NotUsernamePasswordPolicyProvider.java +++ b/server-spi/src/main/java/org/keycloak/policy/NotUsernamePasswordPolicyProvider.java @@ -18,6 +18,7 @@ package org.keycloak.policy; import org.keycloak.models.KeycloakContext; +import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; /** @@ -42,7 +43,7 @@ public PolicyError validate(String username, String password) { } @Override - public PolicyError validate(UserModel user, String password) { + public PolicyError validate(RealmModel realm, UserModel user, String password) { return validate(user.getUsername(), password); } diff --git a/server-spi/src/main/java/org/keycloak/policy/PasswordPolicyManagerProvider.java b/server-spi/src/main/java/org/keycloak/policy/PasswordPolicyManagerProvider.java index 3039c95e0935..e5e84977633b 100644 --- a/server-spi/src/main/java/org/keycloak/policy/PasswordPolicyManagerProvider.java +++ b/server-spi/src/main/java/org/keycloak/policy/PasswordPolicyManagerProvider.java @@ -17,6 +17,7 @@ package org.keycloak.policy; +import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; import org.keycloak.provider.Provider; @@ -25,7 +26,7 @@ */ public interface PasswordPolicyManagerProvider extends Provider { - PolicyError validate(UserModel user, String password); + PolicyError validate(RealmModel realm, UserModel user, String password); PolicyError validate(String user, String password); } diff --git a/server-spi/src/main/java/org/keycloak/policy/PasswordPolicyProvider.java b/server-spi/src/main/java/org/keycloak/policy/PasswordPolicyProvider.java index 96b1803b093d..3f7c3ea869d0 100644 --- a/server-spi/src/main/java/org/keycloak/policy/PasswordPolicyProvider.java +++ b/server-spi/src/main/java/org/keycloak/policy/PasswordPolicyProvider.java @@ -17,6 +17,7 @@ package org.keycloak.policy; +import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; import org.keycloak.provider.Provider; @@ -28,7 +29,7 @@ public interface PasswordPolicyProvider extends Provider { String STRING_CONFIG_TYPE = "String"; String INT_CONFIG_TYPE = "int"; - PolicyError validate(UserModel user, String password); + PolicyError validate(RealmModel realm, UserModel user, String password); PolicyError validate(String user, String password); Object parseConfig(String value); diff --git a/server-spi/src/main/java/org/keycloak/policy/RegexPatternsPasswordPolicyProvider.java b/server-spi/src/main/java/org/keycloak/policy/RegexPatternsPasswordPolicyProvider.java index 7d4dbccf870b..52c83b86e841 100644 --- a/server-spi/src/main/java/org/keycloak/policy/RegexPatternsPasswordPolicyProvider.java +++ b/server-spi/src/main/java/org/keycloak/policy/RegexPatternsPasswordPolicyProvider.java @@ -18,6 +18,7 @@ package org.keycloak.policy; import org.keycloak.models.KeycloakContext; +import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; import java.util.regex.Matcher; @@ -47,7 +48,7 @@ public PolicyError validate(String username, String password) { } @Override - public PolicyError validate(UserModel user, String password) { + public PolicyError validate(RealmModel realm, UserModel user, String password) { return validate(user.getUsername(), password); } diff --git a/server-spi/src/main/java/org/keycloak/policy/SpecialCharsPasswordPolicyProvider.java b/server-spi/src/main/java/org/keycloak/policy/SpecialCharsPasswordPolicyProvider.java index 694d81e23a6f..fa851372a9ef 100644 --- a/server-spi/src/main/java/org/keycloak/policy/SpecialCharsPasswordPolicyProvider.java +++ b/server-spi/src/main/java/org/keycloak/policy/SpecialCharsPasswordPolicyProvider.java @@ -18,6 +18,7 @@ package org.keycloak.policy; import org.keycloak.models.KeycloakContext; +import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; /** @@ -46,7 +47,7 @@ public PolicyError validate(String username, String password) { } @Override - public PolicyError validate(UserModel user, String password) { + public PolicyError validate(RealmModel realm, UserModel user, String password) { return validate(user.getUsername(), password); } diff --git a/server-spi/src/main/java/org/keycloak/policy/UpperCasePasswordPolicyProvider.java b/server-spi/src/main/java/org/keycloak/policy/UpperCasePasswordPolicyProvider.java index d8a570b6df81..16ac1eff58f2 100644 --- a/server-spi/src/main/java/org/keycloak/policy/UpperCasePasswordPolicyProvider.java +++ b/server-spi/src/main/java/org/keycloak/policy/UpperCasePasswordPolicyProvider.java @@ -18,6 +18,7 @@ package org.keycloak.policy; import org.keycloak.models.KeycloakContext; +import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; /** @@ -46,7 +47,7 @@ public PolicyError validate(String username, String password) { } @Override - public PolicyError validate(UserModel user, String password) { + public PolicyError validate(RealmModel realm, UserModel user, String password) { return validate(user.getUsername(), password); } diff --git a/server-spi/src/main/java/org/keycloak/storage/adapter/AbstractUserAdapter.java b/server-spi/src/main/java/org/keycloak/storage/adapter/AbstractUserAdapter.java index 6a4e7d2be8da..fdf1893a05ca 100644 --- a/server-spi/src/main/java/org/keycloak/storage/adapter/AbstractUserAdapter.java +++ b/server-spi/src/main/java/org/keycloak/storage/adapter/AbstractUserAdapter.java @@ -386,23 +386,6 @@ public void setEmailVerified(boolean verified) { } - @Override - public void updateCredential(UserCredentialModel cred) { - throw new ReadOnlyException("user is read only for this update"); - - } - - @Override - public List getCredentialsDirectly() { - return Collections.EMPTY_LIST; - } - - @Override - public void updateCredentialDirectly(UserCredentialValueModel cred) { - throw new ReadOnlyException("user is read only for this update"); - - } - @Override public boolean equals(Object o) { if (this == o) return true; diff --git a/server-spi/src/main/java/org/keycloak/storage/adapter/AbstractUserAdapterFederatedStorage.java b/server-spi/src/main/java/org/keycloak/storage/adapter/AbstractUserAdapterFederatedStorage.java index bfe0b95f6eef..1b83fffa663a 100644 --- a/server-spi/src/main/java/org/keycloak/storage/adapter/AbstractUserAdapterFederatedStorage.java +++ b/server-spi/src/main/java/org/keycloak/storage/adapter/AbstractUserAdapterFederatedStorage.java @@ -399,23 +399,6 @@ public void setEmailVerified(boolean verified) { } - @Override - public void updateCredential(UserCredentialModel cred) { - getFederatedStorage().updateCredential(realm, this, cred); - - } - - @Override - public List getCredentialsDirectly() { - return getFederatedStorage().getCredentials(realm, this); - } - - @Override - public void updateCredentialDirectly(UserCredentialValueModel cred) { - getFederatedStorage().updateCredential(realm, this, cred); - - } - @Override public boolean equals(Object o) { if (this == o) return true; diff --git a/server-spi/src/main/java/org/keycloak/storage/federated/UserFederatedStorageProvider.java b/server-spi/src/main/java/org/keycloak/storage/federated/UserFederatedStorageProvider.java index b60499199c86..4a4530196732 100755 --- a/server-spi/src/main/java/org/keycloak/storage/federated/UserFederatedStorageProvider.java +++ b/server-spi/src/main/java/org/keycloak/storage/federated/UserFederatedStorageProvider.java @@ -27,6 +27,9 @@ import org.keycloak.models.UserModel; import org.keycloak.provider.Provider; +import java.util.List; +import java.util.Set; + /** * @author Bill Burke * @version $Revision: 1 $ @@ -40,6 +43,8 @@ public interface UserFederatedStorageProvider extends Provider, UserRequiredActionsFederatedStorage, UserRoleMappingsFederatedStorage { + List getStoredUsers(RealmModel realm, int first, int max); + void preRemove(RealmModel realm); void preRemove(RealmModel realm, UserFederationProviderModel link); diff --git a/server-spi/src/main/resources/META-INF/services/org.keycloak.provider.Spi b/server-spi/src/main/resources/META-INF/services/org.keycloak.provider.Spi index 5ab0346aa3a7..889c7a48cf4a 100755 --- a/server-spi/src/main/resources/META-INF/services/org.keycloak.provider.Spi +++ b/server-spi/src/main/resources/META-INF/services/org.keycloak.provider.Spi @@ -63,3 +63,5 @@ org.keycloak.protocol.oidc.TokenIntrospectionSpi org.keycloak.policy.PasswordPolicySpi org.keycloak.policy.PasswordPolicyManagerSpi org.keycloak.transaction.TransactionManagerLookupSpi +org.keycloak.credential.hash.PasswordHashSpi +org.keycloak.credential.CredentialSpi diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/browser/AbstractUsernameFormAuthenticator.java b/services/src/main/java/org/keycloak/authentication/authenticators/browser/AbstractUsernameFormAuthenticator.java index ad4bb0bc6c53..52491012f07c 100755 --- a/services/src/main/java/org/keycloak/authentication/authenticators/browser/AbstractUsernameFormAuthenticator.java +++ b/services/src/main/java/org/keycloak/authentication/authenticators/browser/AbstractUsernameFormAuthenticator.java @@ -20,6 +20,7 @@ import org.keycloak.authentication.AbstractFormAuthenticator; import org.keycloak.authentication.AuthenticationFlowError; import org.keycloak.authentication.AuthenticationFlowContext; +import org.keycloak.credential.CredentialInput; import org.keycloak.events.Details; import org.keycloak.events.Errors; import org.keycloak.models.ModelDuplicateException; @@ -167,10 +168,10 @@ public boolean validateUserAndPassword(AuthenticationFlowContext context, Multiv } public boolean validatePassword(AuthenticationFlowContext context, UserModel user, MultivaluedMap inputData) { - List credentials = new LinkedList<>(); + List credentials = new LinkedList<>(); String password = inputData.getFirst(CredentialRepresentation.PASSWORD); credentials.add(UserCredentialModel.password(password)); - boolean valid = context.getSession().users().validCredentials(context.getSession(), context.getRealm(), user, credentials); + boolean valid = context.getSession().userCredentialManager().isValid(context.getRealm(), user, credentials); if (!valid) { context.getEvent().user(user); context.getEvent().error(Errors.INVALID_USER_CREDENTIALS); diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/browser/OTPFormAuthenticator.java b/services/src/main/java/org/keycloak/authentication/authenticators/browser/OTPFormAuthenticator.java index 4f3dde072bc5..543aa967c56f 100755 --- a/services/src/main/java/org/keycloak/authentication/authenticators/browser/OTPFormAuthenticator.java +++ b/services/src/main/java/org/keycloak/authentication/authenticators/browser/OTPFormAuthenticator.java @@ -58,15 +58,14 @@ public void validateOTP(AuthenticationFlowContext context) { context.resetFlow(); return; } - List credentials = new LinkedList<>(); String password = inputData.getFirst(CredentialRepresentation.TOTP); if (password == null) { Response challengeResponse = challenge(context, null); context.challenge(challengeResponse); return; } - credentials.add(UserCredentialModel.otp(context.getRealm().getOTPPolicy().getType(), password)); - boolean valid = context.getSession().users().validCredentials(context.getSession(), context.getRealm(), context.getUser(), credentials); + boolean valid = context.getSession().userCredentialManager().isValid(context.getRealm(), context.getUser(), + UserCredentialModel.otp(context.getRealm().getOTPPolicy().getType(), password)); if (!valid) { context.getEvent().user(context.getUser()) .error(Errors.INVALID_USER_CREDENTIALS); @@ -91,7 +90,7 @@ protected Response challenge(AuthenticationFlowContext context, String error) { @Override public boolean configuredFor(KeycloakSession session, RealmModel realm, UserModel user) { - return session.users().configuredForCredentialType(realm.getOTPPolicy().getType(), realm, user); + return session.userCredentialManager().isConfiguredFor(realm, user, realm.getOTPPolicy().getType()); } @Override diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/directgrant/ValidateOTP.java b/services/src/main/java/org/keycloak/authentication/authenticators/directgrant/ValidateOTP.java index cb91956c2e2c..fd3736cf6a6e 100755 --- a/services/src/main/java/org/keycloak/authentication/authenticators/directgrant/ValidateOTP.java +++ b/services/src/main/java/org/keycloak/authentication/authenticators/directgrant/ValidateOTP.java @@ -65,8 +65,7 @@ public void authenticate(AuthenticationFlowContext context) { context.failure(AuthenticationFlowError.INVALID_USER, challengeResponse); return; } - credentials.add(UserCredentialModel.otp(context.getRealm().getOTPPolicy().getType(), otp)); - boolean valid = context.getSession().users().validCredentials(context.getSession(), context.getRealm(), context.getUser(), credentials); + boolean valid = context.getSession().userCredentialManager().isValid(context.getRealm(), context.getUser(), UserCredentialModel.otp(context.getRealm().getOTPPolicy().getType(), otp)); if (!valid) { context.getEvent().user(context.getUser()); context.getEvent().error(Errors.INVALID_USER_CREDENTIALS); @@ -89,7 +88,7 @@ public boolean configuredFor(KeycloakSession session, RealmModel realm, UserMode } private boolean isConfigured(KeycloakSession session, RealmModel realm, UserModel user) { - return session.users().configuredForCredentialType(realm.getOTPPolicy().getType(), realm, user); + return session.userCredentialManager().isConfiguredFor(realm, user, realm.getOTPPolicy().getType()); } @Override diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/directgrant/ValidatePassword.java b/services/src/main/java/org/keycloak/authentication/authenticators/directgrant/ValidatePassword.java index fd9af689f406..033972b9022f 100755 --- a/services/src/main/java/org/keycloak/authentication/authenticators/directgrant/ValidatePassword.java +++ b/services/src/main/java/org/keycloak/authentication/authenticators/directgrant/ValidatePassword.java @@ -46,8 +46,7 @@ public void authenticate(AuthenticationFlowContext context) { MultivaluedMap inputData = context.getHttpRequest().getDecodedFormParameters(); List credentials = new LinkedList<>(); String password = inputData.getFirst(CredentialRepresentation.PASSWORD); - credentials.add(UserCredentialModel.password(password)); - boolean valid = context.getSession().users().validCredentials(context.getSession(), context.getRealm(), context.getUser(), credentials); + boolean valid = context.getSession().userCredentialManager().isValid(context.getRealm(), context.getUser(), UserCredentialModel.password(password)); if (!valid) { context.getEvent().user(context.getUser()); context.getEvent().error(Errors.INVALID_USER_CREDENTIALS); diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/resetcred/ResetOTP.java b/services/src/main/java/org/keycloak/authentication/authenticators/resetcred/ResetOTP.java index b3b0200677e9..40c703b988b4 100755 --- a/services/src/main/java/org/keycloak/authentication/authenticators/resetcred/ResetOTP.java +++ b/services/src/main/java/org/keycloak/authentication/authenticators/resetcred/ResetOTP.java @@ -39,7 +39,7 @@ public void authenticate(AuthenticationFlowContext context) { } protected boolean configuredFor(AuthenticationFlowContext context) { - return context.getSession().users().configuredForCredentialType(context.getRealm().getOTPPolicy().getType(), context.getRealm(), context.getUser()); + return context.getSession().userCredentialManager().isConfiguredFor(context.getRealm(), context.getUser(), context.getRealm().getOTPPolicy().getType()); } @Override diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/resetcred/ResetPassword.java b/services/src/main/java/org/keycloak/authentication/authenticators/resetcred/ResetPassword.java index 795c4ecbbedb..5ec467d95e03 100755 --- a/services/src/main/java/org/keycloak/authentication/authenticators/resetcred/ResetPassword.java +++ b/services/src/main/java/org/keycloak/authentication/authenticators/resetcred/ResetPassword.java @@ -40,7 +40,7 @@ public void authenticate(AuthenticationFlowContext context) { } protected boolean configuredFor(AuthenticationFlowContext context) { - return context.getSession().users().configuredForCredentialType(UserCredentialModel.PASSWORD, context.getRealm(), context.getUser()); + return context.getSession().userCredentialManager().isConfiguredFor(context.getRealm(), context.getUser(), UserCredentialModel.PASSWORD); } @Override diff --git a/services/src/main/java/org/keycloak/authentication/forms/RegistrationPassword.java b/services/src/main/java/org/keycloak/authentication/forms/RegistrationPassword.java index a42a0735eb1a..d44e3944f9a4 100755 --- a/services/src/main/java/org/keycloak/authentication/forms/RegistrationPassword.java +++ b/services/src/main/java/org/keycloak/authentication/forms/RegistrationPassword.java @@ -97,7 +97,7 @@ public void success(FormContext context) { credentials.setValue(password); UserModel user = context.getUser(); try { - context.getSession().users().updateCredential(context.getRealm(), user, UserCredentialModel.password(formData.getFirst("password"))); + context.getSession().userCredentialManager().updateCredential(context.getRealm(), user, UserCredentialModel.password(formData.getFirst("password"))); } catch (Exception me) { user.addRequiredAction(UserModel.RequiredAction.UPDATE_PASSWORD); } diff --git a/services/src/main/java/org/keycloak/authentication/requiredactions/UpdatePassword.java b/services/src/main/java/org/keycloak/authentication/requiredactions/UpdatePassword.java index 7918815eece1..9393d3eefab8 100755 --- a/services/src/main/java/org/keycloak/authentication/requiredactions/UpdatePassword.java +++ b/services/src/main/java/org/keycloak/authentication/requiredactions/UpdatePassword.java @@ -21,6 +21,10 @@ import org.keycloak.authentication.RequiredActionContext; import org.keycloak.authentication.RequiredActionFactory; import org.keycloak.authentication.RequiredActionProvider; +import org.keycloak.credential.CredentialModel; +import org.keycloak.credential.CredentialProvider; +import org.keycloak.credential.PasswordCredentialProvider; +import org.keycloak.credential.PasswordCredentialProviderFactory; import org.keycloak.events.Details; import org.keycloak.events.Errors; import org.keycloak.events.EventBuilder; @@ -50,22 +54,20 @@ public class UpdatePassword implements RequiredActionProvider, RequiredActionFac public void evaluateTriggers(RequiredActionContext context) { int daysToExpirePassword = context.getRealm().getPasswordPolicy().getDaysToExpirePassword(); if(daysToExpirePassword != -1) { - for (UserCredentialValueModel entity : context.getUser().getCredentialsDirectly()) { - if (entity.getType().equals(UserCredentialModel.PASSWORD)) { - - if(entity.getCreatedDate() == null) { + PasswordCredentialProvider passwordProvider = (PasswordCredentialProvider)context.getSession().getProvider(CredentialProvider.class, PasswordCredentialProviderFactory.PROVIDER_ID); + CredentialModel password = passwordProvider.getPassword(context.getRealm(), context.getUser()); + if (password != null) { + if(password.getCreatedDate() == null) { + context.getUser().addRequiredAction(UserModel.RequiredAction.UPDATE_PASSWORD); + logger.debug("User is required to update password"); + } else { + long timeElapsed = Time.toMillis(Time.currentTime()) - password.getCreatedDate(); + long timeToExpire = TimeUnit.DAYS.toMillis(daysToExpirePassword); + + if(timeElapsed > timeToExpire) { context.getUser().addRequiredAction(UserModel.RequiredAction.UPDATE_PASSWORD); logger.debug("User is required to update password"); - } else { - long timeElapsed = Time.toMillis(Time.currentTime()) - entity.getCreatedDate(); - long timeToExpire = TimeUnit.DAYS.toMillis(daysToExpirePassword); - - if(timeElapsed > timeToExpire) { - context.getUser().addRequiredAction(UserModel.RequiredAction.UPDATE_PASSWORD); - logger.debug("User is required to update password"); - } } - break; } } } @@ -107,7 +109,7 @@ public void processAction(RequiredActionContext context) { } try { - context.getSession().users().updateCredential(context.getRealm(), context.getUser(), UserCredentialModel.password(passwordNew)); + context.getSession().userCredentialManager().updateCredential(context.getRealm(), context.getUser(), UserCredentialModel.password(passwordNew)); context.success(); } catch (ModelException me) { errorEvent.detail(Details.REASON, me.getMessage()).error(Errors.PASSWORD_REJECTED); diff --git a/services/src/main/java/org/keycloak/authentication/requiredactions/UpdateTotp.java b/services/src/main/java/org/keycloak/authentication/requiredactions/UpdateTotp.java index 9ba9922ac3d3..f576805c0f31 100644 --- a/services/src/main/java/org/keycloak/authentication/requiredactions/UpdateTotp.java +++ b/services/src/main/java/org/keycloak/authentication/requiredactions/UpdateTotp.java @@ -77,14 +77,14 @@ public void processAction(RequiredActionContext context) { UserCredentialModel credentials = new UserCredentialModel(); credentials.setType(context.getRealm().getOTPPolicy().getType()); credentials.setValue(totpSecret); - context.getSession().users().updateCredential(context.getRealm(), context.getUser(), credentials); + context.getSession().userCredentialManager().updateCredential(context.getRealm(), context.getUser(), credentials); // if type is HOTP, to update counter we execute validation based on supplied token UserCredentialModel cred = new UserCredentialModel(); cred.setType(context.getRealm().getOTPPolicy().getType()); cred.setValue(totp); - context.getSession().users().validCredentials(context.getSession(), context.getRealm(), context.getUser(), cred); + context.getSession().userCredentialManager().isValid(context.getRealm(), context.getUser(), cred); context.getUser().setOtpEnabled(true); context.success(); diff --git a/services/src/main/java/org/keycloak/credential/LocalOTPCredentialManager.java b/services/src/main/java/org/keycloak/credential/OTPCredentialProvider.java similarity index 93% rename from services/src/main/java/org/keycloak/credential/LocalOTPCredentialManager.java rename to services/src/main/java/org/keycloak/credential/OTPCredentialProvider.java index 6d9baf03459e..48875b681c9c 100644 --- a/services/src/main/java/org/keycloak/credential/LocalOTPCredentialManager.java +++ b/services/src/main/java/org/keycloak/credential/OTPCredentialProvider.java @@ -17,6 +17,7 @@ package org.keycloak.credential; import org.jboss.logging.Logger; +import org.keycloak.common.util.Time; import org.keycloak.models.KeycloakSession; import org.keycloak.models.OTPPolicy; import org.keycloak.models.RealmModel; @@ -34,15 +35,15 @@ * @author Bill Burke * @version $Revision: 1 $ */ -public class LocalOTPCredentialManager implements CredentialInputValidator, CredentialInputUpdater, OnUserCache { - private static final Logger logger = Logger.getLogger(LocalOTPCredentialManager.class); +public class OTPCredentialProvider implements CredentialProvider, CredentialInputValidator, CredentialInputUpdater, OnUserCache { + private static final Logger logger = Logger.getLogger(OTPCredentialProvider.class); protected KeycloakSession session; protected List getCachedCredentials(UserModel user, String type) { if (!(user instanceof CachedUserModel)) return Collections.EMPTY_LIST; CachedUserModel cached = (CachedUserModel)user; - List rtn = (List)cached.getCachedWith().get(LocalOTPCredentialManager.class.getName() + "." + type); + List rtn = (List)cached.getCachedWith().get(OTPCredentialProvider.class.getName() + "." + type); if (rtn == null) return Collections.EMPTY_LIST; return rtn; } @@ -54,11 +55,11 @@ protected UserCredentialStore getCredentialStore() { @Override public void onCache(RealmModel realm, CachedUserModel user) { List creds = getCredentialStore().getStoredCredentialsByType(realm, user, CredentialModel.TOTP); - user.getCachedWith().put(LocalOTPCredentialManager.class.getName() + "." + CredentialModel.TOTP, creds); + user.getCachedWith().put(OTPCredentialProvider.class.getName() + "." + CredentialModel.TOTP, creds); } - public LocalOTPCredentialManager(KeycloakSession session) { + public OTPCredentialProvider(KeycloakSession session) { this.session = session; } @@ -88,10 +89,11 @@ public boolean updateCredential(RealmModel realm, UserModel user, CredentialInpu model.setDigits(policy.getDigits()); model.setCounter(policy.getInitialCounter()); model.setAlgorithm(policy.getAlgorithm()); - model.setType(policy.getType()); + model.setType(input.getType()); model.setValue(inputModel.getValue()); model.setDevice(inputModel.getDevice()); model.setPeriod(policy.getPeriod()); + model.setCreatedDate(Time.toMillis(Time.currentTime())); if (model.getId() == null) { getCredentialStore().createCredential(realm, user, model); } else { diff --git a/services/src/main/java/org/keycloak/credential/OTPCredentialProviderFactory.java b/services/src/main/java/org/keycloak/credential/OTPCredentialProviderFactory.java new file mode 100644 index 000000000000..c4c6cd5edde4 --- /dev/null +++ b/services/src/main/java/org/keycloak/credential/OTPCredentialProviderFactory.java @@ -0,0 +1,37 @@ +/* + * Copyright 2016 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.credential; + +import org.keycloak.models.KeycloakSession; + +/** + * @author Bill Burke + * @version $Revision: 1 $ + */ +public class OTPCredentialProviderFactory implements CredentialProviderFactory { + public static final String PROVIDER_ID="keycloak-otp"; + @Override + public OTPCredentialProvider create(KeycloakSession session) { + return new OTPCredentialProvider(session); + } + + @Override + public String getId() { + return PROVIDER_ID; + } + +} diff --git a/services/src/main/java/org/keycloak/credential/PasswordCredentialProvider.java b/services/src/main/java/org/keycloak/credential/PasswordCredentialProvider.java new file mode 100644 index 000000000000..8b94eb3c655b --- /dev/null +++ b/services/src/main/java/org/keycloak/credential/PasswordCredentialProvider.java @@ -0,0 +1,216 @@ +/* + * Copyright 2016 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.credential; + +import org.jboss.logging.Logger; +import org.keycloak.common.util.Time; +import org.keycloak.credential.hash.PasswordHashProvider; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.ModelException; +import org.keycloak.models.PasswordPolicy; +import org.keycloak.models.RealmModel; +import org.keycloak.models.UserCredentialModel; +import org.keycloak.models.UserModel; +import org.keycloak.models.cache.CachedUserModel; +import org.keycloak.models.cache.OnUserCache; +import org.keycloak.policy.HashAlgorithmPasswordPolicyProviderFactory; +import org.keycloak.policy.PasswordPolicyManagerProvider; +import org.keycloak.policy.PolicyError; + +import java.util.Collections; +import java.util.Comparator; +import java.util.LinkedList; +import java.util.List; + +/** + * @author Bill Burke + * @version $Revision: 1 $ + */ +public class PasswordCredentialProvider implements CredentialProvider, CredentialInputValidator, CredentialInputUpdater, OnUserCache { + + public static final String PASSWORD_CACHE_KEY = PasswordCredentialProvider.class.getName() + "." + CredentialModel.PASSWORD; + private static final Logger logger = Logger.getLogger(PasswordCredentialProvider.class); + + protected KeycloakSession session; + + public PasswordCredentialProvider(KeycloakSession session) { + this.session = session; + } + + protected UserCredentialStore getCredentialStore() { + return session.userCredentialManager(); + } + + public CredentialModel getPassword(RealmModel realm, UserModel user) { + List passwords = null; + if (user instanceof CachedUserModel) { + CachedUserModel cached = (CachedUserModel)user; + passwords = (List)cached.getCachedWith().get(PASSWORD_CACHE_KEY); + + } else { + passwords = getCredentialStore().getStoredCredentialsByType(realm, user, CredentialModel.PASSWORD); + } + if (passwords == null || passwords.isEmpty()) return null; + return passwords.get(0); + } + + + @Override + public boolean updateCredential(RealmModel realm, UserModel user, CredentialInput input) { + if (!supportsCredentialType(input.getType())) return false; + + if (!(input instanceof UserCredentialModel)) { + logger.debug("Expected instance of UserCredentialModel for CredentialInput"); + return false; + } + UserCredentialModel cred = (UserCredentialModel)input; + PasswordPolicy policy = realm.getPasswordPolicy(); + + PolicyError error = session.getProvider(PasswordPolicyManagerProvider.class).validate(realm, user, cred.getValue()); + if (error != null) throw new ModelException(error.getMessage(), error.getParameters()); + + + PasswordHashProvider hash = getHashProvider(policy); + if (hash == null) { + return false; + } + CredentialModel oldPassword = getPassword(realm, user); + + expirePassword(realm, user, policy); + CredentialModel newPassword = new CredentialModel(); + newPassword.setType(CredentialModel.PASSWORD); + long createdDate = Time.toMillis(Time.currentTime()); + newPassword.setCreatedDate(createdDate); + hash.encode(cred.getValue(), policy, newPassword); + getCredentialStore().createCredential(realm, user, newPassword); + session.getUserCache().evict(realm, user); + return true; + } + + protected void expirePassword(RealmModel realm, UserModel user, PasswordPolicy policy) { + + CredentialModel oldPassword = getPassword(realm, user); + if (oldPassword == null) return; + int expiredPasswordsPolicyValue = policy.getExpiredPasswords(); + if (expiredPasswordsPolicyValue > -1) { + List list = getCredentialStore().getStoredCredentialsByType(realm, user, CredentialModel.PASSWORD_HISTORY); + List history = new LinkedList<>(); + history.addAll(list); + if (history.size() + 1 >= expiredPasswordsPolicyValue) { + Collections.sort(history, new Comparator() { + @Override + public int compare(CredentialModel o1, CredentialModel o2) { + long o1Date = o1.getCreatedDate() == null ? 0 : o1.getCreatedDate().longValue(); + long o2Date = o2.getCreatedDate() == null ? 0 : o2.getCreatedDate().longValue(); + if (o1Date > o2Date) return 1; + else if (o1Date < o2Date) return -1; + else return 0; + } + }); + for (int i = 0; i < history.size() + 2 - expiredPasswordsPolicyValue; i++) { + getCredentialStore().removeStoredCredential(realm, user, history.get(i).getId()); + } + + } + oldPassword.setType(CredentialModel.PASSWORD_HISTORY); + getCredentialStore().updateCredential(realm, user, oldPassword); + } else { + session.userCredentialManager().removeStoredCredential(realm, user, oldPassword.getId()); + } + + } + + protected PasswordHashProvider getHashProvider(PasswordPolicy policy) { + PasswordHashProvider hash = session.getProvider(PasswordHashProvider.class, policy.getHashAlgorithm()); + if (hash == null) { + logger.warnv("Realm PasswordPolicy PasswordHashProvider {0} not found", policy.getHashAlgorithm()); + return session.getProvider(PasswordHashProvider.class, HashAlgorithmPasswordPolicyProviderFactory.DEFAULT_VALUE); + } + return hash; + } + + @Override + public void disableCredentialType(RealmModel realm, UserModel user, String credentialType) { + if (!supportsCredentialType(credentialType)) return; + PasswordPolicy policy = realm.getPasswordPolicy(); + expirePassword(realm, user, policy); + } + + @Override + public boolean supportsCredentialType(String credentialType) { + return credentialType.equals(CredentialModel.PASSWORD); + } + + @Override + public boolean isConfiguredFor(RealmModel realm, UserModel user, String credentialType) { + return getPassword(realm, user) != null; + } + + @Override + public boolean isValid(RealmModel realm, UserModel user, CredentialInput input) { + if (! (input instanceof UserCredentialModel)) { + logger.debug("Expected instance of UserCredentialModel for CredentialInput"); + return false; + + } + UserCredentialModel cred = (UserCredentialModel)input; + if (cred.getValue() == null) { + logger.debugv("Input password was null for user {0} ", user.getUsername()); + return false; + } + CredentialModel password = getPassword(realm, user); + if (password == null) { + logger.debugv("No password cached or stored for user {0} ", user.getUsername()); + return false; + } + PasswordHashProvider hash = session.getProvider(PasswordHashProvider.class, password.getAlgorithm()); + if (hash == null) { + logger.debugv("PasswordHashProvider {0} not found for user {1} ", password.getAlgorithm(), user.getUsername()); + return false; + } + if (!hash.verify(cred.getValue(), password)) { + logger.debugv("Failed password validation for user {0} ", user.getUsername()); + return false; + } + PasswordPolicy policy = realm.getPasswordPolicy(); + if (policy == null) { + return true; + } + hash = getHashProvider(policy); + if (hash == null) { + return true; + } + if (hash.policyCheck(policy, password)) { + return true; + } + + hash.encode(cred.getValue(), policy, password); + getCredentialStore().updateCredential(realm, user, password); + session.getUserCache().evict(realm, user); + + return true; + } + + @Override + public void onCache(RealmModel realm, CachedUserModel user) { + List passwords = getCredentialStore().getStoredCredentialsByType(realm, user, CredentialModel.PASSWORD); + if (passwords != null) { + user.getCachedWith().put(PASSWORD_CACHE_KEY, passwords); + } + + } +} diff --git a/services/src/main/java/org/keycloak/credential/PasswordCredentialProviderFactory.java b/services/src/main/java/org/keycloak/credential/PasswordCredentialProviderFactory.java new file mode 100644 index 000000000000..b9aace8c579b --- /dev/null +++ b/services/src/main/java/org/keycloak/credential/PasswordCredentialProviderFactory.java @@ -0,0 +1,37 @@ +/* + * Copyright 2016 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.credential; + +import org.keycloak.models.KeycloakSession; + +/** + * @author Bill Burke + * @version $Revision: 1 $ + */ +public class PasswordCredentialProviderFactory implements CredentialProviderFactory { + public static final String PROVIDER_ID="keycloak-password"; + @Override + public PasswordCredentialProvider create(KeycloakSession session) { + return new PasswordCredentialProvider(session); + } + + @Override + public String getId() { + return PROVIDER_ID; + } + +} diff --git a/services/src/main/java/org/keycloak/credential/UserCredentialStoreManager.java b/services/src/main/java/org/keycloak/credential/UserCredentialStoreManager.java index 5f67768378bb..915cbe7d7c86 100644 --- a/services/src/main/java/org/keycloak/credential/UserCredentialStoreManager.java +++ b/services/src/main/java/org/keycloak/credential/UserCredentialStoreManager.java @@ -22,17 +22,24 @@ import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.models.UserCredentialManager; +import org.keycloak.models.UserCredentialModel; +import org.keycloak.models.UserFederationProvider; +import org.keycloak.models.UserFederationProviderModel; import org.keycloak.models.UserModel; import org.keycloak.models.cache.CachedUserModel; import org.keycloak.models.cache.OnUserCache; +import org.keycloak.models.utils.KeycloakModelUtils; +import org.keycloak.provider.ProviderFactory; import org.keycloak.storage.StorageId; import org.keycloak.storage.UserStorageManager; import org.keycloak.storage.UserStorageProvider; +import java.util.Arrays; import java.util.Collections; import java.util.Iterator; import java.util.LinkedList; import java.util.List; +import java.util.Set; /** * @author Bill Burke @@ -89,6 +96,10 @@ public CredentialModel getStoredCredentialByNameAndType(RealmModel realm, UserMo return getStoreForUser(user).getStoredCredentialByNameAndType(realm, user, name, type); } + @Override + public boolean isValid(RealmModel realm, UserModel user, CredentialInput... inputs) { + return isValid(realm, user, Arrays.asList(inputs)); + } @Override public boolean isValid(RealmModel realm, UserModel user, List inputs) { @@ -108,38 +119,47 @@ public boolean isValid(RealmModel realm, UserModel user, List i } } } + } else { + // + UserFederationProvider link = session.users().getFederationLink(realm, user); + if (link != null) { + session.users().validateUser(realm, user); + Iterator it = toValidate.iterator(); + while (it.hasNext()) { + CredentialInput input = it.next(); + if (link.supportsCredentialType(input.getType()) + && link.isValid(realm, user, input)) { + it.remove(); + } + } + } + // } if (toValidate.isEmpty()) return true; - List components = getCredentialProviderComponents(realm); - for (ComponentModel component : components) { - CredentialProviderFactory factory = (CredentialProviderFactory) session.getKeycloakSessionFactory().getProviderFactory(CredentialProvider.class, component.getProviderId()); - if (!Types.supports(CredentialInputValidator.class, factory, CredentialProviderFactory.class)) continue; + List credentialProviders = getCredentialProviders(realm, CredentialInputValidator.class); + for (CredentialInputValidator validator : credentialProviders) { Iterator it = toValidate.iterator(); while (it.hasNext()) { CredentialInput input = it.next(); - CredentialInputValidator validator = (CredentialInputValidator)session.getAttribute(component.getId()); - if (validator == null) { - validator = (CredentialInputValidator)factory.create(session, component); - session.setAttribute(component.getId(), validator); - } if (validator.supportsCredentialType(input.getType()) && validator.isValid(realm, user, input)) { it.remove(); } } - } + } return toValidate.isEmpty(); } - protected List getCredentialProviderComponents(RealmModel realm) { - List components = realm.getComponents(realm.getId(), CredentialProvider.class.getName()); - if (components.isEmpty()) return components; - List copy = new LinkedList<>(); - copy.addAll(components); - Collections.sort(copy, PrioritizedComponentModel.comparator); - return copy; + protected List getCredentialProviders(RealmModel realm, Class type) { + List list = new LinkedList(); + for (ProviderFactory f : session.getKeycloakSessionFactory().getProviderFactories(CredentialProvider.class)) { + if (!Types.supports(CredentialInputUpdater.class, f, CredentialProviderFactory.class)) continue; + list.add((T)session.getProvider(CredentialProvider.class, f.getId())); + } + return list; + } @Override @@ -153,21 +173,21 @@ public void updateCredential(RealmModel realm, UserModel user, CredentialInput i if (updater.updateCredential(realm, user, input)) return; } } + } else { + // + UserFederationProvider link = session.users().getFederationLink(realm, user); + if (link != null) { + if (link.updateCredential(realm, user, input)) return; + } + // } - List components = getCredentialProviderComponents(realm); - for (ComponentModel component : components) { - CredentialProviderFactory factory = (CredentialProviderFactory) session.getKeycloakSessionFactory().getProviderFactory(CredentialProvider.class, component.getProviderId()); - if (!Types.supports(CredentialInputUpdater.class, factory, CredentialProviderFactory.class)) continue; - CredentialInputUpdater updater = (CredentialInputUpdater)session.getAttribute(component.getId()); - if (updater == null) { - updater = (CredentialInputUpdater)factory.create(session, component); - session.setAttribute(component.getId(), updater); - } + List credentialProviders = getCredentialProviders(realm, CredentialInputUpdater.class); + for (CredentialInputUpdater updater : credentialProviders) { if (!updater.supportsCredentialType(input.getType())) continue; if (updater.updateCredential(realm, user, input)) return; - } + } } @Override public void disableCredential(RealmModel realm, UserModel user, String credentialType) { @@ -180,21 +200,22 @@ public void disableCredential(RealmModel realm, UserModel user, String credentia updater.disableCredentialType(realm, user, credentialType); } } + } else { + UserFederationProvider link = session.users().getFederationLink(realm, user); + if (link != null && link.getSupportedCredentialTypes().contains(credentialType)) { + link.disableCredentialType(realm, user, credentialType); + } + } - List components = getCredentialProviderComponents(realm); - for (ComponentModel component : components) { - CredentialProviderFactory factory = (CredentialProviderFactory) session.getKeycloakSessionFactory().getProviderFactory(CredentialProvider.class, component.getProviderId()); - if (!Types.supports(CredentialInputUpdater.class, factory, CredentialProviderFactory.class)) continue; - CredentialInputUpdater updater = (CredentialInputUpdater)session.getAttribute(component.getId()); - if (updater == null) { - updater = (CredentialInputUpdater)factory.create(session, component); - session.setAttribute(component.getId(), updater); - } + List credentialProviders = getCredentialProviders(realm, CredentialInputUpdater.class); + for (CredentialInputUpdater updater : credentialProviders) { if (!updater.supportsCredentialType(credentialType)) continue; updater.disableCredentialType(realm, user, credentialType); + } + } @Override public boolean isConfiguredFor(RealmModel realm, UserModel user, String type) { @@ -207,39 +228,37 @@ public boolean isConfiguredFor(RealmModel realm, UserModel user, String type) { return true; } } + } else { + // + UserFederationProvider link = session.users().getFederationLink(realm, user); + if (link != null) { + if (link.isConfiguredFor(realm, user, type)) return true; + } + // + } - List components = getCredentialProviderComponents(realm); - for (ComponentModel component : components) { - CredentialProviderFactory factory = (CredentialProviderFactory) session.getKeycloakSessionFactory().getProviderFactory(CredentialProvider.class, component.getProviderId()); - if (!Types.supports(CredentialInputUpdater.class, factory, CredentialProviderFactory.class)) continue; - CredentialInputValidator validator = (CredentialInputValidator)session.getAttribute(component.getId()); - if (validator == null) { - validator = (CredentialInputValidator)factory.create(session, component); - session.setAttribute(component.getId(), validator); - } + return isConfiguredLocally(realm, user, type); + } + + @Override + public boolean isConfiguredLocally(RealmModel realm, UserModel user, String type) { + List credentialProviders = getCredentialProviders(realm, CredentialInputValidator.class); + for (CredentialInputValidator validator : credentialProviders) { if (validator.supportsCredentialType(type) && validator.isConfiguredFor(realm, user, type)) { return true; } + } return false; - } @Override public void onCache(RealmModel realm, CachedUserModel user) { - List components = getCredentialProviderComponents(realm); - for (ComponentModel component : components) { - CredentialProviderFactory factory = (CredentialProviderFactory) session.getKeycloakSessionFactory().getProviderFactory(CredentialProvider.class, component.getProviderId()); - if (!Types.supports(OnUserCache.class, factory, CredentialProviderFactory.class)) continue; - OnUserCache validator = (OnUserCache)session.getAttribute(component.getId()); - if (validator == null) { - validator = (OnUserCache)factory.create(session, component); - session.setAttribute(component.getId(), validator); - } + List credentialProviders = getCredentialProviders(realm, OnUserCache.class); + for (OnUserCache validator : credentialProviders) { validator.onCache(realm, user); } - } @Override diff --git a/services/src/main/java/org/keycloak/credential/hash/Pbkdf2PasswordHashProvider.java b/services/src/main/java/org/keycloak/credential/hash/Pbkdf2PasswordHashProvider.java new file mode 100644 index 000000000000..b477c9c5c4b1 --- /dev/null +++ b/services/src/main/java/org/keycloak/credential/hash/Pbkdf2PasswordHashProvider.java @@ -0,0 +1,138 @@ +/* + * Copyright 2016 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.credential.hash; + +import org.keycloak.Config; +import org.keycloak.common.util.Base64; +import org.keycloak.credential.CredentialModel; +import org.keycloak.credential.hash.PasswordHashProvider; +import org.keycloak.credential.hash.PasswordHashProviderFactory; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.KeycloakSessionFactory; +import org.keycloak.models.PasswordPolicy; +import org.keycloak.models.UserCredentialModel; +import org.keycloak.models.UserCredentialValueModel; + +import javax.crypto.SecretKeyFactory; +import javax.crypto.spec.PBEKeySpec; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.KeySpec; + +/** + * @author Kunal Kerkar + */ +public class Pbkdf2PasswordHashProvider implements PasswordHashProviderFactory, PasswordHashProvider { + + public static final String ID = "pbkdf2"; + + private static final String PBKDF2_ALGORITHM = "PBKDF2WithHmacSHA1"; + private static final int DERIVED_KEY_SIZE = 512; + + public UserCredentialValueModel encode(String rawPassword, int iterations) { + byte[] salt = getSalt(); + String encodedPassword = encode(rawPassword, iterations, salt); + + UserCredentialValueModel credentials = new UserCredentialValueModel(); + credentials.setAlgorithm(ID); + credentials.setType(UserCredentialModel.PASSWORD); + credentials.setSalt(salt); + credentials.setHashIterations(iterations); + credentials.setValue(encodedPassword); + return credentials; + } + + public boolean verify(String rawPassword, UserCredentialValueModel credential) { + return encode(rawPassword, credential.getHashIterations(), credential.getSalt()).equals(credential.getValue()); + } + + @Override + public boolean policyCheck(PasswordPolicy policy, CredentialModel credential) { + return credential.getHashIterations() == policy.getHashIterations() && PBKDF2_ALGORITHM.equals(credential.getAlgorithm()); + } + + @Override + public void encode(String rawPassword, PasswordPolicy policy, CredentialModel credential) { + byte[] salt = getSalt(); + String encodedPassword = encode(rawPassword, policy.getHashIterations(), salt); + + credential.setAlgorithm(ID); + credential.setType(UserCredentialModel.PASSWORD); + credential.setSalt(salt); + credential.setHashIterations(policy.getHashIterations()); + credential.setValue(encodedPassword); + + } + + @Override + public boolean verify(String rawPassword, CredentialModel credential) { + return encode(rawPassword, credential.getHashIterations(), credential.getSalt()).equals(credential.getValue()); + } + + @Override + public PasswordHashProvider create(KeycloakSession session) { + return this; + } + + @Override + public void init(Config.Scope config) { + } + + @Override + public void postInit(KeycloakSessionFactory factory) { + } + + public void close() { + } + + @Override + public String getId() { + return ID; + } + + private String encode(String rawPassword, int iterations, byte[] salt) { + KeySpec spec = new PBEKeySpec(rawPassword.toCharArray(), salt, iterations, DERIVED_KEY_SIZE); + + try { + byte[] key = getSecretKeyFactory().generateSecret(spec).getEncoded(); + return Base64.encodeBytes(key); + } catch (InvalidKeySpecException e) { + throw new RuntimeException("Credential could not be encoded", e); + } catch (Exception e) { + e.printStackTrace(); + throw new RuntimeException(e); + } + } + + private byte[] getSalt() { + byte[] buffer = new byte[16]; + SecureRandom secureRandom = new SecureRandom(); + secureRandom.nextBytes(buffer); + return buffer; + } + + private SecretKeyFactory getSecretKeyFactory() { + try { + return SecretKeyFactory.getInstance(PBKDF2_ALGORITHM); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException("PBKDF2 algorithm not found", e); + } + } + +} diff --git a/services/src/main/java/org/keycloak/exportimport/util/ExportUtils.java b/services/src/main/java/org/keycloak/exportimport/util/ExportUtils.java index 1b2caf946e33..5ea3fb802375 100755 --- a/services/src/main/java/org/keycloak/exportimport/util/ExportUtils.java +++ b/services/src/main/java/org/keycloak/exportimport/util/ExportUtils.java @@ -34,6 +34,7 @@ import org.keycloak.common.util.Base64; import org.keycloak.common.util.MultivaluedHashMap; import org.keycloak.component.ComponentModel; +import org.keycloak.credential.CredentialModel; import org.keycloak.models.ClientModel; import org.keycloak.models.ClientTemplateModel; import org.keycloak.models.FederatedIdentityModel; @@ -499,9 +500,9 @@ public static UserRepresentation exportUser(KeycloakSession session, RealmModel } // Credentials - List creds = user.getCredentialsDirectly(); + List creds = session.userCredentialManager().getStoredCredentials(realm, user); List credReps = new ArrayList(); - for (UserCredentialValueModel cred : creds) { + for (CredentialModel cred : creds) { CredentialRepresentation credRep = exportCredential(cred); credReps.add(credRep); } @@ -545,7 +546,7 @@ public static FederatedIdentityRepresentation exportSocialLink(FederatedIdentity return socialLinkRep; } - public static CredentialRepresentation exportCredential(UserCredentialValueModel userCred) { + public static CredentialRepresentation exportCredential(CredentialModel userCred) { CredentialRepresentation credRep = new CredentialRepresentation(); credRep.setType(userCred.getType()); credRep.setDevice(userCred.getDevice()); @@ -556,6 +557,7 @@ public static CredentialRepresentation exportCredential(UserCredentialValueModel credRep.setAlgorithm(userCred.getAlgorithm()); credRep.setDigits(userCred.getDigits()); credRep.setCreatedDate(userCred.getCreatedDate()); + credRep.setConfig(userCred.getConfig()); return credRep; } diff --git a/services/src/main/java/org/keycloak/forms/account/freemarker/model/TotpBean.java b/services/src/main/java/org/keycloak/forms/account/freemarker/model/TotpBean.java index 197f98558eab..d90f21e89c43 100644 --- a/services/src/main/java/org/keycloak/forms/account/freemarker/model/TotpBean.java +++ b/services/src/main/java/org/keycloak/forms/account/freemarker/model/TotpBean.java @@ -41,7 +41,7 @@ public class TotpBean { private final boolean enabled; public TotpBean(KeycloakSession session, RealmModel realm, UserModel user) { - this.enabled = session.users().configuredForCredentialType(realm.getOTPPolicy().getType(), realm, user); + this.enabled = session.userCredentialManager().isConfiguredFor(realm, user, realm.getOTPPolicy().getType()); this.totpSecret = HmacOTP.generateSecret(20); this.totpSecretEncoded = TotpUtils.encode(totpSecret); diff --git a/services/src/main/java/org/keycloak/protocol/saml/profile/ecp/authenticator/HttpBasicAuthenticator.java b/services/src/main/java/org/keycloak/protocol/saml/profile/ecp/authenticator/HttpBasicAuthenticator.java index 7daf470e4a66..d1404c2b95e0 100755 --- a/services/src/main/java/org/keycloak/protocol/saml/profile/ecp/authenticator/HttpBasicAuthenticator.java +++ b/services/src/main/java/org/keycloak/protocol/saml/profile/ecp/authenticator/HttpBasicAuthenticator.java @@ -97,7 +97,7 @@ public void authenticate(AuthenticationFlowContext context) { if (user != null) { String password = usernameAndPassword[1]; - boolean valid = context.getSession().users().validCredentials(context.getSession(), realm, user, UserCredentialModel.password(password)); + boolean valid = context.getSession().userCredentialManager().isValid(realm, user, UserCredentialModel.password(password)); if (valid) { context.getClientSession().setAuthenticatedUser(user); diff --git a/services/src/main/java/org/keycloak/services/ServicesLogger.java b/services/src/main/java/org/keycloak/services/ServicesLogger.java index af6c4c13e55d..c6094c4f86ef 100644 --- a/services/src/main/java/org/keycloak/services/ServicesLogger.java +++ b/services/src/main/java/org/keycloak/services/ServicesLogger.java @@ -97,7 +97,7 @@ public interface ServicesLogger extends BasicLogger { @Message(id=12, value="Failed to delete '%s'") void failedToDeleteFile(String fileName); - @LogMessage(level = DEBUG) + @LogMessage(level = WARN) @Message(id=13, value="Failed authentication") void failedAuthentication(@Cause Throwable t); 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 356922e43035..9d374a2416b3 100755 --- a/services/src/main/java/org/keycloak/services/managers/ApplianceBootstrap.java +++ b/services/src/main/java/org/keycloak/services/managers/ApplianceBootstrap.java @@ -96,7 +96,7 @@ public void createMasterRealmUser(String username, String password) { UserCredentialModel usrCredModel = new UserCredentialModel(); usrCredModel.setType(UserCredentialModel.PASSWORD); usrCredModel.setValue(password); - session.users().updateCredential(realm, adminUser, usrCredModel); + session.userCredentialManager().updateCredential(realm, adminUser, usrCredModel); RoleModel adminRole = realm.getRole(AdminRoles.ADMIN); adminUser.grantRole(adminRole); 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 bf6fb885ed46..c3f099cb284b 100755 --- a/services/src/main/java/org/keycloak/services/resources/AccountService.java +++ b/services/src/main/java/org/keycloak/services/resources/AccountService.java @@ -16,6 +16,8 @@ */ package org.keycloak.services.resources; +import org.keycloak.credential.CredentialInput; +import org.keycloak.credential.CredentialModel; import org.keycloak.events.Errors; import org.keycloak.forms.account.AccountPages; import org.keycloak.forms.account.AccountProvider; @@ -81,6 +83,7 @@ import java.net.URI; import java.util.HashSet; import java.util.Iterator; +import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Set; @@ -444,7 +447,7 @@ public Response processTotpRemove(@QueryParam("stateChecker") String stateChecke csrfCheck(stateChecker); UserModel user = auth.getUser(); - user.setOtpEnabled(false); + session.userCredentialManager().disableCredential(realm, user, CredentialModel.OTP); event.event(EventType.REMOVE_TOTP).client(auth.getClient()).user(auth.getUser()).success(); @@ -564,7 +567,7 @@ public Response processTotpUpdate(final MultivaluedMap formData) UserCredentialModel credentials = new UserCredentialModel(); credentials.setType(realm.getOTPPolicy().getType()); credentials.setValue(totpSecret); - session.users().updateCredential(realm, user, credentials); + session.userCredentialManager().updateCredential(realm, user, credentials); user.setOtpEnabled(true); @@ -572,7 +575,7 @@ public Response processTotpUpdate(final MultivaluedMap formData) UserCredentialModel cred = new UserCredentialModel(); cred.setType(realm.getOTPPolicy().getType()); cred.setValue(totp); - session.users().validCredentials(session, realm, user, cred); + session.userCredentialManager().isValid(realm, user, cred); event.event(EventType.UPDATE_TOTP).client(auth.getClient()).user(auth.getUser()).success(); @@ -624,7 +627,7 @@ public Response processPasswordUpdate(final MultivaluedMap formD } UserCredentialModel cred = UserCredentialModel.password(password); - if (!session.users().validCredentials(session, realm, user, cred)) { + if (!session.userCredentialManager().isValid(realm, user, cred)) { setReferrerOnPage(); errorEvent.error(Errors.INVALID_USER_CREDENTIALS); return account.setError(Messages.INVALID_PASSWORD_EXISTING).createResponse(AccountPages.PASSWORD); @@ -644,7 +647,7 @@ public Response processPasswordUpdate(final MultivaluedMap formD } try { - session.users().updateCredential(realm, user, UserCredentialModel.password(passwordNew)); + session.userCredentialManager().updateCredential(realm, user, UserCredentialModel.password(passwordNew)); } catch (ModelReadOnlyException mre) { setReferrerOnPage(); errorEvent.error(Errors.NOT_ALLOWED); @@ -774,32 +777,7 @@ protected URI getBaseRedirectUri() { } public static boolean isPasswordSet(KeycloakSession session, RealmModel realm, UserModel user) { - boolean passwordSet = false; - - // See if password is set for user on linked UserFederationProvider - if (user.getFederationLink() != null) { - - UserFederationProvider federationProvider = null; - for (UserFederationProviderModel fedProviderModel : realm.getUserFederationProviders()) { - if (fedProviderModel.getId().equals(user.getFederationLink())) { - federationProvider = KeycloakModelUtils.getFederationProviderInstance(session, fedProviderModel); - } - } - - if (federationProvider != null) { - Set supportedCreds = federationProvider.getSupportedCredentialTypes(user); - if (supportedCreds.contains(UserCredentialModel.PASSWORD)) { - passwordSet = true; - } - } - } - - for (UserCredentialValueModel c : user.getCredentialsDirectly()) { - if (c.getType().equals(CredentialRepresentation.PASSWORD)) { - passwordSet = true; - } - } - return passwordSet; + return session.userCredentialManager().isConfiguredFor(realm, user, CredentialModel.PASSWORD); } private String[] getReferrer() { 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 68e080663bed..1e7c0b702455 100644 --- a/services/src/main/java/org/keycloak/services/resources/KeycloakApplication.java +++ b/services/src/main/java/org/keycloak/services/resources/KeycloakApplication.java @@ -405,7 +405,7 @@ public void importAddUser() { } else { UserModel user = session.users().addUser(realm, userRep.getUsername()); user.setEnabled(userRep.isEnabled()); - RepresentationToModel.createCredentials(userRep, user); + RepresentationToModel.createCredentials(userRep, session, realm, user); RepresentationToModel.createRoleMappings(userRep, user, realm); } diff --git a/services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java b/services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java index 26daf84019f7..47daa76f5ac5 100755 --- a/services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java @@ -22,6 +22,7 @@ import org.jboss.resteasy.spi.ResteasyProviderFactory; import org.keycloak.common.ClientConnection; import org.keycloak.authentication.RequiredActionProvider; +import org.keycloak.credential.CredentialModel; import org.keycloak.email.EmailException; import org.keycloak.email.EmailTemplateProvider; import org.keycloak.events.Details; @@ -746,7 +747,7 @@ public void resetPassword(@PathParam("id") String id, CredentialRepresentation p UserCredentialModel cred = RepresentationToModel.convertCredential(pass); try { - session.users().updateCredential(realm, user, cred); + session.userCredentialManager().updateCredential(realm, user, cred); } catch (IllegalStateException ise) { throw new BadRequestException("Resetting to N old passwords is not allowed."); } catch (ModelReadOnlyException mre) { @@ -777,7 +778,7 @@ public void removeTotp(@PathParam("id") String id) { throw new NotFoundException("User not found"); } - user.setOtpEnabled(false); + session.userCredentialManager().disableCredential(realm, user, CredentialModel.OTP); adminEvent.operation(OperationType.ACTION).resourcePath(uriInfo).success(); } diff --git a/services/src/main/java/org/keycloak/storage/UserStorageManager.java b/services/src/main/java/org/keycloak/storage/UserStorageManager.java index 226a083fd8fe..d68262717986 100755 --- a/services/src/main/java/org/keycloak/storage/UserStorageManager.java +++ b/services/src/main/java/org/keycloak/storage/UserStorageManager.java @@ -34,15 +34,12 @@ import org.keycloak.models.cache.OnUserCache; import org.keycloak.storage.user.UserCredentialAuthenticationProvider; import org.keycloak.models.UserCredentialModel; -import org.keycloak.storage.user.UserCredentialValidatorProvider; -import org.keycloak.models.UserCredentialValueModel; import org.keycloak.models.UserFederationProviderModel; import org.keycloak.storage.user.UserLookupProvider; import org.keycloak.models.UserModel; import org.keycloak.models.UserProvider; import org.keycloak.storage.user.UserQueryProvider; import org.keycloak.storage.user.UserRegistrationProvider; -import org.keycloak.models.utils.CredentialValidation; import org.keycloak.storage.federated.UserFederatedStorageProvider; import java.util.Arrays; @@ -532,52 +529,6 @@ public void preRemove(ProtocolMapperModel protocolMapper) { if (getFederatedStorage() != null) getFederatedStorage().preRemove(protocolMapper); } - @Override - public boolean validCredentials(KeycloakSession session, RealmModel realm, UserModel user, List input) { - if (StorageId.isLocalStorage(user)) { - return localStorage().validCredentials(session, realm, user, input); - } - // make sure we hit the cache here! - List userCreds = user.getCredentialsDirectly(); - - LinkedList toValidate = new LinkedList<>(); - toValidate.addAll(input); - Iterator it = toValidate.iterator(); - boolean failedStoredCredential = false; - // we allow for multiple credentials of same type, i.e. multiple OTP devices - while (it.hasNext()) { - UserCredentialModel cred = it.next(); - boolean credValidated = false; - for (UserCredentialValueModel userCred : userCreds) { - if (!userCred.getType().equals(cred.getType())) continue; - if (CredentialValidation.validCredential(session, realm, user, cred)) { - credValidated = true; - break; - } else { - failedStoredCredential = true; - } - } - if (credValidated) { - it.remove(); - } else if (failedStoredCredential) { - return false; - } - } - - if (toValidate.isEmpty()) return true; - - UserStorageProvider provider = getStorageProvider(session, realm, StorageId.resolveProviderId(user)); - if (!(provider instanceof UserCredentialValidatorProvider)) { - return false; - } - return ((UserCredentialValidatorProvider)provider).validCredentials(session, realm, user, toValidate); - } - - @Override - public boolean validCredentials(KeycloakSession session, RealmModel realm, UserModel user, UserCredentialModel... input) { - return validCredentials(session, realm, user, Arrays.asList(input)); - } - @Override public CredentialValidationOutput validCredentials(KeycloakSession session, RealmModel realm, UserCredentialModel... input) { List providers = getStorageProviders(session, realm, UserCredentialAuthenticationProvider.class); diff --git a/services/src/main/resources/META-INF/services/org.keycloak.credential.CredentialProviderFactory b/services/src/main/resources/META-INF/services/org.keycloak.credential.CredentialProviderFactory new file mode 100644 index 000000000000..84530ed0fd5e --- /dev/null +++ b/services/src/main/resources/META-INF/services/org.keycloak.credential.CredentialProviderFactory @@ -0,0 +1,2 @@ +org.keycloak.credential.OTPCredentialProviderFactory +org.keycloak.credential.PasswordCredentialProviderFactory \ No newline at end of file diff --git a/services/src/main/resources/META-INF/services/org.keycloak.credential.hash.PasswordHashProviderFactory b/services/src/main/resources/META-INF/services/org.keycloak.credential.hash.PasswordHashProviderFactory new file mode 100644 index 000000000000..e72e56d77d3d --- /dev/null +++ b/services/src/main/resources/META-INF/services/org.keycloak.credential.hash.PasswordHashProviderFactory @@ -0,0 +1 @@ +org.keycloak.credential.hash.Pbkdf2PasswordHashProvider \ No newline at end of file diff --git a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/federation/DummyUserFederationProvider.java b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/federation/DummyUserFederationProvider.java index 0ba4816fa8f4..5a8b6d41fee7 100644 --- a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/federation/DummyUserFederationProvider.java +++ b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/federation/DummyUserFederationProvider.java @@ -17,6 +17,8 @@ package org.keycloak.testsuite.federation; +import org.keycloak.credential.CredentialInput; +import org.keycloak.credential.CredentialModel; import org.keycloak.models.CredentialValidationOutput; import org.keycloak.models.GroupModel; import org.keycloak.models.RealmModel; @@ -105,37 +107,49 @@ public boolean isValid(RealmModel realm, UserModel local) { return users.containsKey(username); } - @Override - public Set getSupportedCredentialTypes(UserModel user) { - // Just user "test-user" is able to validate password with this federationProvider - if (user.getUsername().equals("test-user")) { - return Collections.singleton(UserCredentialModel.PASSWORD); - } else { - return Collections.emptySet(); - } - } - @Override public Set getSupportedCredentialTypes() { return Collections.singleton(UserCredentialModel.PASSWORD); } @Override - public boolean validCredentials(RealmModel realm, UserModel user, List input) { - if (user.getUsername().equals("test-user") && input.size() == 1) { - UserCredentialModel password = input.get(0); - if (password.getType().equals(UserCredentialModel.PASSWORD)) { - return "secret".equals(password.getValue()); - } - } + public boolean updateCredential(RealmModel realm, UserModel user, CredentialInput input) { + if (!(input instanceof UserCredentialModel) || !CredentialModel.PASSWORD.equals(input.getType())) return false; + return false; } @Override - public boolean validCredentials(RealmModel realm, UserModel user, UserCredentialModel... input) { - return validCredentials(realm, user, Arrays.asList(input)); + public void disableCredentialType(RealmModel realm, UserModel user, String credentialType) { + + } + + @Override + public boolean supportsCredentialType(String credentialType) { + return getSupportedCredentialTypes().contains(credentialType); } + @Override + public boolean isConfiguredFor(RealmModel realm, UserModel user, String credentialType) { + if (!CredentialModel.PASSWORD.equals(credentialType)) return false; + + if (user.getUsername().equals("test-user")) { + return true; + } else { + return false; + } + } + + @Override + public boolean isValid(RealmModel realm, UserModel user, CredentialInput input) { + if (user.getUsername().equals("test-user")) { + UserCredentialModel password = (UserCredentialModel)input; + if (password.getType().equals(UserCredentialModel.PASSWORD)) { + return "secret".equals(password.getValue()); + } + } + return false; } + @Override public CredentialValidationOutput validCredentials(RealmModel realm, UserCredentialModel credential) { return CredentialValidationOutput.failed(); diff --git a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/TestingResourceProvider.java b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/TestingResourceProvider.java index 2085cfac1414..cba8d8ec340c 100644 --- a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/TestingResourceProvider.java +++ b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/TestingResourceProvider.java @@ -578,7 +578,7 @@ public boolean validCredentials(@QueryParam("realmName") String realmName, @Quer if (realm == null) return false; UserProvider userProvider = session.getProvider(UserProvider.class); UserModel user = userProvider.getUserByUsername(userName, realm); - return userProvider.validCredentials(session, realm, user, UserCredentialModel.password(password)); + return session.userCredentialManager().isValid(realm, user, UserCredentialModel.password(password)); } @GET diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/BruteForceTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/BruteForceTest.java index 90178a9dab89..65af5a485a8c 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/BruteForceTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/BruteForceTest.java @@ -34,6 +34,7 @@ import org.keycloak.testsuite.pages.RegisterPage; import java.net.MalformedURLException; + import org.jboss.arquillian.graphene.page.Page; import org.keycloak.representations.idm.RealmRepresentation; import org.keycloak.representations.idm.UserRepresentation; @@ -202,8 +203,10 @@ public void testGrantInvalidOtp() throws Exception { events.clear(); } - } @Test - public void testGrantMissingOtp() throws Exception { + } + + @Test + public void testGrantMissingOtp() throws Exception { { String totpSecret = totp.generateTOTP("totpSecret"); OAuthClient.AccessTokenResponse response = getTestToken("password", totpSecret); @@ -337,7 +340,7 @@ public void expectTemporarilyDisabled(String username, String userId) throws Exc .error(Errors.USER_TEMPORARILY_DISABLED) .detail(Details.USERNAME, username) .removeDetail(Details.CONSENT); - if(userId != null) { + if (userId != null) { event.user(userId); } event.assertEvent(); @@ -416,12 +419,12 @@ public void loginMissingPassword() { events.clear(); } - public void registerUser(String username){ + public void registerUser(String username) { loginPage.open(); loginPage.clickRegister(); registerPage.assertCurrent(); - registerPage.register("user", "name", username + "@localhost", username, "password", "password"); + registerPage.register("user", "name", username + "@localhost", username, "password", "password"); Assert.assertNull(registerPage.getInstruction()); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/LoginHotpTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/LoginHotpTest.java index 43c6153c9141..d478f04ccb6b 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/LoginHotpTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/LoginHotpTest.java @@ -30,6 +30,8 @@ import org.keycloak.testsuite.pages.LoginTotpPage; import java.net.MalformedURLException; +import java.util.List; + import org.jboss.arquillian.graphene.page.Page; import org.keycloak.models.UserCredentialModel; import org.keycloak.representations.idm.RealmRepresentation; @@ -49,6 +51,9 @@ public class LoginHotpTest extends TestRealmKeycloakTest { @Override public void configureTestRealm(RealmRepresentation testRealm) { + testRealm.setOtpPolicyType(UserCredentialModel.HOTP); + testRealm.setOtpPolicyAlgorithm(HmacOTP.DEFAULT_ALGORITHM); + testRealm.setOtpPolicyLookAheadWindow(2); UserRepresentation user = RealmRepUtil.findUser(testRealm, "test-user@localhost"); UserBuilder.edit(user) .hotpSecret("hotpSecret") @@ -79,9 +84,6 @@ public void configureTestRealm(RealmRepresentation testRealm) { @Before public void before() throws MalformedURLException { RealmRepresentation testRealm = testRealm().toRepresentation(); - testRealm.setOtpPolicyType(UserCredentialModel.HOTP); - testRealm.setOtpPolicyLookAheadWindow(2); - testRealm().update(testRealm); policy = new OTPPolicy(); policy.setAlgorithm(testRealm.getOtpPolicyAlgorithm()); @@ -137,7 +139,7 @@ public void loginWithHotpSuccess() throws Exception { loginPage.open(); loginPage.login("test-user@localhost", "password"); - Assert.assertTrue(loginTotpPage.isCurrent()); + Assert.assertTrue("expecting totpPage got: " + driver.getCurrentUrl(), loginTotpPage.isCurrent()); loginTotpPage.login(otp.generateHOTP("hotpSecret", counter++)); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/LoginTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/LoginTest.java index accbd37e332d..f040b13848a2 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/LoginTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/LoginTest.java @@ -354,7 +354,9 @@ public void loginWithForcePasswordChangePolicy() { events.expectRequiredAction(EventType.UPDATE_PASSWORD).user(userId).detail(Details.USERNAME, "login-test").assertEvent(); - assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType()); + String currentUrl = driver.getCurrentUrl(); + String pageSource = driver.getPageSource(); + assertEquals("bad expectation, on page: " + currentUrl, RequestType.AUTH_RESPONSE, appPage.getRequestType()); events.expectLogin().user(userId).detail(Details.USERNAME, "login-test").assertEvent(); diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/KerberosLdapTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/KerberosLdapTest.java index 7ea5c85556ef..67c5ba891c0f 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/KerberosLdapTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/KerberosLdapTest.java @@ -114,6 +114,11 @@ public void spnegoLoginTest() throws Exception { assertUser("hnelson", "hnelson@keycloak.org", "Horatio", "Nelson", false); } + @Test + @Override + public void usernamePasswordLoginTest() throws Exception { + super.usernamePasswordLoginTest(); + } @Test public void writableEditModeTest() throws Exception { diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/ldap/FederationTestUtils.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/ldap/FederationTestUtils.java index 81b10461b7c7..febb3271724e 100644 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/ldap/FederationTestUtils.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/ldap/FederationTestUtils.java @@ -68,7 +68,7 @@ public static UserModel addLocalUser(KeycloakSession session, RealmModel realm, creds.setType(CredentialRepresentation.PASSWORD); creds.setValue(password); - user.updateCredential(creds); + session.userCredentialManager().updateCredential(realm, user, creds); return user; } diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/ldap/base/FederationProvidersIntegrationTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/ldap/base/FederationProvidersIntegrationTest.java index be8c97bd8dd5..c46938770b3b 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/ldap/base/FederationProvidersIntegrationTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/ldap/base/FederationProvidersIntegrationTest.java @@ -26,6 +26,7 @@ import org.junit.rules.TestRule; import org.junit.runners.MethodSorters; import org.keycloak.OAuth2Constants; +import org.keycloak.credential.CredentialModel; import org.keycloak.federation.ldap.LDAPConfig; import org.keycloak.federation.ldap.LDAPFederationProvider; import org.keycloak.federation.ldap.LDAPFederationProviderFactory; @@ -687,7 +688,7 @@ public void testReadonly() { } try { UserCredentialModel cred = UserCredentialModel.password("PoopyPoop1"); - user.updateCredential(cred); + session.userCredentialManager().updateCredential(appRealm, user, cred); Assert.fail("should fail"); } catch (ModelReadOnlyException e) { @@ -818,10 +819,10 @@ public void testUnsynced() throws Exception { Assert.assertEquals(user.getFederationLink(), ldapModel.getId()); UserCredentialModel cred = UserCredentialModel.password("Candycand1"); - user.updateCredential(cred); - UserCredentialValueModel userCredentialValueModel = user.getCredentialsDirectly().get(0); + session.userCredentialManager().updateCredential(appRealm, user, cred); + CredentialModel userCredentialValueModel = session.userCredentialManager().getStoredCredentialsByType(appRealm, user, CredentialModel.PASSWORD).get(0); Assert.assertEquals(UserCredentialModel.PASSWORD, userCredentialValueModel.getType()); - Assert.assertTrue(session.users().validCredentials(session, appRealm, user, cred)); + Assert.assertTrue(session.userCredentialManager().isValid(appRealm, user, cred)); // LDAP password is still unchanged LDAPFederationProvider ldapProvider = FederationTestUtils.getLdapProvider(session, model); diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/storage/UserMapStorage.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/storage/UserMapStorage.java index 7f72e8efab2f..962262af16c3 100644 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/storage/UserMapStorage.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/storage/UserMapStorage.java @@ -17,6 +17,10 @@ package org.keycloak.testsuite.federation.storage; import org.keycloak.component.ComponentModel; +import org.keycloak.credential.CredentialInput; +import org.keycloak.credential.CredentialInputUpdater; +import org.keycloak.credential.CredentialInputValidator; +import org.keycloak.credential.CredentialModel; import org.keycloak.models.GroupModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; @@ -40,7 +44,7 @@ * @author Bill Burke * @version $Revision: 1 $ */ -public class UserMapStorage implements UserLookupProvider, UserStorageProvider, UserRegistrationProvider { +public class UserMapStorage implements UserLookupProvider, UserStorageProvider, UserRegistrationProvider, CredentialInputUpdater, CredentialInputValidator { protected Map userPasswords; protected ComponentModel model; @@ -78,37 +82,45 @@ public void setUsername(String username) { throw new RuntimeException("Unsupported"); } - @Override - public void updateCredential(UserCredentialModel cred) { - if (cred.getType().equals(UserCredentialModel.PASSWORD)) { - userPasswords.put(getUsername(), cred.getValue()); - } else { - super.updateCredential(cred); - } - } + }; + } - @Override - public List getCredentialsDirectly() { - UserCredentialValueModel pw = new UserCredentialValueModel(); - pw.setId(getId()); - pw.setType(UserCredentialModel.PASSWORD); - pw.setAlgorithm("text"); - pw.setValue(userPasswords.get(getUsername())); - List creds = new LinkedList<>(); - creds.addAll(super.getCredentialsDirectly()); - creds.add(pw); - return creds; - } + @Override + public boolean supportsCredentialType(String credentialType) { + return CredentialModel.PASSWORD.equals(credentialType); + } - @Override - public void updateCredentialDirectly(UserCredentialValueModel cred) { - if (cred.getType().equals(UserCredentialModel.PASSWORD)) { - //userPasswords.put(getUsername(), cred.getValue()); - } else { - super.updateCredentialDirectly(cred); - } - } - }; + @Override + public boolean updateCredential(RealmModel realm, UserModel user, CredentialInput input) { + if (!(input instanceof UserCredentialModel)) return false; + if (input.getType().equals(UserCredentialModel.PASSWORD)) { + userPasswords.put(user.getUsername(), ((UserCredentialModel)input).getValue()); + return true; + + } else { + return false; + } + } + + @Override + public void disableCredentialType(RealmModel realm, UserModel user, String credentialType) { + + } + + @Override + public boolean isConfiguredFor(RealmModel realm, UserModel user, String credentialType) { + return CredentialModel.PASSWORD.equals(credentialType); + } + + @Override + public boolean isValid(RealmModel realm, UserModel user, CredentialInput input) { + if (!(input instanceof UserCredentialModel)) return false; + if (input.getType().equals(UserCredentialModel.PASSWORD)) { + String pw = userPasswords.get(user.getUsername()); + return pw != null && pw.equals( ((UserCredentialModel)input).getValue()); + } else { + return false; + } } @Override diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/storage/UserPropertyFileStorage.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/storage/UserPropertyFileStorage.java index c297aca807a9..5a1db7e6afe0 100644 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/storage/UserPropertyFileStorage.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/storage/UserPropertyFileStorage.java @@ -17,6 +17,8 @@ package org.keycloak.testsuite.federation.storage; import org.keycloak.component.ComponentModel; +import org.keycloak.credential.CredentialInput; +import org.keycloak.credential.CredentialInputValidator; import org.keycloak.models.GroupModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; @@ -41,7 +43,7 @@ * @author Bill Burke * @version $Revision: 1 $ */ -public class UserPropertyFileStorage implements UserLookupProvider, UserStorageProvider, UserCredentialValidatorProvider, UserQueryProvider { +public class UserPropertyFileStorage implements UserLookupProvider, UserStorageProvider, UserQueryProvider, CredentialInputValidator { protected Properties userPasswords; protected ComponentModel model; @@ -116,16 +118,27 @@ public void preRemove(RealmModel realm, RoleModel role) { } @Override - public boolean validCredentials(KeycloakSession session, RealmModel realm, UserModel user, List input) { - for (UserCredentialModel cred : input) { - if (!cred.getType().equals(UserCredentialModel.PASSWORD)) return false; - String password = (String)userPasswords.get(user.getUsername()); - if (password == null) return false; - if (!password.equals(cred.getValue())) return false; + public boolean supportsCredentialType(String credentialType) { + return credentialType.equals(UserCredentialModel.PASSWORD); + } + + @Override + public boolean isConfiguredFor(RealmModel realm, UserModel user, String credentialType) { + return credentialType.equals(UserCredentialModel.PASSWORD) && userPasswords.get(user.getUsername()) != null; + } + + @Override + public boolean isValid(RealmModel realm, UserModel user, CredentialInput input) { + if (!(input instanceof UserCredentialModel)) return false; + if (input.getType().equals(UserCredentialModel.PASSWORD)) { + String pw = (String)userPasswords.get(user.getUsername()); + return pw != null && pw.equals( ((UserCredentialModel)input).getValue()); + } else { + return false; } - return true; } + @Override public int getUsersCount(RealmModel realm) { return userPasswords.size(); diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/storage/UserStorageTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/storage/UserStorageTest.java index 3c3a6a04a929..2b2d9623a126 100644 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/storage/UserStorageTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/storage/UserStorageTest.java @@ -159,7 +159,7 @@ public void testUpdate() { System.out.println("num groups " + groups.size()); Assert.assertTrue(thor.getRequiredActions().iterator().next().equals("POOP")); thor.removeRequiredAction("POOP"); - thor.updateCredential(UserCredentialModel.password("lightning")); + session.userCredentialManager().updateCredential(realm, thor, UserCredentialModel.password("lightning")); keycloakRule.stopSession(session, true); loginSuccessAndLogout("thor", "lightning"); } @@ -254,7 +254,7 @@ public void testRegistration() { KeycloakSession session = keycloakRule.startSession(); RealmModel realm = session.realms().getRealmByName("test"); UserModel user = session.users().addUser(realm, "memuser"); - user.updateCredential(UserCredentialModel.password("password")); + session.userCredentialManager().updateCredential(realm, user, UserCredentialModel.password("password")); keycloakRule.stopSession(session, true); loginSuccessAndLogout("memuser", "password"); loginSuccessAndLogout("memuser", "password"); @@ -264,7 +264,6 @@ public void testRegistration() { realm = session.realms().getRealmByName("test"); user = session.users().getUserByUsername("memuser", realm); Assert.assertEquals(memoryProvider.getId(), StorageId.resolveProviderId(user)); - Assert.assertEquals(1, user.getCredentialsDirectly().size()); session.users().removeUser(realm, user); Assert.assertNull(session.users().getUserByUsername("memuser", realm)); keycloakRule.stopSession(session, true); diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/model/AdapterTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/AdapterTest.java index 42d190c980da..2d57d21bcdde 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/model/AdapterTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/AdapterTest.java @@ -23,6 +23,7 @@ import org.junit.runners.MethodSorters; import org.keycloak.Config; import org.keycloak.component.ComponentModel; +import org.keycloak.credential.CredentialModel; import org.keycloak.models.ClientModel; import org.keycloak.models.FederatedIdentityModel; import org.keycloak.models.ModelDuplicateException; @@ -164,13 +165,14 @@ public void testCredentialValidation() throws Exception { UserCredentialModel cred = new UserCredentialModel(); cred.setType(CredentialRepresentation.PASSWORD); cred.setValue("geheim"); - user.updateCredential(cred); - Assert.assertTrue(userProvider.validCredentials(session, realmModel, user, UserCredentialModel.password("geheim"))); - List creds = user.getCredentialsDirectly(); + realmManager.getSession().userCredentialManager().updateCredential(realmModel, user, cred); + Assert.assertTrue(realmManager.getSession().userCredentialManager().isValid(realmModel, user, UserCredentialModel.password("geheim"))); + List creds = realmManager.getSession().userCredentialManager().getStoredCredentialsByType(realmModel, user, CredentialModel.PASSWORD); + Assert.assertEquals(creds.get(0).getHashIterations(), 20000); realmModel.setPasswordPolicy(PasswordPolicy.parse(realmManager.getSession(), "hashIterations(200)")); - Assert.assertTrue(userProvider.validCredentials(session, realmModel, user, UserCredentialModel.password("geheim"))); - creds = user.getCredentialsDirectly(); + Assert.assertTrue(realmManager.getSession().userCredentialManager().isValid(realmModel, user, UserCredentialModel.password("geheim"))); + creds = realmManager.getSession().userCredentialManager().getStoredCredentialsByType(realmModel, user, CredentialModel.PASSWORD); Assert.assertEquals(creds.get(0).getHashIterations(), 200); realmModel.setPasswordPolicy(PasswordPolicy.parse(realmManager.getSession(), "hashIterations(1)")); } @@ -196,8 +198,7 @@ public void testDeleteUser() throws Exception { UserCredentialModel cred = new UserCredentialModel(); cred.setType(CredentialRepresentation.PASSWORD); cred.setValue("password"); - user.updateCredential(cred); - + realmManager.getSession().userCredentialManager().updateCredential(realmModel, user, cred); commit(); realmModel = model.getRealm("JUGGLER"); @@ -237,7 +238,7 @@ public void testRemoveRealm() throws Exception { UserCredentialModel cred = new UserCredentialModel(); cred.setType(CredentialRepresentation.PASSWORD); cred.setValue("password"); - user.updateCredential(cred); + realmManager.getSession().userCredentialManager().updateCredential(realmModel, user, cred); ClientModel client = realmModel.addClient("client"); diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/model/MultipleRealmsTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/MultipleRealmsTest.java index f91540ebc78a..027230eb6971 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/model/MultipleRealmsTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/MultipleRealmsTest.java @@ -53,13 +53,13 @@ public void testUsers() { Assert.assertNotEquals(r1user1.getId(), r2user1.getId()); // Test password - r1user1.updateCredential(UserCredentialModel.password("pass1")); - r2user1.updateCredential(UserCredentialModel.password("pass2")); + session.userCredentialManager().updateCredential(realm1, r1user1, UserCredentialModel.password("pass1")); + session.userCredentialManager().updateCredential(realm2, r2user1, UserCredentialModel.password("pass2")); - Assert.assertTrue(session.users().validCredentials(session, realm1, r1user1, UserCredentialModel.password("pass1"))); - Assert.assertFalse(session.users().validCredentials(session, realm1, r1user1, UserCredentialModel.password("pass2"))); - Assert.assertFalse(session.users().validCredentials(session, realm2, r2user1, UserCredentialModel.password("pass1"))); - Assert.assertTrue(session.users().validCredentials(session, realm2, r2user1, UserCredentialModel.password("pass2"))); + Assert.assertTrue(session.userCredentialManager().isValid(realm1, r1user1, UserCredentialModel.password("pass1"))); + Assert.assertFalse(session.userCredentialManager().isValid(realm1, r1user1, UserCredentialModel.password("pass2"))); + Assert.assertFalse(session.userCredentialManager().isValid(realm2, r2user1, UserCredentialModel.password("pass1"))); + Assert.assertTrue(session.userCredentialManager().isValid(realm2, r2user1, UserCredentialModel.password("pass2"))); // Test searching Assert.assertEquals(2, session.users().searchForUser("user", realm1).size()); diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/util/cli/UserCommands.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/util/cli/UserCommands.java index 9bde85207b9c..fe556024dc47 100644 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/util/cli/UserCommands.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/util/cli/UserCommands.java @@ -103,7 +103,7 @@ private void createUsersInBatch(KeycloakSession session, int first, int count) { user.setEnabled(true); user.setEmail(username + "@keycloak.org"); UserCredentialModel passwordCred = UserCredentialModel.password(password); - user.updateCredential(passwordCred); + session.userCredentialManager().updateCredential(realm, user, passwordCred); for (RoleModel role : roles) { user.grantRole(role); From a1bcd0651dbf0fe850c8eba01de91da98a2ec842 Mon Sep 17 00:00:00 2001 From: Bill Burke Date: Fri, 23 Sep 2016 10:38:49 -0400 Subject: [PATCH 2/3] fixes --- .../hash/Pbkdf2PasswordHashProvider.java | 2 +- .../managers/DefaultBruteForceProtector.java | 38 +------------------ .../testsuite/forms/BruteForceTest.java | 29 +------------- 3 files changed, 4 insertions(+), 65 deletions(-) diff --git a/services/src/main/java/org/keycloak/credential/hash/Pbkdf2PasswordHashProvider.java b/services/src/main/java/org/keycloak/credential/hash/Pbkdf2PasswordHashProvider.java index b477c9c5c4b1..d3d9b60fe0cc 100644 --- a/services/src/main/java/org/keycloak/credential/hash/Pbkdf2PasswordHashProvider.java +++ b/services/src/main/java/org/keycloak/credential/hash/Pbkdf2PasswordHashProvider.java @@ -64,7 +64,7 @@ public boolean verify(String rawPassword, UserCredentialValueModel credential) { @Override public boolean policyCheck(PasswordPolicy policy, CredentialModel credential) { - return credential.getHashIterations() == policy.getHashIterations() && PBKDF2_ALGORITHM.equals(credential.getAlgorithm()); + return credential.getHashIterations() == policy.getHashIterations() && ID.equals(credential.getAlgorithm()); } @Override diff --git a/services/src/main/java/org/keycloak/services/managers/DefaultBruteForceProtector.java b/services/src/main/java/org/keycloak/services/managers/DefaultBruteForceProtector.java index 67d3dc97744f..2e809d1b1285 100644 --- a/services/src/main/java/org/keycloak/services/managers/DefaultBruteForceProtector.java +++ b/services/src/main/java/org/keycloak/services/managers/DefaultBruteForceProtector.java @@ -53,19 +53,6 @@ public class DefaultBruteForceProtector implements Runnable, BruteForceProtector protected LinkedBlockingQueue queue = new LinkedBlockingQueue(); public static final int TRANSACTION_SIZE = 20; - public static volatile int markNotBefore = -1; - public static volatile int markCheck = -1; - public static volatile int testCount = 0; - public static volatile int clearCount = 0; - public static volatile int nullFailureCount = 0; - public static volatile int logFailureCalled = 0; - public static volatile int logFailure = 0; - public static volatile int didntWait = 0; - public static volatile int exception = 0; - public static volatile Exception exceptionObject = null; - public static volatile String notFoundUserId = null; - - protected abstract class LoginEvent implements Comparable { protected final String realmId; protected final String userId; @@ -103,7 +90,6 @@ public DefaultBruteForceProtector(KeycloakSessionFactory factory) { public void failure(KeycloakSession session, LoginEvent event) { logger.debug("failure"); - logFailureCalled++; RealmModel realm = getRealmModel(session, event); logFailure(event); @@ -111,7 +97,6 @@ public void failure(KeycloakSession session, LoginEvent event) { UserModel user = session.users().getUserById(userId, realm); UserLoginFailureModel userLoginFailure = getUserModel(session, event); if (user != null) { - logFailure++; if (userLoginFailure == null) { userLoginFailure = session.sessions().addUserLoginFailure(realm, userId); } @@ -126,7 +111,6 @@ public void failure(KeycloakSession session, LoginEvent event) { if (deltaTime > 0) { // if last failure was more than MAX_DELTA clear failures if (deltaTime > (long) realm.getMaxDeltaTimeSeconds() * 1000L) { - clearCount++; userLoginFailure.clearFailures(); } } @@ -147,12 +131,8 @@ public void failure(KeycloakSession session, LoginEvent event) { waitSeconds = Math.min(realm.getMaxFailureWaitSeconds(), waitSeconds); int notBefore = (int) (currentTime / 1000) + waitSeconds; logger.debugv("set notBefore: {0}", notBefore); - markNotBefore = notBefore; userLoginFailure.setFailedLoginNotBefore(notBefore); } - } else { - notFoundUserId = event.userId; - } } @@ -210,8 +190,6 @@ public void run() { } session.getTransactionManager().commit(); } catch (Exception e) { - exception++; - exceptionObject = e; session.getTransactionManager().rollback(); throw e; } finally { @@ -258,13 +236,8 @@ public void failedLogin(RealmModel realm, UserModel user, ClientConnection clien // wait a minimum of seconds for type to process so that a hacker // cannot flood with failed logins and overwhelm the queue and not have notBefore updated to block next requests // todo failure HTTP responses should be queued via async HTTP - boolean awaited = - event.latch.await(5, TimeUnit.SECONDS); - if (!awaited) { - didntWait++; - } + event.latch.await(5, TimeUnit.SECONDS); } catch (InterruptedException e) { - didntWait++; } logger.trace("sent failure event"); } @@ -274,19 +247,12 @@ public boolean isTemporarilyDisabled(KeycloakSession session, RealmModel realm, UserLoginFailureModel failure = session.sessions().getUserLoginFailure(realm, user.getId()); if (failure != null) { - int currTime = markCheck = (int) (Time.currentTimeMillis() / 1000); + int currTime = (int) (Time.currentTimeMillis() / 1000); int failedLoginNotBefore = failure.getFailedLoginNotBefore(); - testCount++; if (currTime < failedLoginNotBefore) { logger.debugv("Current: {0} notBefore: {1}", currTime, failedLoginNotBefore); return true; - } else if (failedLoginNotBefore > 0){ - logger.debugv("failedLoginNotBefore > 0 Current: {0} notBefore: {1}", currTime, failedLoginNotBefore); } - } else { - logger.debugv("failure was null"); - nullFailureCount++; - } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/BruteForceTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/BruteForceTest.java index f8fbb91b1ae7..31c297f9d56c 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/BruteForceTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/BruteForceTest.java @@ -173,7 +173,6 @@ public void testGrantInvalidPassword() throws Exception { @Test public void testGrantInvalidOtp() throws Exception { - clearVariables(); { String totpSecret = totp.generateTOTP("totpSecret"); OAuthClient.AccessTokenResponse response = getTestToken("password", totpSecret); @@ -215,40 +214,14 @@ public void testGrantInvalidOtp() throws Exception { } - public void clearVariables() { - DefaultBruteForceProtector.testCount = 0; - DefaultBruteForceProtector.markNotBefore = -1; - DefaultBruteForceProtector.markCheck = -1; - DefaultBruteForceProtector.testCount = 0; - DefaultBruteForceProtector.clearCount = 0; - DefaultBruteForceProtector.logFailure = 0; - DefaultBruteForceProtector.logFailureCalled = 0; - DefaultBruteForceProtector.didntWait = 0; - DefaultBruteForceProtector.nullFailureCount = 0; - DefaultBruteForceProtector.notFoundUserId = null; - } - public void assertTokenNull(OAuthClient.AccessTokenResponse response) { - if (response.getAccessToken() != null) { - if (DefaultBruteForceProtector.exceptionObject != null) { - DefaultBruteForceProtector.exceptionObject.printStackTrace(); - } - } - Assert.assertNull("was mark set: " + DefaultBruteForceProtector.markNotBefore + " time test: " + DefaultBruteForceProtector.markCheck - + " test count " + DefaultBruteForceProtector.testCount - + " nullFailure: " + DefaultBruteForceProtector.nullFailureCount - + " logFailureCalled: " + DefaultBruteForceProtector.logFailureCalled - + " logFailure: " + DefaultBruteForceProtector.logFailure - + " notFoundUserId: " + DefaultBruteForceProtector.notFoundUserId - + " exception: " + DefaultBruteForceProtector.exception - , response.getAccessToken()); + Assert.assertNull(response.getAccessToken()); } @Test public void testGrantMissingOtp() throws Exception { - clearVariables(); { String totpSecret = totp.generateTOTP("totpSecret"); OAuthClient.AccessTokenResponse response = getTestToken("password", totpSecret); From ff1326fe358c7d99ac0731829e3903b805daa733 Mon Sep 17 00:00:00 2001 From: Bill Burke Date: Fri, 23 Sep 2016 16:50:08 -0400 Subject: [PATCH 3/3] authenticator example updated --- .../demo-dist/src/main/xslt/standalone.xsl | 2 +- examples/providers/authenticator/README.md | 32 +++----- examples/providers/authenticator/pom.xml | 7 ++ .../SecretQuestionAuthenticator.java | 17 +++- .../SecretQuestionCredentialProvider.java | 2 +- .../user/EjbExampleUserStorageProvider.java | 82 ++++++++++++++++--- .../EjbExampleUserStorageProviderFactory.java | 8 ++ .../examples/storage/user/UserAdapter.java | 43 ++-------- .../main/resources/META-INF/persistence.xml | 2 +- .../models/cache/infinispan/UserAdapter.java | 4 +- .../cache/infinispan/UserCacheSession.java | 21 ++--- .../org/keycloak/models/UserProvider.java | 2 - .../models/cache/CachedUserModel.java | 14 ++++ .../keycloak/models/cache/OnUserCache.java | 3 +- .../user/UserCredentialValidatorProvider.java | 33 -------- .../credential/OTPCredentialProvider.java | 2 +- .../PasswordCredentialProvider.java | 2 +- .../UserCredentialStoreManager.java | 4 +- .../DefaultKeycloakSessionFactory.java | 2 + .../managers/AuthenticationManager.java | 4 + .../keycloak/storage/UserStorageManager.java | 6 +- .../federation/storage/UserMapStorage.java | 2 - .../storage/UserPropertyFileStorage.java | 1 - .../keycloak-datasources.xml | 16 ++-- 24 files changed, 175 insertions(+), 136 deletions(-) delete mode 100644 server-spi/src/main/java/org/keycloak/storage/user/UserCredentialValidatorProvider.java diff --git a/distribution/demo-dist/src/main/xslt/standalone.xsl b/distribution/demo-dist/src/main/xslt/standalone.xsl index ca45244efea6..d67ce63f0288 100755 --- a/distribution/demo-dist/src/main/xslt/standalone.xsl +++ b/distribution/demo-dist/src/main/xslt/standalone.xsl @@ -43,7 +43,7 @@ - jdbc:h2:${jboss.server.data.dir}/keycloak;AUTO_SERVER=TRUE + jdbc:h2:${jboss.server.data.dir}/keycloak;AUTO_SERVER=TRUE h2 sa diff --git a/examples/providers/authenticator/README.md b/examples/providers/authenticator/README.md index 54dc752734b9..ef612cc2a76b 100755 --- a/examples/providers/authenticator/README.md +++ b/examples/providers/authenticator/README.md @@ -1,31 +1,23 @@ Example Custom Authenticator =================================================== -This is an example of defining a custom Authenticator and Required action. This example is explained in the user documentation -of Keycloak. To deploy, build this directory then take the jar and copy it to providers directory. Alternatively you can deploy as a module by running: +1. First, Keycloak must be running. - KEYCLOAK_HOME/bin/jboss-cli.sh --command="module add --name=org.keycloak.examples.secret-question --resources=target/authenticator-required-action-example.jar --dependencies=org.keycloak.keycloak-core,org.keycloak.keycloak-server-spi,org.keycloak.keycloak-services,org.jboss.resteasy.resteasy-jaxrs,javax.ws.rs.api" +2. Execute the follow. This will build the example and deploy it -Then registering the provider by editing `standalone/configuration/standalone.xml` and adding the module to the providers element: + $ mvn clean install wildfly:deploy - - ... - module:org.keycloak.examples.secret-question - +3. Copy the secret-question.ftl and secret-question-config.ftl files to the themes/base/login directory. -You then have to copy the secret-question.ftl and secret-question-config.ftl files to the themes/base/login directory. +4. Login to admin console. Hit browser refresh if you are already logged in so that the new providers show up. -After you do all this, you then have to reboot keycloak. When reboot is complete, you will need to log into -the admin console to create a new flow with your new authenticator. +5. Go to the Authentication menu item and go to the Flow tab, you will be able to view the currently + defined flows. You cannot modify an built in flows, so, to add the Authenticator you + have to copy an existing flow or create your own. Copy the "Browser" flow. -If you go to the Authentication menu item and go to the Flow tab, you will be able to view the currently -defined flows. You cannot modify an built in flows, so, to add the Authenticator you -have to copy an existing flow or create your own. +6. In your copy, click the "Actions" menu item and "Add Execution". Pick Secret Question -Next you have to register your required action. -Click on the Required Actions tab. Click on the Register button and choose your new Required Action. -Your new required action should now be displayed and enabled in the required actions list. +7. Next you have to register the required action that you created. Click on the Required Actions tab in the Authenticaiton menu. + Click on the Register button and choose your new Required Action. + Your new required action should now be displayed and enabled in the required actions list. -I'm hoping the UI is intuitive enough so that you -can figure out for yourself how to create a flow and add the Authenticator and Required Action. We're looking to add a screencast -to show this in action. diff --git a/examples/providers/authenticator/pom.xml b/examples/providers/authenticator/pom.xml index 0123a91977bb..16a95d6fe6f3 100755 --- a/examples/providers/authenticator/pom.xml +++ b/examples/providers/authenticator/pom.xml @@ -64,6 +64,13 @@ 1.8 + + org.wildfly.plugins + wildfly-maven-plugin + + false + + diff --git a/examples/providers/authenticator/src/main/java/org/keycloak/examples/authenticator/SecretQuestionAuthenticator.java b/examples/providers/authenticator/src/main/java/org/keycloak/examples/authenticator/SecretQuestionAuthenticator.java index e25ff812b671..fb71a7c9cfe7 100755 --- a/examples/providers/authenticator/src/main/java/org/keycloak/examples/authenticator/SecretQuestionAuthenticator.java +++ b/examples/providers/authenticator/src/main/java/org/keycloak/examples/authenticator/SecretQuestionAuthenticator.java @@ -17,18 +17,20 @@ package org.keycloak.examples.authenticator; +import org.jboss.resteasy.spi.HttpResponse; +import org.jboss.resteasy.spi.ResteasyProviderFactory; import org.keycloak.authentication.AuthenticationFlowContext; import org.keycloak.authentication.AuthenticationFlowError; import org.keycloak.authentication.Authenticator; +import org.keycloak.common.util.ServerCookie; import org.keycloak.models.AuthenticatorConfigModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.models.UserCredentialModel; -import org.keycloak.models.UserCredentialValueModel; import org.keycloak.models.UserModel; -import org.keycloak.services.util.CookieHelper; import javax.ws.rs.core.Cookie; +import javax.ws.rs.core.HttpHeaders; import javax.ws.rs.core.MultivaluedMap; import javax.ws.rs.core.Response; import java.net.URI; @@ -87,13 +89,22 @@ protected void setCookie(AuthenticationFlowContext context) { } URI uri = context.getUriInfo().getBaseUriBuilder().path("realms").path(context.getRealm().getName()).build(); - CookieHelper.addCookie("SECRET_QUESTION_ANSWERED", "true", + addCookie("SECRET_QUESTION_ANSWERED", "true", uri.getRawPath(), null, null, maxCookieAge, false, true); } + public static void addCookie(String name, String value, String path, String domain, String comment, int maxAge, boolean secure, boolean httpOnly) { + HttpResponse response = ResteasyProviderFactory.getContextData(HttpResponse.class); + StringBuffer cookieBuf = new StringBuffer(); + ServerCookie.appendCookieValue(cookieBuf, 1, name, value, path, domain, comment, maxAge, secure, httpOnly); + String cookie = cookieBuf.toString(); + response.getOutputHeaders().add(HttpHeaders.SET_COOKIE, cookie); + } + + protected boolean validateAnswer(AuthenticationFlowContext context) { MultivaluedMap formData = context.getHttpRequest().getDecodedFormParameters(); String secret = formData.getFirst("secret_answer"); diff --git a/examples/providers/authenticator/src/main/java/org/keycloak/examples/authenticator/SecretQuestionCredentialProvider.java b/examples/providers/authenticator/src/main/java/org/keycloak/examples/authenticator/SecretQuestionCredentialProvider.java index 693c0eb2c26e..520aeb0080cc 100644 --- a/examples/providers/authenticator/src/main/java/org/keycloak/examples/authenticator/SecretQuestionCredentialProvider.java +++ b/examples/providers/authenticator/src/main/java/org/keycloak/examples/authenticator/SecretQuestionCredentialProvider.java @@ -110,7 +110,7 @@ public boolean isValid(RealmModel realm, UserModel user, CredentialInput input) } @Override - public void onCache(RealmModel realm, CachedUserModel user) { + public void onCache(RealmModel realm, CachedUserModel user, UserModel delegate) { List creds = session.userCredentialManager().getStoredCredentialsByType(realm, user, SECRET_QUESTION); if (!creds.isEmpty()) user.getCachedWith().put(CACHE_KEY, creds.get(0)); } diff --git a/examples/providers/user-storage-jpa/src/main/java/org/keycloak/examples/storage/user/EjbExampleUserStorageProvider.java b/examples/providers/user-storage-jpa/src/main/java/org/keycloak/examples/storage/user/EjbExampleUserStorageProvider.java index a9bbc9b961db..8f7f147d4396 100644 --- a/examples/providers/user-storage-jpa/src/main/java/org/keycloak/examples/storage/user/EjbExampleUserStorageProvider.java +++ b/examples/providers/user-storage-jpa/src/main/java/org/keycloak/examples/storage/user/EjbExampleUserStorageProvider.java @@ -18,16 +18,21 @@ import org.jboss.logging.Logger; import org.keycloak.component.ComponentModel; +import org.keycloak.credential.CredentialInput; +import org.keycloak.credential.CredentialInputUpdater; +import org.keycloak.credential.CredentialInputValidator; +import org.keycloak.credential.CredentialModel; import org.keycloak.models.GroupModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.models.RoleModel; import org.keycloak.models.UserCredentialModel; import org.keycloak.models.UserModel; +import org.keycloak.models.cache.CachedUserModel; +import org.keycloak.models.cache.OnUserCache; import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.storage.StorageId; import org.keycloak.storage.UserStorageProvider; -import org.keycloak.storage.user.UserCredentialValidatorProvider; import org.keycloak.storage.user.UserLookupProvider; import org.keycloak.storage.user.UserQueryProvider; import org.keycloak.storage.user.UserRegistrationProvider; @@ -52,9 +57,13 @@ public class EjbExampleUserStorageProvider implements UserStorageProvider, UserLookupProvider, UserRegistrationProvider, - UserCredentialValidatorProvider, - UserQueryProvider { + UserQueryProvider, + CredentialInputUpdater, + CredentialInputValidator, + OnUserCache +{ private static final Logger logger = Logger.getLogger(EjbExampleUserStorageProvider.class); + public static final String PASSWORD_CACHE_KEY = UserAdapter.class.getName() + ".password"; @PersistenceContext protected EntityManager em; @@ -150,18 +159,69 @@ public void grantToAllUsers(RealmModel realm, RoleModel role) { } @Override - public boolean validCredentials(KeycloakSession session, RealmModel realm, UserModel user, List input) { - // having a "password" attribute is a workaround so that passwords can be cached. All done for performance reasons... - // If we override getCredentialsDirectly/updateCredentialsDirectly - // then the realm passsword policy will/may try and overwrite the plain text password with a hash. - // If you don't like this workaround, you can query the database every time to validate the password - for (UserCredentialModel cred : input) { - if (!UserCredentialModel.PASSWORD.equals(cred.getType())) return false; - if (!cred.getValue().equals(user.getFirstAttribute("password"))) return false; + public void onCache(RealmModel realm, CachedUserModel user, UserModel delegate) { + String password = ((UserAdapter)delegate).getPassword(); + if (password != null) { + user.getCachedWith().put(PASSWORD_CACHE_KEY, password); } + } + + @Override + public boolean supportsCredentialType(String credentialType) { + return CredentialModel.PASSWORD.equals(credentialType); + } + + @Override + public boolean updateCredential(RealmModel realm, UserModel user, CredentialInput input) { + if (!supportsCredentialType(input.getType()) || !(input instanceof UserCredentialModel)) return false; + UserCredentialModel cred = (UserCredentialModel)input; + UserAdapter adapter = getUserAdapter(user); + adapter.setPassword(cred.getValue()); + return true; } + public UserAdapter getUserAdapter(UserModel user) { + UserAdapter adapter = null; + if (user instanceof CachedUserModel) { + adapter = (UserAdapter)((CachedUserModel)user).getDelegateForUpdate(); + } else { + adapter = (UserAdapter)user; + } + return adapter; + } + + @Override + public void disableCredentialType(RealmModel realm, UserModel user, String credentialType) { + if (!supportsCredentialType(credentialType)) return; + + getUserAdapter(user).setPassword(null); + + } + + @Override + public boolean isConfiguredFor(RealmModel realm, UserModel user, String credentialType) { + return supportsCredentialType(credentialType) && getPassword(user) != null; + } + + @Override + public boolean isValid(RealmModel realm, UserModel user, CredentialInput input) { + if (!supportsCredentialType(input.getType()) || !(input instanceof UserCredentialModel)) return false; + UserCredentialModel cred = (UserCredentialModel)input; + String password = getPassword(user); + return password != null && password.equals(cred.getValue()); + } + + public String getPassword(UserModel user) { + String password = null; + if (user instanceof CachedUserModel) { + password = (String)((CachedUserModel)user).getCachedWith().get(PASSWORD_CACHE_KEY); + } else if (user instanceof UserAdapter) { + password = ((UserAdapter)user).getPassword(); + } + return password; + } + @Override public int getUsersCount(RealmModel realm) { Object count = em.createNamedQuery("getUserCount") diff --git a/examples/providers/user-storage-jpa/src/main/java/org/keycloak/examples/storage/user/EjbExampleUserStorageProviderFactory.java b/examples/providers/user-storage-jpa/src/main/java/org/keycloak/examples/storage/user/EjbExampleUserStorageProviderFactory.java index a1db65d1b80c..1e1610047889 100644 --- a/examples/providers/user-storage-jpa/src/main/java/org/keycloak/examples/storage/user/EjbExampleUserStorageProviderFactory.java +++ b/examples/providers/user-storage-jpa/src/main/java/org/keycloak/examples/storage/user/EjbExampleUserStorageProviderFactory.java @@ -16,6 +16,7 @@ */ package org.keycloak.examples.storage.user; +import org.jboss.logging.Logger; import org.keycloak.Config; import org.keycloak.component.ComponentModel; import org.keycloak.models.KeycloakSession; @@ -32,6 +33,7 @@ * @version $Revision: 1 $ */ public class EjbExampleUserStorageProviderFactory implements UserStorageProviderFactory { + private static final Logger logger = Logger.getLogger(EjbExampleUserStorageProviderFactory.class); @Override @@ -56,4 +58,10 @@ public String getId() { public String getHelpText() { return "JPA Example User Storage Provider"; } + + @Override + public void close() { + logger.info("<<<<<< Closing factory"); + + } } diff --git a/examples/providers/user-storage-jpa/src/main/java/org/keycloak/examples/storage/user/UserAdapter.java b/examples/providers/user-storage-jpa/src/main/java/org/keycloak/examples/storage/user/UserAdapter.java index 84f4084d8b7a..8c8bcd6025f1 100644 --- a/examples/providers/user-storage-jpa/src/main/java/org/keycloak/examples/storage/user/UserAdapter.java +++ b/examples/providers/user-storage-jpa/src/main/java/org/keycloak/examples/storage/user/UserAdapter.java @@ -34,7 +34,7 @@ * @version $Revision: 1 $ */ public class UserAdapter extends AbstractUserAdapterFederatedStorage { - private static final Logger logger = Logger.getLogger(EjbExampleUserStorageProvider.class); + private static final Logger logger = Logger.getLogger(UserAdapter.class); protected UserEntity entity; protected String keycloakId; @@ -44,6 +44,14 @@ public UserAdapter(KeycloakSession session, RealmModel realm, ComponentModel mod keycloakId = StorageId.keycloakId(model, entity.getId()); } + public String getPassword() { + return entity.getPassword(); + } + + public void setPassword(String password) { + entity.setPassword(password); + } + @Override public String getUsername() { return entity.getUsername(); @@ -74,13 +82,6 @@ public String getId() { public void setSingleAttribute(String name, String value) { if (name.equals("phone")) { entity.setPhone(value); - } else if (name.equals("password")) { - // ignore - - // having a "password" attribute is a workaround so that passwords can be cached. All done for performance reasons... - // If we override getCredentialsDirectly/updateCredentialsDirectly - // then the realm passsword policy will/may try and overwrite the plain text password with a hash. - // If you don't like this workaround, you can query the database every time to validate the password } else { super.setSingleAttribute(name, value); } @@ -90,13 +91,6 @@ public void setSingleAttribute(String name, String value) { public void removeAttribute(String name) { if (name.equals("phone")) { entity.setPhone(null); - } else if (name.equals("password")) { - // ignore - - // having a "password" attribute is a workaround so that passwords can be cached. All done for performance reasons... - // If we override getCredentialsDirectly/updateCredentialsDirectly - // then the realm passsword policy will/may try and overwrite the plain text password with a hash. - // If you don't like this workaround, you can query the database every time to validate the password } else { super.removeAttribute(name); } @@ -106,13 +100,6 @@ public void removeAttribute(String name) { public void setAttribute(String name, List values) { if (name.equals("phone")) { entity.setPhone(values.get(0)); - } else if (name.equals("password")) { - // ignore - - // having a "password" attribute is a workaround so that passwords can be cached. All done for performance reasons... - // If we override getCredentialsDirectly/updateCredentialsDirectly - // then the realm passsword policy will/may try and overwrite the plain text password with a hash. - // If you don't like this workaround, you can query the database every time to validate the password } else { super.setAttribute(name, values); } @@ -122,12 +109,6 @@ public void setAttribute(String name, List values) { public String getFirstAttribute(String name) { if (name.equals("phone")) { return entity.getPhone(); - } else if (name.equals("password")) { - // having a "password" attribute is a workaround so that passwords can be cached. All done for performance reasons... - // If we override getCredentialsDirectly/updateCredentialsDirectly - // then the realm passsword policy will/may try and overwrite the plain text password with a hash. - // If you don't like this workaround, you can query the database every time to validate the password - return entity.getPassword(); } else { return super.getFirstAttribute(name); } @@ -139,12 +120,6 @@ public Map> getAttributes() { MultivaluedHashMap all = new MultivaluedHashMap<>(); all.putAll(attrs); all.add("phone", entity.getPhone()); - - // having a "password" attribute is a workaround so that passwords can be cached. All done for performance reasons... - // If we override getCredentialsDirectly/updateCredentialsDirectly - // then the realm passsword policy will/may try and overwrite the plain text password with a hash. - // If you don't like this workaround, you can query the database every time to validate the password - all.add("password", entity.getPassword()); return all; } diff --git a/examples/providers/user-storage-jpa/src/main/resources/META-INF/persistence.xml b/examples/providers/user-storage-jpa/src/main/resources/META-INF/persistence.xml index 9894af479ec5..51082e159c43 100644 --- a/examples/providers/user-storage-jpa/src/main/resources/META-INF/persistence.xml +++ b/examples/providers/user-storage-jpa/src/main/resources/META-INF/persistence.xml @@ -4,7 +4,7 @@ xsi:schemaLocation=" http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd"> - + java:jboss/datasources/ExampleDS org.keycloak.examples.storage.user.UserEntity diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UserAdapter.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UserAdapter.java index 6483f2a477e5..002761ea2901 100755 --- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UserAdapter.java +++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UserAdapter.java @@ -60,12 +60,14 @@ public UserAdapter(CachedUser cached, UserCacheSession userProvider, KeycloakSes this.realm = realm; } - protected void getDelegateForUpdate() { + @Override + public UserModel getDelegateForUpdate() { if (updated == null) { userProviderCache.registerUserInvalidation(realm, cached); updated = userProviderCache.getDelegate().getUserById(getId(), realm); if (updated == null) throw new IllegalStateException("Not found in database"); } + return updated; } @Override diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UserCacheSession.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UserCacheSession.java index 9143052ef5e9..73007680a6c0 100755 --- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UserCacheSession.java +++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UserCacheSession.java @@ -168,21 +168,22 @@ public UserModel getUserById(String id, RealmModel realm) { } CachedUser cached = cache.get(id, CachedUser.class); + UserModel delegate = null; boolean wasCached = cached != null; if (cached == null) { logger.trace("not cached"); Long loaded = cache.getCurrentRevision(id); - UserModel model = getDelegate().getUserById(id, realm); - if (model == null) { + delegate = getDelegate().getUserById(id, realm); + if (delegate == null) { logger.trace("delegate returning null"); return null; } - cached = new CachedUser(loaded, realm, model); + cached = new CachedUser(loaded, realm, delegate); cache.addRevisioned(cached, startupRevision); } logger.trace("returning new cache adapter"); UserAdapter adapter = new UserAdapter(cached, this, session, realm); - if (!wasCached) onCache(realm, adapter); + if (!wasCached) onCache(realm, adapter, delegate); managedUsers.put(id, adapter); return adapter; } @@ -251,24 +252,24 @@ public UserModel getUserByUsername(String username, RealmModel realm) { } } - protected UserAdapter getUserAdapter(RealmModel realm, String userId, Long loaded, UserModel model) { + protected UserAdapter getUserAdapter(RealmModel realm, String userId, Long loaded, UserModel delegate) { CachedUser cached = cache.get(userId, CachedUser.class); boolean wasCached = cached != null; if (cached == null) { - cached = new CachedUser(loaded, realm, model); + cached = new CachedUser(loaded, realm, delegate); cache.addRevisioned(cached, startupRevision); } UserAdapter adapter = new UserAdapter(cached, this, session, realm); if (!wasCached) { - onCache(realm, adapter); + onCache(realm, adapter, delegate); } return adapter; } - private void onCache(RealmModel realm, UserAdapter adapter) { - ((OnUserCache)getDelegate()).onCache(realm, adapter); - ((OnUserCache)session.userCredentialManager()).onCache(realm, adapter); + private void onCache(RealmModel realm, UserAdapter adapter, UserModel delegate) { + ((OnUserCache)getDelegate()).onCache(realm, adapter, delegate); + ((OnUserCache)session.userCredentialManager()).onCache(realm, adapter, delegate); } @Override diff --git a/server-spi/src/main/java/org/keycloak/models/UserProvider.java b/server-spi/src/main/java/org/keycloak/models/UserProvider.java index d6267c906467..e1b64fa927fe 100755 --- a/server-spi/src/main/java/org/keycloak/models/UserProvider.java +++ b/server-spi/src/main/java/org/keycloak/models/UserProvider.java @@ -18,9 +18,7 @@ package org.keycloak.models; import org.keycloak.component.ComponentModel; -import org.keycloak.credential.CredentialInput; import org.keycloak.provider.Provider; -import org.keycloak.storage.user.UserCredentialValidatorProvider; import org.keycloak.storage.user.UserLookupProvider; import org.keycloak.storage.user.UserQueryProvider; import org.keycloak.storage.user.UserRegistrationProvider; diff --git a/server-spi/src/main/java/org/keycloak/models/cache/CachedUserModel.java b/server-spi/src/main/java/org/keycloak/models/cache/CachedUserModel.java index 5d9c7cd2781e..9ee5a6a090c8 100644 --- a/server-spi/src/main/java/org/keycloak/models/cache/CachedUserModel.java +++ b/server-spi/src/main/java/org/keycloak/models/cache/CachedUserModel.java @@ -21,10 +21,24 @@ import java.util.concurrent.ConcurrentHashMap; /** + * Cached users will implement this interface + * * @author Bill Burke * @version $Revision: 1 $ */ public interface CachedUserModel extends UserModel { + + /** + * Invalidates the cache for this user and returns a delegate that represents the actual data provider + * + * @return + */ + UserModel getDelegateForUpdate(); + + /** + * Invalidate the cache for this user + * + */ void invalidate(); /** diff --git a/server-spi/src/main/java/org/keycloak/models/cache/OnUserCache.java b/server-spi/src/main/java/org/keycloak/models/cache/OnUserCache.java index e676ce14e6de..319b5c55dbc6 100644 --- a/server-spi/src/main/java/org/keycloak/models/cache/OnUserCache.java +++ b/server-spi/src/main/java/org/keycloak/models/cache/OnUserCache.java @@ -17,11 +17,12 @@ package org.keycloak.models.cache; import org.keycloak.models.RealmModel; +import org.keycloak.models.UserModel; /** * @author Bill Burke * @version $Revision: 1 $ */ public interface OnUserCache { - void onCache(RealmModel realm, CachedUserModel user); + void onCache(RealmModel realm, CachedUserModel user, UserModel delegate); } diff --git a/server-spi/src/main/java/org/keycloak/storage/user/UserCredentialValidatorProvider.java b/server-spi/src/main/java/org/keycloak/storage/user/UserCredentialValidatorProvider.java deleted file mode 100644 index 4c3d89790d8d..000000000000 --- a/server-spi/src/main/java/org/keycloak/storage/user/UserCredentialValidatorProvider.java +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright 2016 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.storage.user; - -import org.keycloak.models.KeycloakSession; -import org.keycloak.models.RealmModel; -import org.keycloak.models.UserCredentialModel; -import org.keycloak.models.UserModel; - -import java.util.List; -import java.util.Set; - -/** - * @author Bill Burke - * @version $Revision: 1 $ - */ -public interface UserCredentialValidatorProvider { - boolean validCredentials(KeycloakSession session, RealmModel realm, UserModel user, List input); -} diff --git a/services/src/main/java/org/keycloak/credential/OTPCredentialProvider.java b/services/src/main/java/org/keycloak/credential/OTPCredentialProvider.java index b6dd4894d633..f5c973ad4514 100644 --- a/services/src/main/java/org/keycloak/credential/OTPCredentialProvider.java +++ b/services/src/main/java/org/keycloak/credential/OTPCredentialProvider.java @@ -53,7 +53,7 @@ protected UserCredentialStore getCredentialStore() { } @Override - public void onCache(RealmModel realm, CachedUserModel user) { + public void onCache(RealmModel realm, CachedUserModel user, UserModel delegate) { List creds = getCredentialStore().getStoredCredentialsByType(realm, user, CredentialModel.TOTP); user.getCachedWith().put(OTPCredentialProvider.class.getName() + "." + CredentialModel.TOTP, creds); diff --git a/services/src/main/java/org/keycloak/credential/PasswordCredentialProvider.java b/services/src/main/java/org/keycloak/credential/PasswordCredentialProvider.java index 17803c14e74a..4fc58569d69e 100644 --- a/services/src/main/java/org/keycloak/credential/PasswordCredentialProvider.java +++ b/services/src/main/java/org/keycloak/credential/PasswordCredentialProvider.java @@ -206,7 +206,7 @@ public boolean isValid(RealmModel realm, UserModel user, CredentialInput input) } @Override - public void onCache(RealmModel realm, CachedUserModel user) { + public void onCache(RealmModel realm, CachedUserModel user, UserModel delegate) { List passwords = getCredentialStore().getStoredCredentialsByType(realm, user, CredentialModel.PASSWORD); if (passwords != null) { user.getCachedWith().put(PASSWORD_CACHE_KEY, passwords); diff --git a/services/src/main/java/org/keycloak/credential/UserCredentialStoreManager.java b/services/src/main/java/org/keycloak/credential/UserCredentialStoreManager.java index 915cbe7d7c86..fab312774a2e 100644 --- a/services/src/main/java/org/keycloak/credential/UserCredentialStoreManager.java +++ b/services/src/main/java/org/keycloak/credential/UserCredentialStoreManager.java @@ -254,10 +254,10 @@ public boolean isConfiguredLocally(RealmModel realm, UserModel user, String type } @Override - public void onCache(RealmModel realm, CachedUserModel user) { + public void onCache(RealmModel realm, CachedUserModel user, UserModel delegate) { List credentialProviders = getCredentialProviders(realm, OnUserCache.class); for (OnUserCache validator : credentialProviders) { - validator.onCache(realm, user); + validator.onCache(realm, user, delegate); } } diff --git a/services/src/main/java/org/keycloak/services/DefaultKeycloakSessionFactory.java b/services/src/main/java/org/keycloak/services/DefaultKeycloakSessionFactory.java index 36f9b7f163cb..808b4133cc41 100755 --- a/services/src/main/java/org/keycloak/services/DefaultKeycloakSessionFactory.java +++ b/services/src/main/java/org/keycloak/services/DefaultKeycloakSessionFactory.java @@ -136,6 +136,7 @@ public void deploy(ProviderManager pm) { @Override public void undeploy(ProviderManager pm) { + logger.debug("undeploy"); // we make a copy to avoid concurrent access exceptions Map, Map> copy = getFactoriesCopy(); MultivaluedHashMap, ProviderFactory> factories = pm.getLoadedFactories(); @@ -144,6 +145,7 @@ public void undeploy(ProviderManager pm) { Map registered = copy.get(entry.getKey()); for (ProviderFactory factory : entry.getValue()) { undeployed.add(factory); + logger.debugv("undeploying {0} of id {1}", factory.getClass().getName(), factory.getId()); if (registered != null) { registered.remove(factory.getId()); } diff --git a/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java b/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java index aa536f5fca07..b188501d7caa 100755 --- a/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java +++ b/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java @@ -570,6 +570,10 @@ protected static Response executionActions(KeycloakSession session, UserSessionM Set requiredActions) { for (String action : requiredActions) { RequiredActionProviderModel model = realm.getRequiredActionProviderByAlias(action); + if (model == null) { + logger.warnv("Could not find configuration for Required Action {0}, did you forget to register it?", action); + continue; + } if (!model.isEnabled()) { continue; } diff --git a/services/src/main/java/org/keycloak/storage/UserStorageManager.java b/services/src/main/java/org/keycloak/storage/UserStorageManager.java index d68262717986..8da231ede54c 100755 --- a/services/src/main/java/org/keycloak/storage/UserStorageManager.java +++ b/services/src/main/java/org/keycloak/storage/UserStorageManager.java @@ -566,15 +566,15 @@ public void preRemove(RealmModel realm, ComponentModel component) { } @Override - public void onCache(RealmModel realm, CachedUserModel user) { + public void onCache(RealmModel realm, CachedUserModel user, UserModel delegate) { if (StorageId.isLocalStorage(user)) { if (session.userLocalStorage() instanceof OnUserCache) { - ((OnUserCache)session.userLocalStorage()).onCache(realm, user); + ((OnUserCache)session.userLocalStorage()).onCache(realm, user, delegate); } } else { Object provider = getStorageProvider(session, realm, StorageId.resolveProviderId(user)); if (provider != null && provider instanceof OnUserCache) { - ((OnUserCache)provider).onCache(realm, user); + ((OnUserCache)provider).onCache(realm, user, delegate); } } } diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/storage/UserMapStorage.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/storage/UserMapStorage.java index 962262af16c3..a9fe9abdc06e 100644 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/storage/UserMapStorage.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/storage/UserMapStorage.java @@ -26,12 +26,10 @@ import org.keycloak.models.RealmModel; import org.keycloak.models.RoleModel; import org.keycloak.models.UserCredentialModel; -import org.keycloak.models.UserCredentialValueModel; import org.keycloak.models.UserModel; import org.keycloak.storage.StorageId; import org.keycloak.storage.UserStorageProvider; import org.keycloak.storage.adapter.AbstractUserAdapterFederatedStorage; -import org.keycloak.storage.user.UserCredentialValidatorProvider; import org.keycloak.storage.user.UserLookupProvider; import org.keycloak.storage.user.UserRegistrationProvider; diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/storage/UserPropertyFileStorage.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/storage/UserPropertyFileStorage.java index 5a1db7e6afe0..bab2c4af4bf9 100644 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/storage/UserPropertyFileStorage.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/storage/UserPropertyFileStorage.java @@ -29,7 +29,6 @@ import org.keycloak.storage.UserStorageProvider; import org.keycloak.storage.adapter.AbstractUserAdapter; import org.keycloak.storage.adapter.AbstractUserAdapterFederatedStorage; -import org.keycloak.storage.user.UserCredentialValidatorProvider; import org.keycloak.storage.user.UserLookupProvider; import org.keycloak.storage.user.UserQueryProvider; diff --git a/wildfly/server-subsystem/src/main/resources/subsystem-templates/keycloak-datasources.xml b/wildfly/server-subsystem/src/main/resources/subsystem-templates/keycloak-datasources.xml index c823e4f3a3f8..cd7fc81abdf7 100755 --- a/wildfly/server-subsystem/src/main/resources/subsystem-templates/keycloak-datasources.xml +++ b/wildfly/server-subsystem/src/main/resources/subsystem-templates/keycloak-datasources.xml @@ -21,22 +21,22 @@ org.jboss.as.connector - - jdbc:h2:mem:test;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE + + jdbc:h2:mem:test;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE h2 sa sa - - - + + + h2 sa sa - + org.h2.jdbcx.JdbcDataSource @@ -46,12 +46,12 @@ - jdbc:h2:${jboss.server.data.dir}/keycloak;AUTO_SERVER=TRUE + jdbc:h2:${jboss.server.data.dir}/keycloak;AUTO_SERVER=TRUE - jdbc:h2:${jboss.server.data.dir}/../../shared-database/keycloak;AUTO_SERVER=TRUE + jdbc:h2:${jboss.server.data.dir}/../../shared-database/keycloak;AUTO_SERVER=TRUE