/*******************************************************************************
* Cloud Foundry
* Copyright (c) [2009-2016] Pivotal Software, Inc. All Rights Reserved.
* <p>
* 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.
* <p>
* 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 com.fasterxml.jackson.core.type.TypeReference;
import org.apache.commons.codec.binary.Base64;
import org.cloudfoundry.identity.uaa.authentication.AccountNotPreCreatedException;
import org.cloudfoundry.identity.uaa.authentication.UaaAuthentication;
import org.cloudfoundry.identity.uaa.authentication.manager.ExternalGroupAuthorizationEvent;
import org.cloudfoundry.identity.uaa.authentication.manager.InvitedUserAuthenticatedEvent;
import org.cloudfoundry.identity.uaa.authentication.manager.NewUserAuthenticatedEvent;
import org.cloudfoundry.identity.uaa.cache.ExpiringUrlCache;
import org.cloudfoundry.identity.uaa.constants.OriginKeys;
import org.cloudfoundry.identity.uaa.oauth.KeyInfo;
import org.cloudfoundry.identity.uaa.oauth.TokenKeyEndpoint;
import org.cloudfoundry.identity.uaa.oauth.jwk.JsonWebKey;
import org.cloudfoundry.identity.uaa.oauth.jwk.JsonWebKeySet;
import org.cloudfoundry.identity.uaa.oauth.token.ClaimConstants;
import org.cloudfoundry.identity.uaa.oauth.token.CompositeAccessToken;
import org.cloudfoundry.identity.uaa.oauth.token.VerificationKeyResponse;
import org.cloudfoundry.identity.uaa.oauth.token.VerificationKeysListResponse;
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.user.InMemoryUaaUserDatabase;
import org.cloudfoundry.identity.uaa.user.UaaAuthority;
import org.cloudfoundry.identity.uaa.user.UaaUser;
import org.cloudfoundry.identity.uaa.user.UaaUserPrototype;
import org.cloudfoundry.identity.uaa.user.UserInfo;
import org.cloudfoundry.identity.uaa.util.JsonUtils;
import org.cloudfoundry.identity.uaa.util.RestTemplateFactory;
import org.cloudfoundry.identity.uaa.util.TimeServiceImpl;
import org.cloudfoundry.identity.uaa.util.UaaTokenUtils;
import org.cloudfoundry.identity.uaa.zone.MultitenancyFixture;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.Matchers;
import org.springframework.context.ApplicationEvent;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.jwt.crypto.sign.InvalidSignatureException;
import org.springframework.security.jwt.crypto.sign.RsaSigner;
import org.springframework.security.oauth2.common.exceptions.InvalidTokenException;
import org.springframework.test.web.client.MockRestServiceServer;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.HttpClientErrorException;
import org.springframework.web.client.HttpServerErrorException;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import java.net.MalformedURLException;
import java.net.URL;
import java.time.Instant;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import static org.cloudfoundry.identity.uaa.provider.ExternalIdentityProviderDefinition.GROUP_ATTRIBUTE_NAME;
import static org.cloudfoundry.identity.uaa.provider.ExternalIdentityProviderDefinition.USER_NAME_ATTRIBUTE_NAME;
import static org.cloudfoundry.identity.uaa.util.UaaMapUtils.entry;
import static org.cloudfoundry.identity.uaa.util.UaaMapUtils.map;
import static org.hamcrest.Matchers.containsInAnyOrder;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.instanceOf;
import static org.hamcrest.Matchers.is;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.mockito.Matchers.anyBoolean;
import static org.mockito.Matchers.anyObject;
import static org.mockito.Matchers.anyString;
import static org.mockito.Matchers.eq;
import static org.mockito.Matchers.same;
import static org.mockito.Mockito.atLeast;
import static org.mockito.Mockito.doAnswer;
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.when;
import static org.springframework.http.HttpStatus.OK;
import static org.springframework.http.MediaType.APPLICATION_JSON;
import static org.springframework.test.web.client.match.MockRestRequestMatchers.content;
import static org.springframework.test.web.client.match.MockRestRequestMatchers.header;
import static org.springframework.test.web.client.match.MockRestRequestMatchers.requestTo;
import static org.springframework.test.web.client.response.MockRestResponseCreators.withBadRequest;
import static org.springframework.test.web.client.response.MockRestResponseCreators.withServerError;
import static org.springframework.test.web.client.response.MockRestResponseCreators.withStatus;
public class XOAuthAuthenticationManagerTest {
private MockRestServiceServer mockUaaServer;
private XOAuthAuthenticationManager xoAuthAuthenticationManager;
private IdentityProviderProvisioning provisioning;
private InMemoryUaaUserDatabase userDatabase;
private XOAuthCodeToken xCodeToken;
private ApplicationEventPublisher publisher;
private static final String CODE = "the_code";
private static final String ORIGIN = "the_origin";
private static final String ISSUER = "cf-app.com";
private IdentityProvider<AbstractXOAuthIdentityProviderDefinition> identityProvider;
private Map<String, Object> claims;
private HashMap<String, Object> attributeMappings;
private OIDCIdentityProviderDefinition config;
private String rsaSigningKey;
private RsaSigner signer;
private Map<String, Object> header;
private String invalidRsaSigningKey;
private XOAuthProviderConfigurator xoAuthProviderConfigurator;
@Before
@After
public void clearContext() {
SecurityContextHolder.clearContext();
header = map(
entry("alg", "HS256"),
entry("kid", "testKey"),
entry("typ", "JWT")
);
}
@Before
public void setUp() throws Exception {
rsaSigningKey = "-----BEGIN RSA PRIVATE KEY-----\n" +
"MIIBOQIBAAJAcjAgsHEfrUxeTFwQPb17AkZ2Im4SfZdpY8Ada9pZfxXz1PZSqv9T\n" +
"PTMAzNx+EkzMk2IMYN+uNm1bfDzaxVdz+QIDAQABAkBoR39y4rw0/QsY3PKQD5xo\n" +
"hYSZCMCmJUI/sFCuECevIFY4h6q9KBP+4Set96f7Bgs9wJWVvCMx/nJ6guHAjsIB\n" +
"AiEAywVOoCGIZ2YzARXWYcMRYZ89hxoHh8kZ+QMthRSZieECIQCP/GWQYgyofAQA\n" +
"BtM8YwThXEV+S3KtuCn4IAQ89gqdGQIgULBASpZpPyc4OEM0nFBKFTGT46EtwwLj\n" +
"RrvDmLPSPiECICQi9FqIQSUH+vkGvX0qXM8ymT5ZMS7oSaA8aNPj7EYBAiEAx5V3\n" +
"2JGEulMY3bK1PVGYmtsXF1gq6zbRMoollMCRSMg=\n" +
"-----END RSA PRIVATE KEY-----";
signer = new RsaSigner(rsaSigningKey);
provisioning = mock(IdentityProviderProvisioning.class);
userDatabase = new InMemoryUaaUserDatabase(Collections.emptySet());
publisher = mock(ApplicationEventPublisher.class);
RestTemplateFactory restTemplateFactory = mock(RestTemplateFactory.class);
when(restTemplateFactory.getRestTemplate(anyBoolean())).thenReturn(new RestTemplate());
xoAuthProviderConfigurator = spy(
new XOAuthProviderConfigurator(
provisioning,
new ExpiringUrlCache(10000, new TimeServiceImpl(), 10),
restTemplateFactory
)
);
xoAuthAuthenticationManager = spy(new XOAuthAuthenticationManager(xoAuthProviderConfigurator, restTemplateFactory));
xoAuthAuthenticationManager.setUserDatabase(userDatabase);
xoAuthAuthenticationManager.setApplicationEventPublisher(publisher);
xCodeToken = new XOAuthCodeToken(CODE, ORIGIN, "http://localhost/callback/the_origin");
claims = map(
entry("sub", "12345"),
entry("preferred_username", "marissa"),
entry("origin", "uaa"),
entry("iss", "http://oidc10.identity.cf-app.com/oauth/token"),
entry("given_name", "Marissa"),
entry("client_id", "client"),
entry("aud", Arrays.asList("identity", "another_trusted_client")),
entry("zid", "uaa"),
entry("user_id", "12345"),
entry("azp", "client"),
entry("scope", Arrays.asList("openid")),
entry("auth_time", 1458603913),
entry("phone_number", "1234567890"),
entry("exp", Instant.now().getEpochSecond() + 3600),
entry("iat", 1458603913),
entry("family_name", "Bloggs"),
entry("jti", "b23fe183-158d-4adc-8aff-65c440bbbee1"),
entry("email", "marissa@bloggs.com"),
entry("rev_sig", "3314dc98"),
entry("cid", "client"),
entry(ClaimConstants.ACR, JsonUtils.readValue("{\"values\": [\"urn:oasis:names:tc:SAML:2.0:ac:classes:Password\"] }", Map.class))
);
attributeMappings = new HashMap<>();
config = new OIDCIdentityProviderDefinition()
.setAuthUrl(new URL("http://oidc10.identity.cf-app.com/oauth/authorize"))
.setTokenUrl(new URL("http://oidc10.identity.cf-app.com/oauth/token"))
.setIssuer("http://oidc10.identity.cf-app.com/oauth/token")
.setShowLinkText(true)
.setLinkText("My OIDC Provider")
.setRelyingPartyId("identity")
.setRelyingPartySecret("identitysecret")
.setUserInfoUrl(new URL("http://oidc10.identity.cf-app.com/userinfo"))
.setTokenKey("-----BEGIN PUBLIC KEY-----\n" +
"MFswDQYJKoZIhvcNAQEBBQADSgAwRwJAcjAgsHEfrUxeTFwQPb17AkZ2Im4SfZdp\n" +
"Y8Ada9pZfxXz1PZSqv9TPTMAzNx+EkzMk2IMYN+uNm1bfDzaxVdz+QIDAQAB\n" +
"-----END PUBLIC KEY-----");
config.setExternalGroupsWhitelist(
Arrays.asList(
"*"
)
);
mockUaaServer = MockRestServiceServer.createServer(restTemplateFactory.getRestTemplate(config.isSkipSslValidation()));
reset(xoAuthAuthenticationManager);
invalidRsaSigningKey = "-----BEGIN RSA PRIVATE KEY-----\n" +
"MIIBOgIBAAJBAJnlBG4lLmUiHslsKDODfd0MqmGZRNUOhn7eO3cKobsFljUKzRQe\n" +
"GB7LYMjPavnKccm6+jWSXutpzfAc9A9wXG8CAwEAAQJADwwdiseH6cuURw2UQLUy\n" +
"sVJztmdOG6b375+7IMChX6/cgoF0roCPP0Xr70y1J4TXvFhjcwTgm4RI+AUiIDKw\n" +
"gQIhAPQHwHzdYG1639Qz/TCHzuai0ItwVC1wlqKpat+CaqdZAiEAoXFyS7249mRu\n" +
"xtwRAvxKMe+eshHvG2le+ZDrM/pz8QcCIQCzmCDpxGL7L7sbCUgFN23l/11Lwdex\n" +
"uXKjM9wbsnebwQIgeZIbVovUp74zaQ44xT3EhVwC7ebxXnv3qAkIBMk526sCIDVg\n" +
"z1jr3KEcaq9zjNJd9sKBkqpkVSqj8Mv+Amq+YjBA\n" +
"-----END RSA PRIVATE KEY-----";
}
@Test
public void discoveryURL_is_used() throws MalformedURLException {
URL authUrl = config.getAuthUrl();
URL tokenUrl = config.getTokenUrl();
config.setAuthUrl(null);
config.setTokenUrl(null);
config.setDiscoveryUrl(new URL("http://some.discovery.url"));
Map<String, Object> discoveryContent = new HashMap();
discoveryContent.put("authorization_endpoint", authUrl.toString());
discoveryContent.put("token_endpoint", tokenUrl.toString());
//mandatory but not used
discoveryContent.put("userinfo_endpoint", "http://localhost/userinfo");
discoveryContent.put("jwks_uri", "http://localhost/token_keys");
discoveryContent.put("issuer", "http://localhost/issuer");
mockUaaServer.expect(requestTo("http://some.discovery.url"))
.andRespond(withStatus(OK).contentType(APPLICATION_JSON).body(JsonUtils.writeValueAsBytes(discoveryContent)));
IdentityProvider<AbstractXOAuthIdentityProviderDefinition> identityProvider = getProvider();
when(provisioning.retrieveByOrigin(eq(ORIGIN), anyString())).thenReturn(identityProvider);
mockToken();
addTheUserOnAuth();
xoAuthAuthenticationManager.authenticate(xCodeToken);
verify(xoAuthProviderConfigurator, atLeast(1)).overlay(eq(config));
mockUaaServer.verify();
}
@Test
public void idToken_In_Redirect_Should_Use_it() throws Exception {
mockToken();
addTheUserOnAuth();
String tokenResponse = getIdTokenResponse();
String idToken = (String) JsonUtils.readValue(tokenResponse, Map.class).get("id_token");
xCodeToken.setIdToken(idToken);
xoAuthAuthenticationManager.authenticate(xCodeToken);
verify(xoAuthAuthenticationManager, times(1)).getClaimsFromToken(same(xCodeToken), anyObject());
verify(xoAuthAuthenticationManager, times(1)).getClaimsFromToken(eq(idToken), anyObject());
verify(xoAuthAuthenticationManager, never()).getRestTemplate(anyObject());
ArgumentCaptor<ApplicationEvent> userArgumentCaptor = ArgumentCaptor.forClass(ApplicationEvent.class);
verify(publisher,times(3)).publishEvent(userArgumentCaptor.capture());
assertEquals(3, userArgumentCaptor.getAllValues().size());
NewUserAuthenticatedEvent event = (NewUserAuthenticatedEvent)userArgumentCaptor.getAllValues().get(0);
assertUserCreated(event);
}
@Test
public void exchangeExternalCodeForIdToken_andCreateShadowUser() throws Exception {
mockToken();
addTheUserOnAuth();
xoAuthAuthenticationManager.authenticate(xCodeToken);
mockUaaServer.verify();
ArgumentCaptor<ApplicationEvent> userArgumentCaptor = ArgumentCaptor.forClass(ApplicationEvent.class);
verify(publisher,times(3)).publishEvent(userArgumentCaptor.capture());
assertEquals(3, userArgumentCaptor.getAllValues().size());
NewUserAuthenticatedEvent event = (NewUserAuthenticatedEvent)userArgumentCaptor.getAllValues().get(0);
assertUserCreated(event);
}
@Test
public void test_single_key_response() throws Exception {
configureTokenKeyResponse(
"http://oidc10.identity.cf-app.com/token_key",
rsaSigningKey,
"correctKey",
false);
addTheUserOnAuth();
xoAuthAuthenticationManager.authenticate(xCodeToken);
}
@Test
public void test_single_key_response_without_value() throws Exception {
String json = getKeyJson(rsaSigningKey, "correctKey", false);
Map<String, Object> map = JsonUtils.readValue(json, new TypeReference<Map<String, Object>>() {});
map.remove("value");
json = JsonUtils.writeValueAsString(map);
configureTokenKeyResponse("http://oidc10.identity.cf-app.com/token_key",json);
addTheUserOnAuth();
xoAuthAuthenticationManager.authenticate(xCodeToken);
}
@Test
public void test_multi_key_response_without_value() throws Exception {
String jsonValid = getKeyJson(rsaSigningKey, "correctKey", false);
String jsonInvalid = getKeyJson(invalidRsaSigningKey, "invalidKey", false);
Map<String, Object> mapValid = JsonUtils.readValue(jsonValid, new TypeReference<Map<String, Object>>() {});
Map<String, Object> mapInvalid = JsonUtils.readValue(jsonInvalid, new TypeReference<Map<String, Object>>() {});
mapValid.remove("value");
mapInvalid.remove("value");
String json = JsonUtils.writeValueAsString(new JsonWebKeySet<>(Arrays.asList(new JsonWebKey(mapInvalid), new JsonWebKey(mapValid))));
configureTokenKeyResponse("http://oidc10.identity.cf-app.com/token_key",json);
addTheUserOnAuth();
xoAuthAuthenticationManager.authenticate(xCodeToken);
}
@Test
public void test_multi_key_all_invalid() throws Exception {
String jsonInvalid = getKeyJson(invalidRsaSigningKey, "invalidKey", false);
String jsonInvalid2 = getKeyJson(invalidRsaSigningKey, "invalidKey2", false);
Map<String, Object> mapInvalid = JsonUtils.readValue(jsonInvalid, new TypeReference<Map<String, Object>>() {});
Map<String, Object> mapInvalid2 = JsonUtils.readValue(jsonInvalid2, new TypeReference<Map<String, Object>>() {});
String json = JsonUtils.writeValueAsString(new JsonWebKeySet<>(Arrays.asList(new JsonWebKey(mapInvalid), new JsonWebKey(mapInvalid2))));
assertTrue(json.contains("\"invalidKey\""));
assertTrue(json.contains("\"invalidKey2\""));
configureTokenKeyResponse("http://oidc10.identity.cf-app.com/token_key",json);
addTheUserOnAuth();
try {
xoAuthAuthenticationManager.authenticate(xCodeToken);
fail("not expected");
} catch (Exception e) {
assertTrue(e.getCause() instanceof InvalidSignatureException);
}
}
@Test
public void test_multi_key_response() throws Exception {
configureTokenKeyResponse(
"http://oidc10.identity.cf-app.com/token_key",
rsaSigningKey,
"correctKey",
true);
addTheUserOnAuth();
xoAuthAuthenticationManager.authenticate(xCodeToken);
}
public void assertUserCreated(NewUserAuthenticatedEvent event) {
assertNotNull(event);
UaaUser uaaUser = event.getUser();
assertNotNull(uaaUser);
assertEquals("Marissa",uaaUser.getGivenName());
assertEquals("Bloggs", uaaUser.getFamilyName());
assertEquals("marissa@bloggs.com", uaaUser.getEmail());
assertEquals("the_origin", uaaUser.getOrigin());
assertEquals("1234567890", uaaUser.getPhoneNumber());
assertEquals("marissa",uaaUser.getUsername());
assertEquals(OriginKeys.UAA, uaaUser.getZoneId());
}
@Test(expected = AccountNotPreCreatedException.class)
public void doesNotCreateShadowUserAndFailsAuthentication_IfAddShadowUserOnLoginIsFalse() throws Exception {
config.setAddShadowUserOnLogin(false);
mockToken();
xoAuthAuthenticationManager.authenticate(xCodeToken);
}
@Test(expected = InvalidTokenException.class)
public void rejectTokenWithInvalidSignature() throws Exception {
mockToken();
config.setTokenKey("WRONG_KEY");
xoAuthAuthenticationManager.authenticate(xCodeToken);
}
@Test(expected = InvalidTokenException.class)
public void rejectTokenWithInvalidSignatureAccordingToTokenKeyEndpoint() throws Exception {
configureTokenKeyResponse("http://oidc10.identity.cf-app.com/token_key", invalidRsaSigningKey, "wrongKey");
xoAuthAuthenticationManager.authenticate(xCodeToken);
}
public void configureTokenKeyResponse(String keyUrl, String signingKey, String keyId) throws MalformedURLException {
configureTokenKeyResponse(keyUrl, signingKey, keyId, false);
}
public void configureTokenKeyResponse(String keyUrl, String signingKey, String keyId, boolean list) throws MalformedURLException {
String response = getKeyJson(signingKey, keyId, list);
configureTokenKeyResponse(keyUrl, response);
}
public String getKeyJson(String signingKey, String keyId, boolean list) {
KeyInfo key = new KeyInfo();
key.setKeyId(keyId);
key.setSigningKey(signingKey);
VerificationKeyResponse keyResponse = TokenKeyEndpoint.getVerificationKeyResponse(key);
Object verificationKeyResponse = list ? new VerificationKeysListResponse(Arrays.asList(keyResponse)) : keyResponse;
return JsonUtils.writeValueAsString(verificationKeyResponse);
}
public void configureTokenKeyResponse(String keyUrl, String response) throws MalformedURLException {
config.setTokenKey(null);
config.setTokenKeyUrl(new URL(keyUrl));
mockToken();
mockUaaServer.expect(requestTo(keyUrl))
.andExpect(header("Authorization", "Basic " + new String(Base64.encodeBase64("identity:identitysecret".getBytes()))))
.andExpect(header("Accept", "application/json"))
.andRespond(withStatus(OK).contentType(APPLICATION_JSON).body(response));
}
@Test(expected = InvalidTokenException.class)
public void rejectTokenWithInvalidIssuer() throws Exception {
claims.put("iss", "http://wrong.issuer/");
mockToken();
xoAuthAuthenticationManager.authenticate(xCodeToken);
}
@Test(expected = InvalidTokenException.class)
public void rejectExpiredToken() throws Exception {
claims.put("exp", Instant.now().getEpochSecond() - 1);
mockToken();
xoAuthAuthenticationManager.authenticate(xCodeToken);
}
@Test(expected = InvalidTokenException.class)
public void rejectWrongAudience() throws Exception {
claims.put("aud", Arrays.asList("another_client", "a_complete_stranger"));
mockToken();
xoAuthAuthenticationManager.authenticate(xCodeToken);
}
@Test
public void updateShadowUser_IfAlreadyExists() throws MalformedURLException {
claims.put("scope", Arrays.asList("openid", "some.other.scope", "closedid"));
attributeMappings.put(GROUP_ATTRIBUTE_NAME, "scope");
mockToken();
UaaUser existingShadowUser = new UaaUser(new UaaUserPrototype()
.withUsername("marissa")
.withPassword("")
.withEmail("marissa_old@bloggs.com")
.withGivenName("Marissa_Old")
.withFamilyName("Bloggs_Old")
.withId("user-id")
.withOrigin("the_origin")
.withZoneId("uaa")
.withAuthorities(UaaAuthority.USER_AUTHORITIES));
userDatabase.addUser(existingShadowUser);
xoAuthAuthenticationManager.authenticate(xCodeToken);
mockUaaServer.verify();
ArgumentCaptor<ApplicationEvent> userArgumentCaptor = ArgumentCaptor.forClass(ApplicationEvent.class);
verify(publisher,times(2)).publishEvent(userArgumentCaptor.capture());
assertEquals(2, userArgumentCaptor.getAllValues().size());
ExternalGroupAuthorizationEvent event = (ExternalGroupAuthorizationEvent)userArgumentCaptor.getAllValues().get(0);
UaaUser uaaUser = event.getUser();
assertEquals("Marissa",uaaUser.getGivenName());
assertEquals("Bloggs",uaaUser.getFamilyName());
assertEquals("marissa@bloggs.com", uaaUser.getEmail());
assertEquals("the_origin", uaaUser.getOrigin());
assertEquals("1234567890", uaaUser.getPhoneNumber());
assertEquals("marissa", uaaUser.getUsername());
assertEquals(OriginKeys.UAA, uaaUser.getZoneId());
}
@Test
public void invitedUser_becomesVerifiedOnAccept() throws Exception {
getInvitedUser();
claims.remove("preferred_username");
claims.put("preferred_username", "marissa@bloggs.com");
mockToken();
xoAuthAuthenticationManager.authenticate(xCodeToken);
mockUaaServer.verify();
ArgumentCaptor<ApplicationEvent> userArgumentCaptor = ArgumentCaptor.forClass(ApplicationEvent.class);
verify(publisher,times(3)).publishEvent(userArgumentCaptor.capture());
assertEquals(3, userArgumentCaptor.getAllValues().size());
assertThat(userArgumentCaptor.getAllValues().get(0), instanceOf(InvitedUserAuthenticatedEvent.class));
RequestContextHolder.resetRequestAttributes();
}
private UaaUser getInvitedUser() {
UaaUser existingShadowUser = new UaaUser(new UaaUserPrototype()
.withUsername("marissa@bloggs.com")
.withPassword("")
.withEmail("marissa@bloggs.com")
.withGivenName("Marissa_Old")
.withFamilyName("Bloggs_Old")
.withId("user-id")
.withOrigin("the_origin")
.withZoneId("uaa")
.withAuthorities(UaaAuthority.USER_AUTHORITIES));
userDatabase.addUser(existingShadowUser);
RequestAttributes attributes = new ServletRequestAttributes(new MockHttpServletRequest());
attributes.setAttribute("IS_INVITE_ACCEPTANCE", true, RequestAttributes.SCOPE_SESSION);
attributes.setAttribute("user_id", existingShadowUser.getId(), RequestAttributes.SCOPE_SESSION);
RequestContextHolder.setRequestAttributes(attributes);
return existingShadowUser;
}
@Test
public void loginAndValidateSignatureUsingTokenKeyEndpoint() throws Exception {
config.setTokenKeyUrl(new URL("http://oidc10.identity.cf-app.com/token_key"));
config.setTokenKey(null);
KeyInfo key = new KeyInfo();
key.setKeyId("correctKey");
key.setSigningKey(rsaSigningKey);
VerificationKeyResponse verificationKeyResponse = TokenKeyEndpoint.getVerificationKeyResponse(key);
String response = JsonUtils.writeValueAsString(verificationKeyResponse);
mockToken();
mockUaaServer.expect(requestTo("http://oidc10.identity.cf-app.com/token_key"))
.andExpect(header("Authorization", "Basic " + new String(Base64.encodeBase64("identity:identitysecret".getBytes()))))
.andExpect(header("Accept", "application/json"))
.andRespond(withStatus(OK).contentType(APPLICATION_JSON).body(response));
mockToken();
UaaUser existingShadowUser = new UaaUser(new UaaUserPrototype()
.withUsername("marissa")
.withPassword("")
.withEmail("marissa_old@bloggs.com")
.withGivenName("Marissa_Old")
.withFamilyName("Bloggs_Old")
.withId("user-id")
.withOrigin("the_origin")
.withZoneId("uaa")
.withAuthorities(UaaAuthority.USER_AUTHORITIES));
userDatabase.addUser(existingShadowUser);
xoAuthAuthenticationManager.authenticate(xCodeToken);
}
@Test
public void authenticatedUser_hasAuthoritiesFromListOfIDTokenRoles() throws MalformedURLException {
claims.put("scope", Arrays.asList("openid", "some.other.scope", "closedid"));
testTokenHasAuthoritiesFromIdTokenRoles();
}
@Test
public void authenticatedUser_hasAuthoritiesFromCommaSeparatedStringOfIDTokenRoles() throws MalformedURLException {
claims.put("scope", "openid,some.other.scope,closedid");
testTokenHasAuthoritiesFromIdTokenRoles();
}
@Test
public void authenticatedUser_hasConfigurableUsernameField() throws Exception {
attributeMappings.put(USER_NAME_ATTRIBUTE_NAME, "username");
claims.remove("preferred_username");
claims.put("username", "marissa");
mockToken();
UaaUser uaaUser = xoAuthAuthenticationManager.getUser(xCodeToken, getAuthenticationData(xCodeToken));
assertThat(uaaUser.getUsername(), is("marissa"));
}
@Test
public void getUserWithNullEmail() throws MalformedURLException {
claims.put("email", null);
mockToken();
UaaUser user = xoAuthAuthenticationManager.getUser(xCodeToken, getAuthenticationData(xCodeToken));
assertEquals("marissa@user.from.the_origin.cf", user.getEmail());
}
private XOAuthAuthenticationManager.AuthenticationData getAuthenticationData(XOAuthCodeToken xCodeToken) {
return xoAuthAuthenticationManager.getExternalAuthenticationDetails(xCodeToken);
}
@Test
public void testGetUserSetsTheRightOrigin() {
xoAuthAuthenticationManager.getUser(xCodeToken, getAuthenticationData(xCodeToken));
assertEquals(ORIGIN, xoAuthAuthenticationManager.getOrigin());
XOAuthCodeToken otherToken = new XOAuthCodeToken(CODE, "other_origin", "http://localhost/callback/the_origin");
xoAuthAuthenticationManager.getUser(otherToken, getAuthenticationData(otherToken));
assertEquals("other_origin", xoAuthAuthenticationManager.getOrigin());
}
@Test
public void testGetUserIssuerOverrideNotUsed() throws Exception {
mockToken();
assertNotNull(xoAuthAuthenticationManager.getUser(xCodeToken, getAuthenticationData(xCodeToken)));
}
@Test
public void testGetUserIssuerOverrideUsedNoMatch() throws Exception {
config.setIssuer(ISSUER);
mockToken();
try {
xoAuthAuthenticationManager.getUser(xCodeToken, getAuthenticationData(xCodeToken));
fail("InvalidTokenException should have been thrown");
} catch(InvalidTokenException ex) { }
}
@Test
public void testGetUserIssuerOverrideUsedMatch() throws Exception {
config.setIssuer(ISSUER);
claims.remove("iss");
claims.put("iss", ISSUER);
mockToken();
assertNotNull(xoAuthAuthenticationManager.getUser(xCodeToken, getAuthenticationData(xCodeToken)));
}
@Test
public void test_authentication_context_transfers_to_authentication() throws Exception {
addTheUserOnAuth();
mockToken();
UaaAuthentication authentication = (UaaAuthentication)xoAuthAuthenticationManager.authenticate(xCodeToken);
assertNotNull(authentication);
assertNotNull(authentication.getAuthContextClassRef());
assertThat(authentication.getAuthContextClassRef(), containsInAnyOrder("urn:oasis:names:tc:SAML:2.0:ac:classes:Password"));
}
@Test
public void test_authentication_context_when_missing() throws Exception {
addTheUserOnAuth();
claims.remove(ClaimConstants.ACR);
mockToken();
UaaAuthentication authentication = (UaaAuthentication)xoAuthAuthenticationManager.authenticate(xCodeToken);
assertNotNull(authentication);
assertNull(authentication.getAuthContextClassRef());
}
@Test
public void failsIfProviderIsNotOIDCOrOAuth() throws Exception {
when(provisioning.retrieveByOrigin(eq(ORIGIN), anyString())).thenReturn(MultitenancyFixture.identityProvider("the_origin", "uaa"));
Authentication authentication = xoAuthAuthenticationManager.authenticate(xCodeToken);
assertNull(authentication);
}
@Test
public void failsIfProviderIsNotFound() throws Exception {
when(provisioning.retrieveByOrigin(eq(ORIGIN), anyString())).thenReturn(null);
Authentication authentication = xoAuthAuthenticationManager.authenticate(xCodeToken);
assertNull(authentication);
}
@Test(expected = HttpServerErrorException.class)
public void tokenCannotBeFetchedFromCodeBecauseOfServerError() throws Exception {
IdentityProvider<AbstractXOAuthIdentityProviderDefinition> identityProvider = getProvider();
when(provisioning.retrieveByOrigin(eq(ORIGIN), anyString())).thenReturn(identityProvider);
mockUaaServer.expect(requestTo("http://oidc10.identity.cf-app.com/oauth/token")).andRespond(withServerError());
xoAuthAuthenticationManager.authenticate(xCodeToken);
}
@Test(expected = HttpClientErrorException.class)
public void tokenCannotBeFetchedFromInvalidCode() throws Exception {
IdentityProvider<AbstractXOAuthIdentityProviderDefinition> identityProvider = getProvider();
when(provisioning.retrieveByOrigin(eq(ORIGIN), anyString())).thenReturn(identityProvider);
mockUaaServer.expect(requestTo("http://oidc10.identity.cf-app.com/oauth/token")).andRespond(withBadRequest());
xoAuthAuthenticationManager.authenticate(xCodeToken);
}
private void addTheUserOnAuth() {
doAnswer(invocation -> {
Object e = invocation.getArguments()[0];
if (e instanceof NewUserAuthenticatedEvent) {
NewUserAuthenticatedEvent event = (NewUserAuthenticatedEvent) e;
UaaUser user = event.getUser();
userDatabase.addUser(user);
}
return null;
}).when(publisher).publishEvent(Matchers.any(ApplicationEvent.class));
}
@Test
public void authenticationContainsAMRClaim_fromExternalOIDCProvider() throws Exception {
addTheUserOnAuth();
claims.put("amr", Arrays.asList("mfa", "rba"));
mockToken();
UaaAuthentication authentication = (UaaAuthentication)xoAuthAuthenticationManager.authenticate(xCodeToken);
assertThat(authentication.getAuthenticationMethods(), containsInAnyOrder("mfa", "rba", "ext"));
}
@Test
public void test_custom_user_attributes_are_stored() throws Exception {
addTheUserOnAuth();
List<String> managers = Arrays.asList("Sue the Sloth", "Kari the AntEater");
List<String> costCenter = Arrays.asList("Austin, TX");
claims.put("managers", managers);
claims.put("employeeCostCenter", costCenter);
attributeMappings.put("user.attribute.costCenter", "employeeCostCenter");
attributeMappings.put("user.attribute.terribleBosses", "managers");
config.setStoreCustomAttributes(true);
config.setExternalGroupsWhitelist(Arrays.asList("*"));
List<String> scopes = Arrays.asList("openid", "some.other.scope", "closedid");
claims.put("scope", scopes);
attributeMappings.put(GROUP_ATTRIBUTE_NAME, "scope");
mockToken();
MultiValueMap<String, String> map = new LinkedMultiValueMap<>();
map.put("costCenter", costCenter);
map.put("terribleBosses", managers);
UaaAuthentication authentication = (UaaAuthentication)xoAuthAuthenticationManager.authenticate(xCodeToken);
assertEquals(map, authentication.getUserAttributes());
assertThat(authentication.getExternalGroups(), containsInAnyOrder(scopes.toArray()));
UserInfo info = new UserInfo()
.setUserAttributes(map)
.setRoles(scopes);
UserInfo actualUserInfo = xoAuthAuthenticationManager.getUserDatabase().getUserInfo(authentication.getPrincipal().getId());
assertEquals(actualUserInfo.getUserAttributes(), info.getUserAttributes());
assertThat(actualUserInfo.getRoles(), containsInAnyOrder(info.getRoles().toArray()));
}
private void mockToken() throws MalformedURLException {
String response = getIdTokenResponse();
mockUaaServer.expect(requestTo("http://oidc10.identity.cf-app.com/oauth/token"))
.andExpect(header("Authorization", "Basic " + new String(Base64.encodeBase64("identity:identitysecret".getBytes()))))
.andExpect(header("Accept", "application/json"))
.andExpect(content().string(containsString("grant_type=authorization_code")))
.andExpect(content().string(containsString("code=the_code")))
.andExpect(content().string(containsString("redirect_uri=http%3A%2F%2Flocalhost%2Fcallback%2Fthe_origin")))
.andExpect(content().string(containsString(("response_type=id_token"))))
.andRespond(withStatus(OK).contentType(APPLICATION_JSON).body(response));
}
private String getIdTokenResponse() throws MalformedURLException {
String idTokenJwt = UaaTokenUtils.constructToken(header, claims, signer);
identityProvider = getProvider();
when(provisioning.retrieveByOrigin(eq(ORIGIN), anyString())).thenReturn(identityProvider);
CompositeAccessToken compositeAccessToken = new CompositeAccessToken("accessToken");
compositeAccessToken.setIdTokenValue(idTokenJwt);
return JsonUtils.writeValueAsString(compositeAccessToken);
}
private IdentityProvider<AbstractXOAuthIdentityProviderDefinition> getProvider() throws MalformedURLException {
IdentityProvider<AbstractXOAuthIdentityProviderDefinition> identityProvider = new IdentityProvider<>();
identityProvider.setName("my oidc provider");
identityProvider.setIdentityZoneId(OriginKeys.UAA);
config.setAttributeMappings(attributeMappings);
identityProvider.setConfig(config);
identityProvider.setOriginKey("puppy");
return identityProvider;
}
private void testTokenHasAuthoritiesFromIdTokenRoles() throws MalformedURLException {
attributeMappings.put(GROUP_ATTRIBUTE_NAME, "scope");
mockToken();
UaaUser uaaUser = xoAuthAuthenticationManager.getUser(xCodeToken, getAuthenticationData(xCodeToken));
List<String> authorities = uaaUser.getAuthorities().stream().map(GrantedAuthority::getAuthority).collect(Collectors.toList());
assertThat(authorities, containsInAnyOrder("openid", "some.other.scope", "closedid"));
}
}