package org.keycloak.model.test;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.ws.rs.core.MultivaluedMap;
import org.jboss.resteasy.specimpl.MultivaluedMapImpl;
import org.jboss.resteasy.spi.ResteasyProviderFactory;
import org.junit.Assert;
import org.junit.Before;
import org.junit.FixMethodOrder;
import org.junit.Test;
import org.junit.runners.MethodSorters;
import org.keycloak.models.AuthenticationLinkModel;
import org.keycloak.models.AuthenticationProviderModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.PasswordPolicy;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserCredentialModel;
import org.keycloak.models.UserModel;
import org.keycloak.representations.idm.CredentialRepresentation;
import org.keycloak.services.managers.AuthenticationManager;
import org.keycloak.authentication.AuthProviderConstants;
import org.keycloak.authentication.AuthenticationProviderException;
import org.keycloak.authentication.AuthenticationProviderManager;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
public class AuthProvidersExternalModelTest extends AbstractModelTest {
private RealmModel realm1;
private RealmModel realm2;
private AuthenticationManager am;
@Before
@Override
public void before() throws Exception {
super.before();
// Create 2 realms and user in realm1
realm1 = realmManager.createRealm("realm1");
realm1.setBruteForceProtected(false);
realm2 = realmManager.createRealm("realm2");
realm2.setBruteForceProtected(false);
realm1.addRequiredCredential(CredentialRepresentation.PASSWORD);
realm2.addRequiredCredential(CredentialRepresentation.PASSWORD);
realm1.setAuthenticationProviders(Arrays.asList(AuthenticationProviderModel.DEFAULT_PROVIDER));
realm2.setAuthenticationProviders(Arrays.asList(AuthenticationProviderModel.DEFAULT_PROVIDER));
UserModel john = realm1.addUser("john");
john.setEnabled(true);
john.setFirstName("John");
john.setLastName("Doe");
john.setEmail("john@email.org");
UserCredentialModel credential = new UserCredentialModel();
credential.setType(CredentialRepresentation.PASSWORD);
credential.setValue("password");
john.updateCredential(credential);
am = new AuthenticationManager(providerSession);
}
@Test
public void testExternalModelAuthentication() {
MultivaluedMap<String, String> formData = createFormData("john", "password");
// Authenticate user with realm1
Assert.assertEquals(AuthenticationManager.AuthenticationStatus.SUCCESS, am.authenticateForm(null, realm1, formData));
// Verify that user doesn't exists in realm2 and can't authenticate here
Assert.assertEquals(AuthenticationManager.AuthenticationStatus.INVALID_USER, am.authenticateForm(null, realm2, formData));
Assert.assertNull(realm2.getUser("john"));
// Add externalModel authenticationProvider into realm2 and point to realm1
setupAuthenticationProviders();
try {
// this is needed for externalModel provider
ResteasyProviderFactory.pushContext(KeycloakSession.class, identitySession);
// Authenticate john in realm2 and verify that now he exists here.
Assert.assertEquals(AuthenticationManager.AuthenticationStatus.SUCCESS, am.authenticateForm(null, realm2, formData));
UserModel john2 = realm2.getUser("john");
Assert.assertNotNull(john2);
Assert.assertEquals("john", john2.getLoginName());
Assert.assertEquals("John", john2.getFirstName());
Assert.assertEquals("Doe", john2.getLastName());
Assert.assertEquals("john@email.org", john2.getEmail());
// Verify link exists
AuthenticationLinkModel authLink = john2.getAuthenticationLink();
Assert.assertNotNull(authLink);
Assert.assertEquals(authLink.getAuthProvider(), AuthProviderConstants.PROVIDER_NAME_EXTERNAL_MODEL);
Assert.assertEquals(authLink.getAuthUserId(), realm1.getUser("john").getId());
} finally {
ResteasyProviderFactory.clearContextData();
}
}
@Test
public void testExternalModelPasswordUpdate() {
// Add externalModel authenticationProvider into realm2 and point to realm1
setupAuthenticationProviders();
// Add john to realm2 and set authentication link
UserModel john = realm2.addUser("john");
john.setEnabled(true);
john.setAuthenticationLink(new AuthenticationLinkModel(AuthProviderConstants.PROVIDER_NAME_EXTERNAL_MODEL, realm1.getUser("john").getId()));
try {
// this is needed for externalModel provider
ResteasyProviderFactory.pushContext(KeycloakSession.class, identitySession);
// Change credential via realm2 and validate that they are changed also in realm1
AuthenticationProviderManager authProviderManager = AuthenticationProviderManager.getManager(realm2, providerSession);
try {
Assert.assertTrue(authProviderManager.updatePassword(john, "password-updated"));
} catch (AuthenticationProviderException ape) {
ape.printStackTrace();
Assert.fail("Error not expected");
}
MultivaluedMap<String, String> formData = createFormData("john", "password-updated");
Assert.assertEquals(AuthenticationManager.AuthenticationStatus.SUCCESS, am.authenticateForm(null, realm1, formData));
Assert.assertEquals(AuthenticationManager.AuthenticationStatus.SUCCESS, am.authenticateForm(null, realm2, formData));
// Switch to disallow password update propagation to realm1
setPasswordUpdateForProvider(false, AuthProviderConstants.PROVIDER_NAME_EXTERNAL_MODEL, realm2);
// Change credential and validate that password is updated just for realm2
try {
Assert.assertFalse(authProviderManager.updatePassword(john, "password-updated2"));
} catch (AuthenticationProviderException ape) {
ape.printStackTrace();
Assert.fail("Error not expected");
}
formData = createFormData("john", "password-updated2");
Assert.assertEquals(AuthenticationManager.AuthenticationStatus.INVALID_CREDENTIALS, am.authenticateForm(null, realm1, formData));
Assert.assertEquals(AuthenticationManager.AuthenticationStatus.INVALID_CREDENTIALS, am.authenticateForm(null, realm2, formData));
// Allow passwordUpdate propagation again
setPasswordUpdateForProvider(true, AuthProviderConstants.PROVIDER_NAME_EXTERNAL_MODEL, realm2);
// Set passwordPolicy for realm1 and verify that password update fail
realm1.setPasswordPolicy(new PasswordPolicy("length(8)"));
try {
authProviderManager.updatePassword(john, "passw");
Assert.fail("Update not expected to pass");
} catch (AuthenticationProviderException ape) {
}
} finally {
ResteasyProviderFactory.clearContextData();
}
}
/**
* Setup authentication providers into realm2
*/
private void setupAuthenticationProviders() {
AuthenticationProviderModel ap1 = new AuthenticationProviderModel(AuthProviderConstants.PROVIDER_NAME_MODEL, true, Collections.EMPTY_MAP);
Map<String,String> config = new HashMap<String,String>();
config.put(AuthProviderConstants.EXTERNAL_REALM_ID, "realm1");
AuthenticationProviderModel ap2 = new AuthenticationProviderModel(AuthProviderConstants.PROVIDER_NAME_EXTERNAL_MODEL, true, config);
realm2.setAuthenticationProviders(Arrays.asList(ap1, ap2));
}
public static void setPasswordUpdateForProvider(boolean isPasswordUpdate, String providerName, RealmModel realm) {
List<AuthenticationProviderModel> authProviders = realm.getAuthenticationProviders();
for (AuthenticationProviderModel authProvider : authProviders) {
if (providerName.equals(authProvider.getProviderName())) {
authProvider.setPasswordUpdateSupported(isPasswordUpdate);
break;
}
}
realm.setAuthenticationProviders(authProviders);
}
public static MultivaluedMap<String, String> createFormData(String username, String password) {
MultivaluedMap<String, String> formData = new MultivaluedMapImpl<String, String>();
formData.add("username", username);
formData.add(CredentialRepresentation.PASSWORD, password);
return formData;
}
}