/*******************************************************************************
* Cloud Foundry
* Copyright (c) [2009-2016] Pivotal Software, Inc. All Rights Reserved.
*
* This product is licensed to you under the Apache License, Version 2.0 (the "License").
* You may not use this product except in compliance with the License.
*
* This product includes a number of subcomponents with
* separate copyright notices and license terms. Your use of these
* subcomponents is subject to the terms and conditions of the
* subcomponent's license, as noted in the LICENSE file.
*******************************************************************************/
package org.cloudfoundry.identity.uaa.client;
import org.cloudfoundry.identity.uaa.audit.event.EntityDeletedEvent;
import org.cloudfoundry.identity.uaa.authentication.SystemAuthentication;
import org.cloudfoundry.identity.uaa.oauth.client.ClientConstants;
import org.cloudfoundry.identity.uaa.test.JdbcTestBase;
import org.cloudfoundry.identity.uaa.zone.IdentityZoneHolder;
import org.cloudfoundry.identity.uaa.zone.MultitenantJdbcClientDetailsService;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.mockito.ArgumentCaptor;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.oauth2.common.util.RandomValueStringGenerator;
import org.springframework.security.oauth2.provider.ClientAlreadyExistsException;
import org.springframework.security.oauth2.provider.ClientDetails;
import org.springframework.security.oauth2.provider.client.BaseClientDetails;
import org.springframework.util.StringUtils;
import org.yaml.snakeyaml.Yaml;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import static java.util.Collections.singletonList;
import static org.cloudfoundry.identity.uaa.oauth.token.TokenConstants.GRANT_TYPE_REFRESH_TOKEN;
import static org.cloudfoundry.identity.uaa.oauth.token.TokenConstants.GRANT_TYPE_SAML2_BEARER;
import static org.cloudfoundry.identity.uaa.oauth.token.TokenConstants.GRANT_TYPE_USER_TOKEN;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.same;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
public class ClientAdminBootstrapTests extends JdbcTestBase {
private ClientAdminBootstrap bootstrap;
private MultitenantJdbcClientDetailsService clientRegistrationService;
private ClientMetadataProvisioning clientMetadataProvisioning;
@Rule
public ExpectedException exception = ExpectedException.none();
private ApplicationEventPublisher publisher;
@Before
public void setUpClientAdminTests() throws Exception {
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
bootstrap = new ClientAdminBootstrap(encoder);
clientRegistrationService = spy(new MultitenantJdbcClientDetailsService(jdbcTemplate));
clientMetadataProvisioning = new JdbcClientMetadataProvisioning(clientRegistrationService,clientRegistrationService,jdbcTemplate);
bootstrap.setClientRegistrationService(clientRegistrationService);
bootstrap.setClientMetadataProvisioning(clientMetadataProvisioning);
clientRegistrationService.setPasswordEncoder(encoder);
publisher = mock(ApplicationEventPublisher.class);
bootstrap.setApplicationEventPublisher(publisher);
}
@Test
public void testSimpleAddClient() throws Exception {
testSimpleAddClient("foo");
}
public ClientDetails testSimpleAddClient(String clientId) throws Exception {
Map<String, Object> map = createClientMap(clientId);
ClientDetails created = doSimpleTest(map);
assertSet((String) map.get("redirect-uri"), null, created.getRegisteredRedirectUri(), String.class);
return created;
}
public Map<String, Object> createClientMap(String clientId) {
Map<String, Object> map = new HashMap<>();
map.put("id", clientId);
map.put("secret", "bar");
map.put("scope", "openid");
map.put("authorized-grant-types", "authorization_code");
map.put("authorities", "uaa.none");
map.put("redirect-uri", "http://localhost/callback");
return map;
}
@Test
public void client_slated_for_deletion_does_not_get_inserted() throws Exception {
String autoApproveId = "autoapprove-"+new RandomValueStringGenerator().generate().toLowerCase();
testSimpleAddClient(autoApproveId);
reset(clientRegistrationService);
bootstrap = spy(bootstrap);
String clientId = "client-"+new RandomValueStringGenerator().generate().toLowerCase();
Map<String, Map<String, Object>> clients = Collections.singletonMap(clientId, createClientMap(clientId));
bootstrap.setClients(clients);
bootstrap.setAutoApproveClients(singletonList(autoApproveId));
bootstrap.setClientsToDelete(Arrays.asList(clientId, autoApproveId));
bootstrap.afterPropertiesSet();
verify(clientRegistrationService, never()).addClientDetails(any());
verify(clientRegistrationService, never()).updateClientDetails(any());
verify(clientRegistrationService, never()).updateClientSecret(any(), any());
}
@Test
public void test_delete_from_yaml_existing_client() throws Exception {
bootstrap = spy(bootstrap);
String clientId = "client-"+new RandomValueStringGenerator().generate().toLowerCase();
testSimpleAddClient(clientId);
verify(bootstrap, never()).publish(any());
bootstrap.setClientsToDelete(Arrays.asList(clientId));
bootstrap.onApplicationEvent(new ContextRefreshedEvent(mock(ApplicationContext.class)));
ArgumentCaptor<EntityDeletedEvent> captor = ArgumentCaptor.forClass(EntityDeletedEvent.class);
verify(bootstrap, times(1)).publish(captor.capture());
assertNotNull(captor.getValue());
verify(publisher, times(1)).publishEvent(same(captor.getValue()));
assertEquals(clientId, captor.getValue().getObjectId());
assertEquals(clientId, ((ClientDetails)captor.getValue().getDeleted()).getClientId());
assertSame(SystemAuthentication.SYSTEM_AUTHENTICATION, captor.getValue().getAuthentication());
assertNotNull(captor.getValue().getAuditEvent());
}
@Test
public void test_delete_from_yaml_non_existing_client() throws Exception {
ApplicationEventPublisher publisher = mock(ApplicationEventPublisher.class);
bootstrap = spy(bootstrap);
bootstrap.setApplicationEventPublisher(publisher);
String clientId = "client-"+new RandomValueStringGenerator().generate().toLowerCase();
verify(bootstrap, never()).publish(any());
bootstrap.setClientsToDelete(Arrays.asList(clientId));
bootstrap.onApplicationEvent(new ContextRefreshedEvent(mock(ApplicationContext.class)));
verify(bootstrap, never()).publish(any());
verify(publisher, never()).publishEvent(any());
}
public Integer countClients(String clientId) {
return jdbcTemplate.queryForObject("SELECT count(*) FROM oauth_client_details WHERE client_id = ? AND identity_zone_id = ?", Integer.class, clientId, IdentityZoneHolder.get().getId());
}
@Test(expected = InvalidClientDetailsException.class)
public void no_registered_redirect_url_for_auth_code() throws Exception {
Map<String, Object> map = new HashMap<>();
map.put("id", "foo");
map.put("secret", "bar");
map.put("scope", "openid");
map.put("authorized-grant-types", "authorization_code");
map.put("authorities", "uaa.none");
doSimpleTest(map);
}
@Test(expected = InvalidClientDetailsException.class)
public void no_registered_redirect_url_for_implicit() throws Exception {
Map<String, Object> map = new HashMap<>();
map.put("id", "foo");
map.put("secret", "bar");
map.put("scope", "openid");
map.put("authorized-grant-types", "implicit");
map.put("authorities", "uaa.none");
doSimpleTest(map);
}
@Test
public void redirect_url_not_required() throws Exception {
Map<String, Object> map = new HashMap<>();
map.put("id", "foo");
map.put("secret", "bar");
map.put("scope", "openid");
map.put("authorities", "uaa.none");
for (String grantType : Arrays.asList("password", "client_credentials", GRANT_TYPE_SAML2_BEARER, GRANT_TYPE_USER_TOKEN, GRANT_TYPE_REFRESH_TOKEN)) {
map.put("authorized-grant-types", grantType);
doSimpleTest(map);
}
}
@Test
public void testSimpleAddClientWithSignupSuccessRedirectUrl() throws Exception {
Map<String, Object> map = new HashMap<>();
map.put("id", "foo");
map.put("secret", "bar");
map.put("scope", "openid");
map.put("authorized-grant-types", "authorization_code");
map.put("authorities", "uaa.none");
map.put("signup_redirect_url", "callback_url");
ClientDetails clientDetails = doSimpleTest(map);
assertTrue(clientDetails.getRegisteredRedirectUri().contains("callback_url"));
}
@Test
public void clientMetadata_getsBootstrapped() throws Exception {
Map<String, Object> map = new HashMap<>();
map.put("id", "foo");
map.put("secret", "bar");
map.put("show-on-homepage", true);
map.put("app-launch-url", "http://takemetothispage.com");
map.put("app-icon", "bAsE64encODEd/iMAgE=");
map.put("redirect-uri", "http://localhost/callback");
map.put("authorized-grant-types","client_credentials");
bootstrap.setClients(Collections.singletonMap((String) map.get("id"), map));
bootstrap.afterPropertiesSet();
ClientMetadata clientMetadata = clientMetadataProvisioning.retrieve("foo");
assertTrue(clientMetadata.isShowOnHomePage());
assertEquals("http://takemetothispage.com", clientMetadata.getAppLaunchUrl().toString());
assertEquals("bAsE64encODEd/iMAgE=", clientMetadata.getAppIcon());
}
@Test
public void testAdditionalInformation() throws Exception {
List<String> idps = Arrays.asList("idp1", "idp1");
Map<String, Object> map = new HashMap<>();
map.put("id", "foo");
map.put("secret", "bar");
map.put("scope", "openid");
map.put("authorized-grant-types", "authorization_code");
map.put("authorities", "uaa.none");
map.put("signup_redirect_url", "callback_url");
map.put("change_email_redirect_url", "change_email_url");
map.put(ClientConstants.ALLOWED_PROVIDERS, idps);
ClientDetails created = doSimpleTest(map);
assertEquals(idps, created.getAdditionalInformation().get(ClientConstants.ALLOWED_PROVIDERS));
assertTrue(created.getRegisteredRedirectUri().contains("callback_url"));
assertTrue(created.getRegisteredRedirectUri().contains("change_email_url"));
}
@Test
public void testSimpleAddClientWithChangeEmailRedirectUrl() throws Exception {
Map<String, Object> map = new HashMap<>();
map.put("id", "foo");
map.put("secret", "bar");
map.put("scope", "openid");
map.put("authorized-grant-types", "authorization_code");
map.put("authorities", "uaa.none");
map.put("change_email_redirect_url", "change_email_callback_url");
ClientDetails created = doSimpleTest(map);
assertTrue(created.getRegisteredRedirectUri().contains("change_email_callback_url"));
}
@Test
public void testSimpleAddClientWithAutoApprove() throws Exception {
ClientMetadataProvisioning clientMetadataProvisioning = mock(ClientMetadataProvisioning.class);
bootstrap.setClientMetadataProvisioning(clientMetadataProvisioning);
Map<String, Object> map = createClientMap("foo");
BaseClientDetails output = new BaseClientDetails("foo", "none", "openid", "authorization_code,refresh_token", "uaa.none", "http://localhost/callback");
output.setClientSecret("bar");
bootstrap.setAutoApproveClients(Arrays.asList("foo", "non-existent-client"));
when(clientMetadataProvisioning.update(any(ClientMetadata.class))).thenReturn(new ClientMetadata());
doReturn(output).when(clientRegistrationService).loadClientByClientId(eq("foo"));
bootstrap.setClients(Collections.singletonMap((String) map.get("id"), map));
BaseClientDetails expectedAdd = new BaseClientDetails(output);
bootstrap.afterPropertiesSet();
verify(clientRegistrationService).addClientDetails(expectedAdd);
BaseClientDetails expectedUpdate = new BaseClientDetails(expectedAdd);
expectedUpdate.setAdditionalInformation(Collections.singletonMap(ClientConstants.AUTO_APPROVE, true));
verify(clientRegistrationService).updateClientDetails(expectedUpdate);
}
@Test
public void testOverrideClient() throws Exception {
ClientMetadataProvisioning clientMetadataProvisioning = mock(ClientMetadataProvisioning.class);
bootstrap.setClientMetadataProvisioning(clientMetadataProvisioning);
BaseClientDetails foo = new BaseClientDetails("foo", "", "openid", "client_credentials,password", "uaa.none");
foo.setClientSecret("secret");
clientRegistrationService.addClientDetails(foo);
reset(clientRegistrationService);
Map<String, Object> map = new HashMap<>();
map.put("secret", "bar");
map.put("override", true);
map.put("authorized-grant-types", "client_credentials");
bootstrap.setClients(Collections.singletonMap("foo", map));
when(clientMetadataProvisioning.update(any(ClientMetadata.class))).thenReturn(new ClientMetadata());
doThrow(new ClientAlreadyExistsException("Planned"))
.when(clientRegistrationService).addClientDetails(any(ClientDetails.class));
bootstrap.afterPropertiesSet();
verify(clientRegistrationService, times(1)).addClientDetails(any(ClientDetails.class));
ArgumentCaptor<ClientDetails> captor = ArgumentCaptor.forClass(ClientDetails.class);
verify(clientRegistrationService, times(1)).updateClientDetails(captor.capture());
verify(clientRegistrationService, times(1)).updateClientSecret("foo", "bar");
assertEquals(new HashSet(Arrays.asList("client_credentials")), captor.getValue().getAuthorizedGrantTypes());
}
@Test
public void testOverrideClientByDefault() throws Exception {
ClientMetadataProvisioning clientMetadataProvisioning = mock(ClientMetadataProvisioning.class);
bootstrap.setClientMetadataProvisioning(clientMetadataProvisioning);
BaseClientDetails foo = new BaseClientDetails("foo", "", "openid", "client_credentials,password", "uaa.none");
foo.setClientSecret("secret");
clientRegistrationService.addClientDetails(foo);
reset(clientRegistrationService);
Map<String, Object> map = new HashMap<>();
map.put("secret", "bar");
map.put("redirect-uri", "http://localhost/callback");
map.put("authorized-grant-types","client_credentials");
bootstrap.setClients(Collections.singletonMap("foo", map));
when(clientMetadataProvisioning.update(any(ClientMetadata.class))).thenReturn(new ClientMetadata());
doThrow(new ClientAlreadyExistsException("Planned")).when(clientRegistrationService).addClientDetails(
any(ClientDetails.class));
bootstrap.afterPropertiesSet();
verify(clientRegistrationService, times(1)).addClientDetails(any(ClientDetails.class));
verify(clientRegistrationService, times(1)).updateClientDetails(any(ClientDetails.class));
verify(clientRegistrationService, times(1)).updateClientSecret("foo", "bar");
}
@Test
@SuppressWarnings("unchecked")
public void testOverrideClientWithYaml() throws Exception {
ClientMetadataProvisioning clientMetadataProvisioning = mock(ClientMetadataProvisioning.class);
bootstrap.setClientMetadataProvisioning(clientMetadataProvisioning);
@SuppressWarnings("rawtypes")
Map fooBeforeClient = new Yaml().loadAs("id: foo\noverride: true\nsecret: somevalue\n"
+ "access-token-validity: 100\nredirect-uri: http://localhost/callback\n"
+ "authorized-grant-types: client_credentials", Map.class);
@SuppressWarnings("rawtypes")
Map barBeforeClient = new Yaml().loadAs("id: bar\noverride: true\nsecret: somevalue\n"
+ "access-token-validity: 100\nredirect-uri: http://localhost/callback\n"
+ "authorized-grant-types: client_credentials", Map.class);
@SuppressWarnings("rawtypes")
Map clients = new HashMap();
clients.put("foo", fooBeforeClient);
clients.put("bar", barBeforeClient);
bootstrap.setClients(clients);
bootstrap.afterPropertiesSet();
Map fooUpdateClient = new HashMap(fooBeforeClient);
fooUpdateClient.put("secret","bar");
Map barUpdateClient = new HashMap(fooBeforeClient);
barUpdateClient.put("secret","bar");
clients = new HashMap();
clients.put("foo", fooUpdateClient);
clients.put("bar", barUpdateClient);
bootstrap.setClients(clients);
reset(clientRegistrationService);
when(clientMetadataProvisioning.update(any(ClientMetadata.class))).thenReturn(new ClientMetadata());
doThrow(new ClientAlreadyExistsException("Planned")).when(clientRegistrationService).addClientDetails(
any(ClientDetails.class));
bootstrap.afterPropertiesSet();
verify(clientRegistrationService, times(2)).addClientDetails(any(ClientDetails.class));
verify(clientRegistrationService, times(2)).updateClientDetails(any(ClientDetails.class));
verify(clientRegistrationService, times(1)).updateClientSecret("foo", "bar");
verify(clientRegistrationService, times(1)).updateClientSecret("bar", "bar");
}
@Test
public void testChangePasswordDuringBootstrap() throws Exception {
Map<String, Object> map = createClientMap("foo");
ClientDetails created = doSimpleTest(map);
assertSet((String) map.get("redirect-uri"), null, created.getRegisteredRedirectUri(), String.class);
ClientDetails details = clientRegistrationService.loadClientByClientId("foo");
assertTrue("Password should match bar:", bootstrap.getPasswordEncoder().matches("bar", details.getClientSecret()));
map.put("secret", "bar1");
created = doSimpleTest(map);
assertSet((String) map.get("redirect-uri"), null, created.getRegisteredRedirectUri(), String.class);
details = clientRegistrationService.loadClientByClientId("foo");
assertTrue("Password should match bar1:", bootstrap.getPasswordEncoder().matches("bar1", details.getClientSecret()));
assertFalse("Password should not match bar:", bootstrap.getPasswordEncoder().matches("bar", details.getClientSecret()));
}
@Test
public void testPasswordHashDidNotChangeDuringBootstrap() throws Exception {
Map<String, Object> map = createClientMap("foo");
ClientDetails created = doSimpleTest(map);
assertSet((String) map.get("redirect-uri"), null, created.getRegisteredRedirectUri(), String.class);
ClientDetails details = clientRegistrationService.loadClientByClientId("foo");
assertTrue("Password should match bar:", bootstrap.getPasswordEncoder().matches("bar", details.getClientSecret()));
String hash = details.getClientSecret();
created = doSimpleTest(map);
assertSet((String) map.get("redirect-uri"), null, created.getRegisteredRedirectUri(), String.class);
details = clientRegistrationService.loadClientByClientId("foo");
assertTrue("Password should match bar:", bootstrap.getPasswordEncoder().matches("bar", details.getClientSecret()));
assertEquals("Password hash must not change on an update:", hash, details.getClientSecret());
}
@Test
public void testClientWithoutGrantTypeFails() throws Exception {
Map<String, Object> map = new HashMap<>();
map.put("id", "foo");
map.put("secret", "bar");
map.put("scope", "openid");
map.put("authorities", "uaa.none");
exception.expect(InvalidClientDetailsException.class);
exception.expectMessage("Client must have at least one authorized-grant-type");
bootstrap.setClients(Collections.singletonMap((String) map.get("id"), map));
bootstrap.afterPropertiesSet();
}
private ClientDetails doSimpleTest(Map<String, Object> map) throws Exception {
bootstrap.setClients(Collections.singletonMap((String) map.get("id"), map));
bootstrap.afterPropertiesSet();
ClientDetails created = clientRegistrationService.loadClientByClientId((String) map.get("id"));
assertNotNull(created);
assertSet((String) map.get("scope"), Collections.singleton("uaa.none"), created.getScope(), String.class);
assertSet((String) map.get("resource-ids"), new HashSet(Arrays.asList("none")), created.getResourceIds(), String.class);
String authTypes = (String) map.get("authorized-grant-types");
if (authTypes!=null && authTypes.contains("authorization_code")) {
authTypes+=",refresh_token";
}
assertSet(authTypes, Collections.emptySet(), created.getAuthorizedGrantTypes(), String.class);
Integer validity = (Integer) map.get("access-token-validity");
assertEquals(validity, created.getAccessTokenValiditySeconds());
validity = (Integer) map.get("refresh-token-validity");
assertEquals(validity, created.getRefreshTokenValiditySeconds());
assertSet((String) map.get("authorities"), Collections.emptySet(), created.getAuthorities(), GrantedAuthority.class);
Map<String, Object> info = new HashMap<>(map);
for (String key : Arrays.asList("resource-ids", "scope", "authorized-grant-types", "authorities",
"redirect-uri", "secret", "id", "override", "access-token-validity",
"refresh-token-validity")) {
info.remove(key);
}
for (Map.Entry<String,Object> entry : info.entrySet()) {
assertTrue("Client should contain additional information key:"+ entry.getKey(), created.getAdditionalInformation().containsKey(entry.getKey()));
if (entry.getValue()!=null) {
assertEquals(entry.getValue(), created.getAdditionalInformation().get(entry.getKey()));
}
}
return created;
}
private void assertSet(String expectedValue, Collection defaultValueIfNull, Collection actualValue, Class<?> type) {
Collection assertScopes = defaultValueIfNull;
if (expectedValue!=null) {
if (String.class.equals(type)) {
assertScopes = StringUtils.commaDelimitedListToSet(expectedValue);
} else {
assertScopes = AuthorityUtils.commaSeparatedStringToAuthorityList(expectedValue);
}
}
assertEquals(assertScopes, actualValue);
}
}