KEYCLOAK-12340 KEYCLOAK-12386 Regression in credential handling when … (#6668)

This commit is contained in:
Marek Posolda 2020-02-03 19:23:30 +01:00 committed by GitHub
parent 337e8f8fad
commit 154bce5693
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 533 additions and 56 deletions

View file

@ -104,6 +104,18 @@ public interface UserResource {
@Produces(MediaType.APPLICATION_JSON)
List<CredentialRepresentation> credentials();
/**
* Return credential types, which are provided by the user storage where user is stored. Returned values can contain for example "password", "otp" etc.
* This will always return empty list for "local" users, which are not backed by any user storage
*
* @return
*/
@GET
@Path("configured-user-storage-credential-types")
@Produces(MediaType.APPLICATION_JSON)
List<String> getConfiguredUserStorageCredentialTypes();
/**
* Remove a credential for a user
*

View file

@ -133,4 +133,12 @@ public interface UserCredentialManager extends UserCredentialStore {
* @return
*/
CredentialValidationOutput authenticate(KeycloakSession session, RealmModel realm, CredentialInput input);
/**
* Return credential types, which are provided by the user storage where user is stored. Returned values can contain for example "password", "otp" etc.
* This will always return empty list for "local" users, which are not backed by any user storage
*
* @return
*/
List<String> getConfiguredUserStorageCredentialTypes(RealmModel realm, UserModel user);
}

View file

@ -33,6 +33,7 @@ import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserCredentialModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.credential.OTPCredentialModel;
import org.keycloak.services.messages.Messages;
import javax.ws.rs.core.MultivaluedMap;
@ -74,8 +75,9 @@ public class OTPFormAuthenticator extends AbstractUsernameFormAuthenticator impl
String credentialId = inputData.getFirst("selectedCredentialId");
if (credentialId == null || credentialId.isEmpty()) {
credentialId = getCredentialProvider(context.getSession())
.getDefaultCredential(context.getSession(), context.getRealm(), context.getUser()).getId();
OTPCredentialModel defaultOtpCredential = getCredentialProvider(context.getSession())
.getDefaultCredential(context.getSession(), context.getRealm(), context.getUser());
credentialId = defaultOtpCredential==null ? "" : defaultOtpCredential.getId();
}
context.form().setAttribute(SELECTED_OTP_CREDENTIAL_ID, credentialId);
@ -90,7 +92,7 @@ public class OTPFormAuthenticator extends AbstractUsernameFormAuthenticator impl
context.challenge(challengeResponse);
return;
}
boolean valid = getCredentialProvider(context.getSession()).isValid(context.getRealm(),context.getUser(),
boolean valid = context.getSession().userCredentialManager().isValid(context.getRealm(),context.getUser(),
new UserCredentialModel(credentialId, getCredentialProvider(context.getSession()).getType(), otp));
if (!valid) {
context.getEvent().user(userModel)
@ -119,7 +121,7 @@ public class OTPFormAuthenticator extends AbstractUsernameFormAuthenticator impl
@Override
public boolean configuredFor(KeycloakSession session, RealmModel realm, UserModel user) {
return getCredentialProvider(session).isConfiguredFor(realm, user);
return session.userCredentialManager().isConfiguredFor(realm, user, getCredentialProvider(session).getType());
}
@Override

View file

@ -79,7 +79,7 @@ public class UpdateTotp implements RequiredActionProvider, RequiredActionFactory
.createResponse(UserModel.RequiredAction.CONFIGURE_TOTP);
context.challenge(challenge);
return;
} else if (!CredentialValidation.validOTP(challengeResponse, credentialModel, policy.getLookAheadWindow())) {
} else if (!validateOTPCredential(context, challengeResponse, credentialModel, policy)) {
Response challenge = context.form()
.setAttribute("mode", mode)
.setError(Messages.INVALID_TOTP)
@ -91,7 +91,7 @@ public class UpdateTotp implements RequiredActionProvider, RequiredActionFactory
CredentialModel createdCredential = otpCredentialProvider.createCredential(context.getRealm(), context.getUser(), credentialModel);
UserCredentialModel credential = new UserCredentialModel(createdCredential.getId(), otpCredentialProvider.getType(), challengeResponse);
//If the type is HOTP, call verify once to consume the OTP used for registration and increase the counter.
if (OTPCredentialModel.HOTP.equals(credentialModel.getOTPCredentialData().getSubType()) && !otpCredentialProvider.isValid(context.getRealm(), context.getUser(), credential)) {
if (OTPCredentialModel.HOTP.equals(credentialModel.getOTPCredentialData().getSubType()) && !context.getSession().userCredentialManager().isValid(context.getRealm(), context.getUser(), credential)) {
Response challenge = context.form()
.setAttribute("mode", mode)
.setError(Messages.INVALID_TOTP)
@ -103,6 +103,12 @@ public class UpdateTotp implements RequiredActionProvider, RequiredActionFactory
}
// Use separate method, so it's possible to override in the custom provider
protected boolean validateOTPCredential(RequiredActionContext context, String token, OTPCredentialModel credentialModel, OTPPolicy policy) {
return CredentialValidation.validOTP(token, credentialModel, policy.getLookAheadWindow());
}
@Override
public void close() {

View file

@ -17,6 +17,7 @@
package org.keycloak.credential;
import org.jboss.logging.Logger;
import org.keycloak.common.util.ObjectUtil;
import org.keycloak.common.util.Time;
import org.keycloak.models.RequiredActionProviderModel;
import org.keycloak.models.credential.OTPCredentialModel;
@ -109,6 +110,10 @@ public class OTPCredentialProvider implements CredentialProvider<OTPCredentialMo
if (challengeResponse == null) {
return false;
}
if (ObjectUtil.isBlank(credentialInput.getCredentialId())) {
logger.debugf("CredentialId is null when validating credential of user %s", user.getUsername());
return false;
}
CredentialModel credential = getCredentialStore().getStoredCredentialById(realm, user, credentialInput.getCredentialId());
OTPCredentialModel otpCredentialModel = OTPCredentialModel.createFromCredentialModel(credential);

View file

@ -285,28 +285,49 @@ public class UserCredentialStoreManager implements UserCredentialManager, OnUser
@Override
public boolean isConfiguredFor(RealmModel realm, UserModel user, String type) {
UserStorageCredentialConfigured userStorageConfigured = isConfiguredThroughUserStorage(realm, user, type);
// Check if we can rely just on userStorage to decide if credential is configured for the user or not
switch (userStorageConfigured) {
case CONFIGURED: return true;
case USER_STORAGE_DISABLED: return false;
}
// Check locally as a fallback
return isConfiguredLocally(realm, user, type);
}
private enum UserStorageCredentialConfigured {
CONFIGURED,
USER_STORAGE_DISABLED,
NOT_CONFIGURED
}
private UserStorageCredentialConfigured isConfiguredThroughUserStorage(RealmModel realm, UserModel user, String type) {
if (!StorageId.isLocalStorage(user)) {
String providerId = StorageId.resolveProviderId(user);
UserStorageProvider provider = UserStorageManager.getStorageProvider(session, realm, providerId);
if (provider instanceof CredentialInputValidator) {
if (!UserStorageManager.isStorageProviderEnabled(realm, providerId)) return false;
if (!UserStorageManager.isStorageProviderEnabled(realm, providerId)) return UserStorageCredentialConfigured.USER_STORAGE_DISABLED;
CredentialInputValidator validator = (CredentialInputValidator) provider;
if (validator.supportsCredentialType(type) && validator.isConfiguredFor(realm, user, type)) {
return true;
return UserStorageCredentialConfigured.CONFIGURED;
}
}
} else {
if (user.getFederationLink() != null) {
UserStorageProvider provider = UserStorageManager.getStorageProvider(session, realm, user.getFederationLink());
if (provider instanceof CredentialInputValidator) {
if (!UserStorageManager.isStorageProviderEnabled(realm, user.getFederationLink())) return false;
if (((CredentialInputValidator) provider).isConfiguredFor(realm, user, type)) return true;
if (!UserStorageManager.isStorageProviderEnabled(realm, user.getFederationLink())) return UserStorageCredentialConfigured.USER_STORAGE_DISABLED;
if (((CredentialInputValidator) provider).isConfiguredFor(realm, user, type)) return UserStorageCredentialConfigured.CONFIGURED;
}
}
}
return isConfiguredLocally(realm, user, type);
return UserStorageCredentialConfigured.NOT_CONFIGURED;
}
@Override
@ -350,6 +371,15 @@ public class UserCredentialStoreManager implements UserCredentialManager, OnUser
}
}
@Override
public List<String> getConfiguredUserStorageCredentialTypes(RealmModel realm, UserModel user) {
List<CredentialProvider> credentialProviders = getCredentialProviders(session, realm, CredentialProvider.class);
return credentialProviders.stream().map(CredentialProvider::getType)
.filter(credentialType -> UserStorageCredentialConfigured.CONFIGURED == isConfiguredThroughUserStorage(realm, user, credentialType))
.collect(Collectors.toList());
}
@Override
public void close() {

View file

@ -27,8 +27,6 @@ import org.keycloak.common.ClientConnection;
import org.keycloak.common.Profile;
import org.keycloak.common.util.Time;
import org.keycloak.credential.CredentialModel;
import org.keycloak.credential.CredentialProvider;
import org.keycloak.credential.PasswordCredentialProvider;
import org.keycloak.email.EmailException;
import org.keycloak.email.EmailTemplateProvider;
import org.keycloak.events.Details;
@ -47,6 +45,7 @@ import org.keycloak.models.ModelDuplicateException;
import org.keycloak.models.ModelException;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserConsentModel;
import org.keycloak.models.UserCredentialModel;
import org.keycloak.models.UserLoginFailureModel;
import org.keycloak.models.UserManager;
import org.keycloak.models.UserModel;
@ -80,7 +79,6 @@ import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
import javax.ws.rs.DefaultValue;
import javax.ws.rs.GET;
import javax.ws.rs.NotFoundException;
import javax.ws.rs.NotSupportedException;
import javax.ws.rs.POST;
import javax.ws.rs.PUT;
@ -598,8 +596,7 @@ public class UserResource {
}
try {
PasswordCredentialProvider provider = (PasswordCredentialProvider)session.getProvider(CredentialProvider.class, "keycloak-password");
provider.createCredential(realm, user, cred.getValue());
session.userCredentialManager().updateCredential(realm, user, UserCredentialModel.password(cred.getValue(), false));
} catch (IllegalStateException ise) {
throw new BadRequestException("Resetting to N old passwords is not allowed.");
} catch (ReadOnlyException mre) {
@ -627,6 +624,24 @@ public class UserResource {
}
/**
* Return credential types, which are provided by the user storage where user is stored. Returned values can contain for example "password", "otp" etc.
* This will always return empty list for "local" users, which are not backed by any user storage
*
* @return
*/
@GET
@Path("configured-user-storage-credential-types")
@NoCache
@Produces(MediaType.APPLICATION_JSON)
public List<String> getConfiguredUserStorageCredentialTypes() {
// This has "requireManage" due the compatibility with "credentials()" endpoint. Strictly said, it is reading endpoint, not writing,
// so may be revisited if to rather use "requireView" here in the future.
auth.users().requireManage(user);
return session.userCredentialManager().getConfiguredUserStorageCredentialTypes(realm, user);
}
/**
* Remove a credential for a user
*

View file

@ -27,12 +27,15 @@ import org.keycloak.models.RealmModel;
import org.keycloak.models.RoleModel;
import org.keycloak.models.UserCredentialModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.credential.OTPCredentialModel;
import org.keycloak.models.credential.PasswordCredentialModel;
import org.keycloak.storage.UserStorageProvider;
import org.keycloak.storage.user.UserLookupProvider;
import org.keycloak.storage.user.UserRegistrationProvider;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
@ -49,6 +52,12 @@ public class DummyUserFederationProvider implements UserStorageProvider,
private KeycloakSession session;
private ComponentModel component;
// Hardcoded password of test-user
public static final String HARDCODED_PASSWORD = "secret";
// Hardcoded otp code, which will be always considered valid for the test-user
public static final String HARDCODED_OTP = "123456";
public DummyUserFederationProvider(KeycloakSession session, ComponentModel component, Map<String, UserModel> users) {
@ -104,7 +113,7 @@ public class DummyUserFederationProvider implements UserStorageProvider,
}
public Set<String> getSupportedCredentialTypes() {
return Collections.singleton(PasswordCredentialModel.TYPE);
return new HashSet<>(Arrays.asList(PasswordCredentialModel.TYPE, OTPCredentialModel.TYPE));
}
@Override
@ -114,7 +123,7 @@ public class DummyUserFederationProvider implements UserStorageProvider,
@Override
public boolean isConfiguredFor(RealmModel realm, UserModel user, String credentialType) {
if (!PasswordCredentialModel.TYPE.equals(credentialType)) return false;
if (!supportsCredentialType(credentialType)) return false;
if (user.getUsername().equals("test-user")) {
return true;
@ -126,7 +135,11 @@ public class DummyUserFederationProvider implements UserStorageProvider,
@Override
public boolean isValid(RealmModel realm, UserModel user, CredentialInput credentialInput) {
if (user.getUsername().equals("test-user")) {
return "secret".equals(credentialInput.getChallengeResponse());
if (PasswordCredentialModel.TYPE.equals(credentialInput.getType())) {
return HARDCODED_PASSWORD.equals(credentialInput.getChallengeResponse());
} else if (OTPCredentialModel.TYPE.equals(credentialInput.getType())) {
return HARDCODED_OTP.equals(credentialInput.getChallengeResponse());
}
}
return false;
}

View file

@ -31,6 +31,7 @@ import org.keycloak.testsuite.pages.AccountPasswordPage;
import org.keycloak.testsuite.pages.AccountUpdateProfilePage;
import org.keycloak.testsuite.pages.AppPage;
import org.keycloak.testsuite.pages.LoginPage;
import org.keycloak.testsuite.pages.LoginPasswordUpdatePage;
import org.keycloak.testsuite.pages.OAuthGrantPage;
import org.keycloak.testsuite.pages.RegisterPage;
import org.keycloak.testsuite.util.LDAPRule;
@ -64,6 +65,9 @@ public abstract class AbstractLDAPTest extends AbstractTestRealmKeycloakTest {
@Page
protected OAuthGrantPage grantPage;
@Page
protected LoginPasswordUpdatePage requiredActionChangePasswordPage;
@Override

View file

@ -23,6 +23,7 @@ import org.junit.FixMethodOrder;
import org.junit.Test;
import org.junit.runners.MethodSorters;
import org.keycloak.OAuth2Constants;
import org.keycloak.admin.client.resource.UserResource;
import org.keycloak.component.ComponentModel;
import org.keycloak.credential.CredentialModel;
import org.keycloak.models.GroupModel;
@ -38,6 +39,7 @@ import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.models.utils.ModelToRepresentation;
import org.keycloak.representations.AccessToken;
import org.keycloak.representations.idm.ComponentRepresentation;
import org.keycloak.representations.idm.CredentialRepresentation;
import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.services.managers.RealmManager;
import org.keycloak.storage.ReadOnlyException;
@ -66,8 +68,11 @@ import org.keycloak.testsuite.util.OAuthClient;
import javax.naming.AuthenticationException;
import javax.ws.rs.core.Response;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
@ -329,7 +334,7 @@ public class LDAPProvidersIntegrationTest extends AbstractLDAPTest {
}
@Test
public void passwordChangeLdap() throws Exception {
public void ldapPasswordChangeWithAccountConsole() throws Exception {
changePasswordPage.open();
loginPage.login("johnkeycloak", "Password1");
changePasswordPage.changePassword("Password1", "New-password1", "New-password1");
@ -352,6 +357,77 @@ public class LDAPProvidersIntegrationTest extends AbstractLDAPTest {
Assert.assertEquals("Your password has been updated.", profilePage.getSuccess());
}
// KEYCLOAK-12340
@Test
public void ldapPasswordChangeWithAdminEndpointAndRequiredAction() throws Exception {
String username = "adminEndpointAndRequiredActionTest";
String email = username + "@email.cz";
// Register new LDAP user with password, logout user
loginPage.open();
loginPage.clickRegister();
registerPage.assertCurrent();
registerPage.register("firstName", "lastName", email,
username, "Password1", "Password1");
Assert.assertEquals(AppPage.RequestType.AUTH_RESPONSE, appPage.getRequestType());
appPage.logout();
// Test admin endpoint. Assert federated endpoint returns password in LDAP "supportedCredentials", but there is no stored password
UserResource user = ApiUtil.findUserByUsernameId(testRealm(), username);
assertPasswordConfiguredThroughLDAPOnly(user);
// Update password through admin REST endpoint. Assert user can authenticate with the new password
ApiUtil.resetUserPassword(user, "Password1-updated1", false);
loginPage.open();
loginSuccessAndLogout(username, "Password1-updated1");
// Test admin endpoint. Assert federated endpoint returns password in LDAP "supportedCredentials", but there is no stored password
assertPasswordConfiguredThroughLDAPOnly(user);
// Test this just for the import mode. No-import mode doesn't support requiredActions right now
if (isImportEnabled()) {
// Update password through required action.
UserRepresentation user2 = user.toRepresentation();
user2.setRequiredActions(Arrays.asList(UserModel.RequiredAction.UPDATE_PASSWORD.toString()));
user.update(user2);
loginPage.open();
loginPage.login(username, "Password1-updated1");
requiredActionChangePasswordPage.assertCurrent();
requiredActionChangePasswordPage.changePassword("Password1-updated2", "Password1-updated2");
appPage.assertCurrent();
appPage.logout();
// Assert user can authenticate with the new password
loginSuccessAndLogout(username, "Password1-updated2");
// Test admin endpoint. Assert federated endpoint returns password in LDAP "supportedCredentials", but there is no stored password
assertPasswordConfiguredThroughLDAPOnly(user);
}
}
// Use admin REST endpoints
private void assertPasswordConfiguredThroughLDAPOnly(UserResource user) {
// Assert password not stored locally
List<CredentialRepresentation> storedCredentials = user.credentials();
for (CredentialRepresentation credential : storedCredentials) {
Assert.assertFalse(PasswordCredentialModel.TYPE.equals(credential.getType()));
}
// Assert password is stored in the LDAP
List<String> userStorageCredentials = user.getConfiguredUserStorageCredentialTypes();
Assert.assertTrue(userStorageCredentials.contains(PasswordCredentialModel.TYPE));
}
@Test
public void registerExistingLdapUser() {
loginPage.open();
@ -908,6 +984,25 @@ public class LDAPProvidersIntegrationTest extends AbstractLDAPTest {
} catch (AuthenticationException ex) {
throw new RuntimeException(ex);
}
});
// Test admin REST endpoints
UserResource userResource = ApiUtil.findUserByUsernameId(testRealm(), "johnkeycloak");
// Assert password is stored locally
List<String> storedCredentials = userResource.credentials().stream()
.map(CredentialRepresentation::getType)
.collect(Collectors.toList());
Assert.assertTrue(storedCredentials.contains(PasswordCredentialModel.TYPE));
// Assert password is supported in the LDAP too.
List<String> userStorageCredentials = userResource.getConfiguredUserStorageCredentialTypes();
Assert.assertTrue(userStorageCredentials.contains(PasswordCredentialModel.TYPE));
testingClient.server().run(session -> {
LDAPTestContext ctx = LDAPTestContext.init(session);
RealmModel appRealm = ctx.getRealm();
UserModel user = session.users().getUserByUsername("johnkeycloak", appRealm);
// User is deleted just locally
Assert.assertTrue(session.users().removeUser(appRealm, user));

View file

@ -0,0 +1,222 @@
/*
* Copyright 2019 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.testsuite.federation.storage;
import java.io.IOException;
import java.net.URISyntaxException;
import java.util.Collections;
import java.util.List;
import org.jboss.arquillian.graphene.page.Page;
import org.junit.Before;
import org.junit.Test;
import org.keycloak.OAuth2Constants;
import org.keycloak.admin.client.resource.UserResource;
import org.keycloak.common.util.MultivaluedHashMap;
import org.keycloak.models.UserModel;
import org.keycloak.models.credential.OTPCredentialModel;
import org.keycloak.models.credential.PasswordCredentialModel;
import org.keycloak.models.utils.TimeBasedOTP;
import org.keycloak.representations.idm.ComponentRepresentation;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.storage.UserStorageProvider;
import org.keycloak.testsuite.AbstractTestRealmKeycloakTest;
import org.keycloak.testsuite.Assert;
import org.keycloak.testsuite.admin.ApiUtil;
import org.keycloak.testsuite.federation.DummyUserFederationProvider;
import org.keycloak.testsuite.federation.DummyUserFederationProviderFactory;
import org.keycloak.testsuite.pages.AppPage;
import org.keycloak.testsuite.pages.LoginConfigTotpPage;
import org.keycloak.testsuite.pages.LoginPage;
import org.keycloak.testsuite.pages.LoginTotpPage;
import org.keycloak.testsuite.util.UserBuilder;
import static org.keycloak.storage.UserStorageProviderModel.IMPORT_ENABLED;
import static org.keycloak.testsuite.federation.storage.UserStorageTest.addComponent;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class UserStorageOTPTest extends AbstractTestRealmKeycloakTest {
@Page
protected LoginPage loginPage;
@Page
protected LoginTotpPage loginTotpPage;
@Page
protected LoginConfigTotpPage loginConfigTotpPage;
@Page
protected AppPage appPage;
protected TimeBasedOTP totp = new TimeBasedOTP();
@Override
public void configureTestRealm(RealmRepresentation testRealm) {
}
@Before
public void addProvidersBeforeTest() throws URISyntaxException, IOException {
ComponentRepresentation dummyProvider = new ComponentRepresentation();
dummyProvider.setName("dummy");
dummyProvider.setId(DummyUserFederationProviderFactory.PROVIDER_NAME);
dummyProvider.setProviderId(DummyUserFederationProviderFactory.PROVIDER_NAME);
dummyProvider.setProviderType(UserStorageProvider.class.getName());
dummyProvider.setConfig(new MultivaluedHashMap<>());
dummyProvider.getConfig().putSingle("priority", Integer.toString(0));
dummyProvider.getConfig().putSingle(IMPORT_ENABLED, Boolean.toString(false));
addComponent(testRealm(), getCleanup(), dummyProvider);
UserRepresentation user = UserBuilder.create()
.username("test-user")
.email("test-user@something.org")
.build();
String testUserId = ApiUtil.createUserWithAdminClient(testRealm(), user);
getCleanup().addUserId(testUserId);
}
@Test
public void testCredentialsThroughRESTAPI() {
// Test that test-user has federation link on him
UserResource user = ApiUtil.findUserByUsernameId(testRealm(), "test-user");
Assert.assertEquals(DummyUserFederationProviderFactory.PROVIDER_NAME, user.toRepresentation().getFederationLink());
// Test that both "password" and "otp" are configured for the test-user
List<String> userStorageCredentialTypes = user.getConfiguredUserStorageCredentialTypes();
Assert.assertNames(userStorageCredentialTypes, PasswordCredentialModel.TYPE, OTPCredentialModel.TYPE);
}
@Test
public void testAuthentication() {
// Test that user is required to provide OTP credential during authentication
loginPage.open();
loginPage.login("test-user", DummyUserFederationProvider.HARDCODED_PASSWORD);
loginTotpPage.assertCurrent();
loginTotpPage.login("654321");
loginTotpPage.assertCurrent();
Assert.assertEquals("Invalid authenticator code.", loginPage.getError());
loginTotpPage.login(DummyUserFederationProvider.HARDCODED_OTP);
appPage.assertCurrent();
Assert.assertEquals(AppPage.RequestType.AUTH_RESPONSE, appPage.getRequestType());
Assert.assertNotNull(oauth.getCurrentQuery().get(OAuth2Constants.CODE));
}
@Test
public void testUpdateOTP() {
// Add requiredAction to the user for update OTP
UserResource user = ApiUtil.findUserByUsernameId(testRealm(), "test-user");
UserRepresentation userRep = user.toRepresentation();
userRep.setRequiredActions(Collections.singletonList(UserModel.RequiredAction.CONFIGURE_TOTP.toString()));
user.update(userRep);
// Authenticate as the user
loginPage.open();
loginPage.login("test-user", DummyUserFederationProvider.HARDCODED_PASSWORD);
loginTotpPage.assertCurrent();
loginTotpPage.login(DummyUserFederationProvider.HARDCODED_OTP);
// User should be required to update OTP
loginConfigTotpPage.assertCurrent();
// Dummy OTP code won't work when configure new OTP
loginConfigTotpPage.configure(DummyUserFederationProvider.HARDCODED_OTP);
Assert.assertEquals("Invalid authenticator code.", loginConfigTotpPage.getError());
// This will save the credential to the local DB
String totpSecret = loginConfigTotpPage.getTotpSecret();
log.infof("Totp Secret: %s", totpSecret);
String totpCode = totp.generateTOTP(totpSecret);
loginConfigTotpPage.configure(totpCode);
appPage.assertCurrent();
// Logout
appPage.logout();
// Authenticate as the user again with the dummy OTP should still work
loginPage.open();
loginPage.login("test-user", DummyUserFederationProvider.HARDCODED_PASSWORD);
loginTotpPage.assertCurrent();
loginTotpPage.login(DummyUserFederationProvider.HARDCODED_OTP);
appPage.assertCurrent();
appPage.logout();
// Authenticate with the new OTP code should work as well
loginPage.open();
loginPage.login("test-user", DummyUserFederationProvider.HARDCODED_PASSWORD);
loginTotpPage.assertCurrent();
loginTotpPage.login(totp.generateTOTP(totpSecret));
appPage.assertCurrent();
appPage.logout();
}
@Test
public void testNormalUser() {
// Add some other user to the KEycloak
UserRepresentation user = UserBuilder.create()
.username("test-user2")
.email("test-user2@something.org")
.build();
String testUserId = ApiUtil.createUserWithAdminClient(testRealm(), user);
getCleanup().addUserId(testUserId);
// Assert he has federation link on him
UserResource userResource = ApiUtil.findUserByUsernameId(testRealm(), "test-user2");
Assert.assertEquals(DummyUserFederationProviderFactory.PROVIDER_NAME, userResource.toRepresentation().getFederationLink());
// Assert no userStorage supported credentials shown through admin REST API for that user. For this user, the validation of password and OTP is not delegated
// to the dummy user storage provider
Assert.assertTrue(userResource.getConfiguredUserStorageCredentialTypes().isEmpty());
// Update password
ApiUtil.resetUserPassword(userResource, "pass", false);
// Authenticate as the user. Only the password will be required for him
loginPage.open();
loginPage.login("test-user2", "pass");
appPage.assertCurrent();
Assert.assertEquals(AppPage.RequestType.AUTH_RESPONSE, appPage.getRequestType());
Assert.assertNotNull(oauth.getCurrentQuery().get(OAuth2Constants.CODE));
}
}

View file

@ -1548,7 +1548,11 @@ disable-credential-types=Disable Credential Types
credentials.disable.tooltip=Click button to disable selected credential types
credential-types=Credential Types
manage-user-password=Manage Password
supported-user-storage-credential-types=Supported User Storage Credential Types
supported-user-storage-credential-types.tooltip=Credential types, which are provided by User Storage Provider. Validation and eventually update of the credentials of those types can be delegated to the User Storage Provider based on the configuration and implementation of the particular provider.
provided-by=Provided By
manage-credentials=Manage Credentials
manage-credentials.tooltip=Credentials, which are not provided by the user storage. They are saved in the local database.
disable-credentials=Disable Credentials
credential-reset-actions=Credential Reset
credential-reset-actions-timeout=Expires In

View file

@ -331,6 +331,46 @@ module.controller('UserTabCtrl', function($scope, $location, Dialog, Notificatio
};
});
function loadUserStorageLink(realm, user, console, Components, UserStorageOperations, $scope, $location) {
if(user.federationLink) {
console.log("federationLink is not null. It is " + user.federationLink);
if ($scope.access.viewRealm) {
Components.get({realm: realm.realm, componentId: user.federationLink}, function (link) {
$scope.federationLinkName = link.name;
$scope.federationLink = "#/realms/" + realm.realm + "/user-storage/providers/" + link.providerId + "/" + link.id;
});
} else {
// KEYCLOAK-4328
UserStorageOperations.simpleName.get({realm: realm.realm, componentId: user.federationLink}, function (link) {
$scope.federationLinkName = link.name;
$scope.federationLink = $location.absUrl();
})
}
} else {
console.log("federationLink is null");
}
if(user.origin) {
if ($scope.access.viewRealm) {
Components.get({realm: realm.realm, componentId: user.origin}, function (link) {
$scope.originName = link.name;
$scope.originLink = "#/realms/" + realm.realm + "/user-storage/providers/" + link.providerId + "/" + link.id;
})
}
else {
// KEYCLOAK-4328
UserStorageOperations.simpleName.get({realm: realm.realm, componentId: user.origin}, function (link) {
$scope.originName = link.name;
$scope.originLink = $location.absUrl();
})
}
} else {
console.log("origin is null");
}
};
module.controller('UserDetailCtrl', function($scope, realm, user, BruteForceUser, User,
Components,
UserImpersonation, RequiredActions,
@ -359,42 +399,9 @@ module.controller('UserDetailCtrl', function($scope, realm, user, BruteForceUser
}
});
};
if(user.federationLink) {
console.log("federationLink is not null. It is " + user.federationLink);
if ($scope.access.viewRealm) {
Components.get({realm: realm.realm, componentId: user.federationLink}, function (link) {
$scope.federationLinkName = link.name;
$scope.federationLink = "#/realms/" + realm.realm + "/user-storage/providers/" + link.providerId + "/" + link.id;
});
} else {
// KEYCLOAK-4328
UserStorageOperations.simpleName.get({realm: realm.realm, componentId: user.federationLink}, function (link) {
$scope.federationLinkName = link.name;
$scope.federationLink = $location.absUrl();
})
}
loadUserStorageLink(realm, user, console, Components, UserStorageOperations, $scope, $location);
} else {
console.log("federationLink is null");
}
if(user.origin) {
if ($scope.access.viewRealm) {
Components.get({realm: realm.realm, componentId: user.origin}, function (link) {
$scope.originName = link.name;
$scope.originLink = "#/realms/" + realm.realm + "/user-storage/providers/" + link.providerId + "/" + link.id;
})
}
else {
// KEYCLOAK-4328
UserStorageOperations.simpleName.get({realm: realm.realm, componentId: user.origin}, function (link) {
$scope.originName = link.name;
$scope.originLink = $location.absUrl();
})
}
} else {
console.log("origin is null");
}
console.log('realm brute force? ' + realm.bruteForceProtected)
$scope.temporarilyDisabled = false;
var isDisabled = function () {
@ -514,7 +521,8 @@ module.controller('UserDetailCtrl', function($scope, realm, user, BruteForceUser
}
});
module.controller('UserCredentialsCtrl', function($scope, realm, user, $route, RequiredActions, User, UserExecuteActionsEmail, UserCredentials, Notifications, Dialog, TimeUnit2) {
module.controller('UserCredentialsCtrl', function($scope, realm, user, $route, $location, RequiredActions, User, UserExecuteActionsEmail,
UserCredentials, Notifications, Dialog, TimeUnit2, Components, UserStorageOperations) {
console.log('UserCredentialsCtrl');
$scope.hasPassword = false;
@ -523,6 +531,16 @@ module.controller('UserCredentialsCtrl', function($scope, realm, user, $route, R
loadCredentials();
loadUserStorageLink(realm, user, console, Components, UserStorageOperations, $scope, $location);
$scope.getUserStorageProviderName = function() {
return user.federationLink ? $scope.federationLinkName : $scope.originName;
}
$scope.getUserStorageProviderLink = function() {
return user.federationLink ? $scope.federationLink : $scope.originLink;
}
$scope.keys = function(object) {
return object ? Object.keys(object) : [];
}
@ -648,6 +666,14 @@ module.controller('UserCredentialsCtrl', function($scope, realm, user, $route, R
Notifications.error("Error while loading user credentials. See console for more information.");
console.log(err);
});
UserCredentials.getConfiguredUserStorageCredentialTypes({ realm: realm.realm, userId: user.id }, null, function(userStorageCredentialTypes) {
$scope.userStorageCredentialTypes = userStorageCredentialTypes;
$scope.hasPassword = $scope.hasPassword || userStorageCredentialTypes.lastIndexOf("password") > -1;
}, function(err) {
Notifications.error("Error while loading user storage credentials. See console for more information.");
console.log(err);
});
}
$scope.resetPassword = function() {

View file

@ -659,6 +659,11 @@ module.factory('UserCredentials', function($resource) {
userId : '@userId'
}).query;
credentials.getConfiguredUserStorageCredentialTypes = $resource(authUrl + '/admin/realms/:realm/users/:userId/configured-user-storage-credential-types', {
realm : '@realm',
userId : '@userId'
}).query;
credentials.deleteCredential = $resource(authUrl + '/admin/realms/:realm/users/:userId/credentials/:credentialId', {
realm : '@realm',
userId : '@userId',

View file

@ -7,8 +7,38 @@
<kc-tabs-user></kc-tabs-user>
<form class="form-horizontal" name="userForm" novalidate>
<fieldset class="border-top" data-ng-show="user.federationLink || user.origin">
<legend>
<span class="text">{{:: 'supported-user-storage-credential-types' | translate}}</span>
<kc-tooltip>{{:: 'supported-user-storage-credential-types.tooltip' | translate}}</kc-tooltip>
</legend>
<table class="datatable table table-bordered dataTable no-footer credentials-table">
<thead>
<tr>
<th>{{:: 'type' | translate}}</th>
<th>{{:: 'provided-by' | translate}}</th>
</tr>
</thead>
<tr ng-repeat="credential in userStorageCredentialTypes">
<td>
<b>{{credential}}</b>
</td>
<td>
<a href="{{getUserStorageProviderLink()}}">{{getUserStorageProviderName()}}</a>
</td>
</tr>
</table>
</fieldset>
<fieldset class="border-top">
<legend><span class="text">{{:: 'manage-credentials' | translate}}</span></legend>
<legend data-ng-show="user.federationLink || user.origin">
<span class="text">{{:: 'manage-credentials' | translate}}</span>
<kc-tooltip>{{:: 'manage-credentials.tooltip' | translate}}</kc-tooltip>
</legend>
<legend data-ng-hide="user.federationLink || user.origin">
<span class="text">{{:: 'manage-credentials' | translate}}</span>
</legend>
<table class="datatable table table-bordered dataTable no-footer credentials-table">
<thead>
<tr>