/*
* ******************************************************************************
* Cloud Foundry
* Copyright (c) [2009-2017] 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.provider.oauth;
import org.cloudfoundry.identity.uaa.cache.UrlContentCache;
import org.cloudfoundry.identity.uaa.provider.AbstractXOAuthIdentityProviderDefinition;
import org.cloudfoundry.identity.uaa.provider.IdentityProvider;
import org.cloudfoundry.identity.uaa.provider.IdentityProviderProvisioning;
import org.cloudfoundry.identity.uaa.provider.OIDCIdentityProviderDefinition;
import org.cloudfoundry.identity.uaa.provider.RawXOAuthIdentityProviderDefinition;
import org.cloudfoundry.identity.uaa.util.RestTemplateFactory;
import org.cloudfoundry.identity.uaa.util.UaaUrlUtils;
import org.cloudfoundry.identity.uaa.zone.IdentityZone;
import org.junit.Before;
import org.junit.Test;
import org.springframework.mock.web.MockHttpServletRequest;
import java.io.UnsupportedEncodingException;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import static junit.framework.TestCase.assertNotSame;
import static org.cloudfoundry.identity.uaa.constants.OriginKeys.LDAP;
import static org.cloudfoundry.identity.uaa.constants.OriginKeys.OAUTH20;
import static org.cloudfoundry.identity.uaa.constants.OriginKeys.OIDC10;
import static org.cloudfoundry.identity.uaa.provider.ExternalIdentityProviderDefinition.USER_NAME_ATTRIBUTE_NAME;
import static org.hamcrest.Matchers.startsWith;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertThat;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.anyObject;
import static org.mockito.Matchers.anyString;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyZeroInteractions;
import static org.mockito.Mockito.when;
import static org.springframework.http.HttpMethod.GET;
public class XOAuthProviderConfiguratorTests {
String jsonResponse = "{\n" +
" \"issuer\": \"https://accounts.google.com\",\n" +
" \"authorization_endpoint\": \"https://accounts.google.com/o/oauth2/v2/auth\",\n" +
" \"token_endpoint\": \"https://www.googleapis.com/oauth2/v4/token\",\n" +
" \"userinfo_endpoint\": \"https://www.googleapis.com/oauth2/v3/userinfo\",\n" +
" \"revocation_endpoint\": \"https://accounts.google.com/o/oauth2/revoke\",\n" +
" \"jwks_uri\": \"https://www.googleapis.com/oauth2/v3/certs\",\n" +
" \"response_types_supported\": [\n" +
" \"code\",\n" +
" \"token\",\n" +
" \"id_token\",\n" +
" \"code token\",\n" +
" \"code id_token\",\n" +
" \"token id_token\",\n" +
" \"code token id_token\",\n" +
" \"none\"\n" +
" ],\n" +
" \"subject_types_supported\": [\n" +
" \"public\"\n" +
" ],\n" +
" \"id_token_signing_alg_values_supported\": [\n" +
" \"RS256\"\n" +
" ],\n" +
" \"scopes_supported\": [\n" +
" \"openid\",\n" +
" \"email\",\n" +
" \"profile\"\n" +
" ],\n" +
" \"token_endpoint_auth_methods_supported\": [\n" +
" \"client_secret_post\",\n" +
" \"client_secret_basic\"\n" +
" ],\n" +
" \"claims_supported\": [\n" +
" \"aud\",\n" +
" \"email\",\n" +
" \"email_verified\",\n" +
" \"exp\",\n" +
" \"family_name\",\n" +
" \"given_name\",\n" +
" \"iat\",\n" +
" \"iss\",\n" +
" \"locale\",\n" +
" \"name\",\n" +
" \"picture\",\n" +
" \"sub\"\n" +
" ],\n" +
" \"code_challenge_methods_supported\": [\n" +
" \"plain\",\n" +
" \"S256\"\n" +
" ]\n" +
"}";
private OIDCIdentityProviderDefinition oidc;
private RawXOAuthIdentityProviderDefinition oauth;
private String baseExpect = "https://oidc10.identity.cf-app.com/oauth/authorize?client_id=%s&response_type=%s&redirect_uri=%s&scope=%s%s";
private String redirectUri;
private MockHttpServletRequest request;
XOAuthProviderConfigurator configurator;
private UrlContentCache cache;
private RestTemplateFactory factory;
private OIDCIdentityProviderDefinition config;
private String discoveryUrl;
private IdentityProviderProvisioning provisioning;
private IdentityProvider<OIDCIdentityProviderDefinition> oidcProvider;
private IdentityProvider<RawXOAuthIdentityProviderDefinition> oauthProvider;
@Before
public void setup() throws MalformedURLException {
discoveryUrl = "https://accounts.google.com/.well-known/openid-configuration";
oidc = new OIDCIdentityProviderDefinition();
oauth = new RawXOAuthIdentityProviderDefinition();
request = new MockHttpServletRequest(GET.name(), "/uaa/login");
request.setContextPath("/uaa");
request.setServletPath("/login");
request.setScheme("https");
request.setServerName("localhost");
request.setServerPort(8443);
for (AbstractXOAuthIdentityProviderDefinition def : Arrays.asList(oidc, oauth)) {
def.setAuthUrl(new URL("https://oidc10.identity.cf-app.com/oauth/authorize"));
def.setTokenUrl(new URL("https://oidc10.identity.cf-app.com/oauth/token"));
def.setTokenKeyUrl(new URL("https://oidc10.identity.cf-app.com/token_keys"));
def.setScopes(Arrays.asList("openid","password.write"));
def.setRelyingPartyId("clientId");
if (def == oidc) {
def.setResponseType("id_token code");
} else {
def.setResponseType("code");
}
}
redirectUri = URLEncoder.encode("https://localhost:8443/uaa/login/callback/alias");
provisioning = mock(IdentityProviderProvisioning.class);
cache = mock(UrlContentCache.class);
when(cache.getUrlContent(anyString(), anyObject())).thenReturn(jsonResponse.getBytes());
factory = mock(RestTemplateFactory.class);
configurator = spy(new XOAuthProviderConfigurator(provisioning, cache, factory));
config = new OIDCIdentityProviderDefinition();
String discoveryUrl = "https://accounts.google.com/.well-known/openid-configuration";
config.setDiscoveryUrl(new URL(discoveryUrl));
config.addAttributeMapping(USER_NAME_ATTRIBUTE_NAME, "user_name");
config.addAttributeMapping("user.attribute." + "the_client_id", "cid");
config.setStoreCustomAttributes(true);
config.setShowLinkText(true);
config.setLinkText("My OIDC Provider");
config.setRelyingPartyId("identity");
config.setRelyingPartySecret("identitysecret");
config.setResponseType("id_token");
List<String> requestedScopes = new ArrayList<>();
requestedScopes.add("openid");
requestedScopes.add("cloud_controller.read");
config.setScopes(requestedScopes);
oidcProvider = new IdentityProvider<>();
oidcProvider.setType(OIDC10);
oidcProvider.setConfig(config);
oidcProvider.setOriginKey(OIDC10);
oauthProvider = new IdentityProvider<>();
oauthProvider.setType(OAUTH20);
oauthProvider.setConfig(new RawXOAuthIdentityProviderDefinition());
when(provisioning.retrieveAll(eq(true), anyString())).thenReturn(Arrays.asList(oidcProvider, oauthProvider, new IdentityProvider<>().setType(LDAP)));
}
@Test
public void retrieveAll() {
List<IdentityProvider> activeXOAuthProviders = configurator.retrieveAll(true, IdentityZone.getUaa().getId());
assertEquals(2, activeXOAuthProviders.size());
verify(configurator, times(1)).overlay(eq(config));
}
@Test
public void retrieveActive() {
List<IdentityProvider> activeXOAuthProviders = configurator.retrieveActive(IdentityZone.getUaa().getId());
assertEquals(2, activeXOAuthProviders.size());
verify(configurator, times(1)).overlay(eq(config));
verify(configurator, times(1)).retrieveAll(eq(true), anyString());
}
@Test
public void retrieveByOrigin() {
when(provisioning.retrieveByOrigin(eq(OIDC10),anyString())).thenReturn(oidcProvider);
when(provisioning.retrieveByOrigin(eq(OAUTH20),anyString())).thenReturn(oauthProvider);
assertNotNull(configurator.retrieveByOrigin(OIDC10, IdentityZone.getUaa().getId()));
verify(configurator, times(1)).overlay(eq(config));
reset(configurator);
assertNotNull(configurator.retrieveByOrigin(OAUTH20, IdentityZone.getUaa().getId()));
verify(configurator, never()).overlay(anyObject());
}
@Test
public void retrieveById() {
when(provisioning.retrieve(eq(OIDC10))).thenReturn(oidcProvider);
when(provisioning.retrieve(eq(OAUTH20))).thenReturn(oauthProvider);
assertNotNull(configurator.retrieve(OIDC10));
verify(configurator, times(1)).overlay(eq(config));
reset(configurator);
assertNotNull(configurator.retrieve(OAUTH20));
verify(configurator, never()).overlay(anyObject());
}
@Test
public void getParameterizedClass() throws Exception {
assertEquals(OIDCIdentityProviderDefinition.class, oidc.getParameterizedClass());
assertEquals(RawXOAuthIdentityProviderDefinition.class, oauth.getParameterizedClass());
}
@Test
public void overlay_noDiscoveryUrl() {
OIDCIdentityProviderDefinition definition = new OIDCIdentityProviderDefinition();
verifyZeroInteractions(cache);
assertSame(definition, configurator.overlay(definition));
}
@Test
public void overlay_withOverrideValues() throws MalformedURLException {
String urlBase = "http://localhost:8080/uaa";
config.setSkipSslValidation(true);
//values from URL
config.setAuthUrl(new URL(urlBase + "/oauth/authorize"));
config.setTokenUrl(new URL(urlBase + "/oauth/token"));
config.setTokenKeyUrl(new URL(urlBase + "/token_key"));
config.setUserInfoUrl(new URL(urlBase + "/userinfo"));
config.setIssuer(urlBase + "/oauth/token");
OIDCIdentityProviderDefinition overlay = configurator.overlay(config);
assertNotSame(config, overlay);
assertEquals(config, overlay);
verify(cache).getUrlContent(eq(discoveryUrl), any());
verify(factory).getRestTemplate(eq(true));
}
@Test
public void overlay_withoutOverrideValues() throws MalformedURLException {
OIDCIdentityProviderDefinition config = new OIDCIdentityProviderDefinition();
config.setDiscoveryUrl(new URL(discoveryUrl));
config.addAttributeMapping(USER_NAME_ATTRIBUTE_NAME, "user_name");
config.addAttributeMapping("user.attribute." + "the_client_id", "cid");
config.setStoreCustomAttributes(true);
config.setShowLinkText(true);
config.setLinkText("My OIDC Provider");
config.setSkipSslValidation(true);
config.setRelyingPartyId("identity");
config.setRelyingPartySecret("identitysecret");
config.setResponseType("id_token");
List<String> requestedScopes = new ArrayList<>();
requestedScopes.add("openid");
requestedScopes.add("cloud_controller.read");
config.setScopes(requestedScopes);
config.setSkipSslValidation(false);
OIDCIdentityProviderDefinition overlay = configurator.overlay(config);
assertNotSame(config, overlay);
assertNotEquals(config, overlay);
assertEquals(new URL("https://accounts.google.com/o/oauth2/v2/auth"), overlay.getAuthUrl());
assertEquals(new URL("https://www.googleapis.com/oauth2/v3/userinfo"), overlay.getUserInfoUrl());
assertEquals("https://accounts.google.com", overlay.getIssuer());
assertEquals(new URL("https://www.googleapis.com/oauth2/v4/token"), overlay.getTokenUrl());
assertEquals(new URL("https://www.googleapis.com/oauth2/v3/certs"), overlay.getTokenKeyUrl());
verify(cache).getUrlContent(any(), any());
verify(factory).getRestTemplate(eq(false));
}
@Test
public void getCompleteAuthorizationURI_includesNonceOnOIDC() throws UnsupportedEncodingException {
String expected = String.format(baseExpect, oidc.getRelyingPartyId(), URLEncoder.encode("id_token code"), redirectUri, URLEncoder.encode("openid password.write"), "&nonce=");
assertThat(configurator.getCompleteAuthorizationURI("alias", UaaUrlUtils.getBaseURL(request), oidc), startsWith(expected));
}
@Test
public void getCompleteAuthorizationURI_doesNotIncludeNonceOnOAuth() throws UnsupportedEncodingException {
String expected = String.format(baseExpect, oauth.getRelyingPartyId(), URLEncoder.encode("code"), redirectUri, URLEncoder.encode("openid password.write"), "");
assertEquals(configurator.getCompleteAuthorizationURI("alias", UaaUrlUtils.getBaseURL(request), oauth), expected);
}
@Test
public void excludeUnreachableOidcProvider() {
when(cache.getUrlContent(anyString(), anyObject())).thenReturn(null);
List<IdentityProvider> providers = configurator.retrieveAll(true, IdentityZone.getUaa().getId());
assertEquals(1, providers.size());
assertEquals(oauthProvider.getName(), providers.get(0).getName());
verify(configurator, times(1)).overlay(eq(config));
}
}