/*
*
* Copyright 2016 Netflix, Inc.
*
* 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 com.netflix.genie.web.security.oauth2.pingfederate;
import com.github.tomakehurst.wiremock.client.WireMock;
import com.github.tomakehurst.wiremock.core.Options;
import com.github.tomakehurst.wiremock.junit.WireMockRule;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.netflix.genie.test.categories.UnitTest;
import com.netflix.spectator.api.Counter;
import com.netflix.spectator.api.Id;
import com.netflix.spectator.api.Registry;
import com.netflix.spectator.api.Timer;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import org.hamcrest.Matchers;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.experimental.categories.Category;
import org.mockito.Mockito;
import org.springframework.boot.autoconfigure.security.oauth2.resource.ResourceServerProperties;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.oauth2.common.exceptions.InvalidTokenException;
import org.springframework.security.oauth2.provider.OAuth2Authentication;
import org.springframework.security.oauth2.provider.OAuth2Request;
import org.springframework.security.oauth2.provider.token.AccessTokenConverter;
import org.springframework.security.oauth2.provider.token.DefaultAccessTokenConverter;
import org.springframework.web.client.HttpClientErrorException;
import org.springframework.web.client.HttpServerErrorException;
import org.springframework.web.client.RestTemplate;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
/**
* Unit tests for PingFederateRemoteTokenServices.
*
* @author tgianos
* @since 3.0.0
*/
@Category(UnitTest.class)
public class PingFederateRemoteTokenServicesUnitTests {
private static final String CLIENT_ID = UUID.randomUUID().toString();
private static final String CLIENT_SECRET = UUID.randomUUID().toString();
private static final String CHECK_TOKEN_ENDPOINT_URL = UUID.randomUUID().toString();
/**
* Create a mock server.
*/
@Rule
@SuppressFBWarnings("URF_UNREAD_PUBLIC_OR_PROTECTED_FIELD")
public WireMockRule wireMockRule = new WireMockRule(Options.DYNAMIC_PORT);
private ResourceServerProperties resourceServerProperties;
private Registry registry;
private Id validationError;
private Timer authenticationTimer;
/**
* Setup for the tests.
*/
@Before
public void setup() {
this.resourceServerProperties = new ResourceServerProperties(CLIENT_ID, CLIENT_SECRET);
this.resourceServerProperties.setTokenInfoUri(CHECK_TOKEN_ENDPOINT_URL);
this.registry = Mockito.mock(Registry.class);
this.validationError = Mockito.mock(Id.class);
this.authenticationTimer = Mockito.mock(Timer.class);
final Timer pingFederateAPITimer = Mockito.mock(Timer.class);
Mockito.when(this.registry.createId(Mockito.anyString())).thenReturn(this.validationError);
Mockito
.when(this.registry.timer(PingFederateRemoteTokenServices.AUTHENTICATION_TIMER_NAME))
.thenReturn(this.authenticationTimer);
Mockito
.when(this.registry.timer(PingFederateRemoteTokenServices.API_TIMER_NAME))
.thenReturn(pingFederateAPITimer);
}
/**
* Make sure we can't construct without a client id.
*/
@Test(expected = IllegalStateException.class)
public void cantConstructWithoutClientId() {
final ResourceServerProperties properties = new ResourceServerProperties(null, null);
final AccessTokenConverter converter = new DefaultAccessTokenConverter();
new PingFederateRemoteTokenServices(properties, converter, this.registry);
}
/**
* Make sure we can't construct without a client secret.
*/
@Test(expected = IllegalStateException.class)
public void cantConstructWithoutClientSecret() {
final ResourceServerProperties properties = new ResourceServerProperties("AnID", null);
final AccessTokenConverter converter = new DefaultAccessTokenConverter();
new PingFederateRemoteTokenServices(properties, converter, this.registry);
}
/**
* Make sure we can't construct without a check token url.
*/
@Test(expected = IllegalStateException.class)
public void cantConstructWithoutCheckTokenURL() {
this.resourceServerProperties.setTokenInfoUri(null);
final AccessTokenConverter converter = new DefaultAccessTokenConverter();
new PingFederateRemoteTokenServices(this.resourceServerProperties, converter, this.registry);
}
/**
* Make sure we can validate a token.
*/
@Test
public void canLoadAuthentication() {
final AccessTokenConverter converter = Mockito.mock(AccessTokenConverter.class);
final RestTemplate restTemplate = Mockito.mock(RestTemplate.class);
final PingFederateRemoteTokenServices services
= new PingFederateRemoteTokenServices(this.resourceServerProperties, converter, this.registry);
services.setRestTemplate(restTemplate);
final String accessToken = UUID.randomUUID().toString();
final String clientId = UUID.randomUUID().toString();
final String scope1 = UUID.randomUUID().toString();
final String scope2 = UUID.randomUUID().toString();
final Map<String, Object> map = Maps.newHashMap();
map.put(PingFederateRemoteTokenServices.CLIENT_ID_KEY, clientId);
map.put(
PingFederateRemoteTokenServices.SCOPE_KEY,
scope1 + " " + scope2
);
@SuppressWarnings("unchecked")
final ResponseEntity<Map> response = Mockito.mock(ResponseEntity.class);
Mockito.when(
restTemplate.exchange(
Mockito.eq(CHECK_TOKEN_ENDPOINT_URL),
Mockito.eq(HttpMethod.POST),
Mockito.any(HttpEntity.class),
Mockito.eq(Map.class)
)
).thenReturn(response);
Mockito.when(response.getBody()).thenReturn(map);
final SimpleGrantedAuthority scope1Authority = new SimpleGrantedAuthority(scope1);
final SimpleGrantedAuthority scope2Authority = new SimpleGrantedAuthority(scope2);
final Set<GrantedAuthority> authorities
= Sets.newHashSet(scope1Authority, scope2Authority);
final Authentication authentication = new UsernamePasswordAuthenticationToken(
clientId,
"NA",
authorities
);
final OAuth2Authentication oauth2Authentication
= new OAuth2Authentication(Mockito.mock(OAuth2Request.class), authentication);
Mockito.when(
converter.extractAuthentication(Mockito.eq(map))
).thenReturn(oauth2Authentication);
final OAuth2Authentication result = services.loadAuthentication(accessToken);
Assert.assertThat(result, Matchers.is(oauth2Authentication));
Assert.assertThat(result.getPrincipal(), Matchers.is(clientId));
Assert.assertThat(result.getAuthorities().size(), Matchers.is(2));
Assert.assertTrue(result.getAuthorities().contains(scope1Authority));
Assert.assertTrue(result.getAuthorities().contains(scope2Authority));
Mockito
.verify(this.authenticationTimer, Mockito.times(1))
.record(Mockito.anyLong(), Mockito.eq(TimeUnit.NANOSECONDS));
}
/**
* Make sure invalid response from server causes authentication to fail.
*/
@Test(expected = InvalidTokenException.class)
public void cantLoadAuthenticationOnError() {
final AccessTokenConverter converter = Mockito.mock(AccessTokenConverter.class);
final RestTemplate restTemplate = Mockito.mock(RestTemplate.class);
final PingFederateRemoteTokenServices services
= new PingFederateRemoteTokenServices(this.resourceServerProperties, converter, this.registry);
services.setRestTemplate(restTemplate);
final String accessToken = UUID.randomUUID().toString();
final Map<String, Object> map = Maps.newHashMap();
map.put(PingFederateRemoteTokenServices.ERROR_KEY, UUID.randomUUID().toString());
@SuppressWarnings("unchecked")
final ResponseEntity<Map> response = Mockito.mock(ResponseEntity.class);
Mockito.when(
restTemplate.exchange(
Mockito.eq(CHECK_TOKEN_ENDPOINT_URL),
Mockito.eq(HttpMethod.POST),
Mockito.any(HttpEntity.class),
Mockito.eq(Map.class)
)
).thenReturn(response);
Mockito.when(response.getBody()).thenReturn(map);
// Should throw exception based on error key being present
services.loadAuthentication(accessToken);
}
/**
* Make sure rest call failures are handled.
*/
@Test
public void cantLoadAuthenticationOnRestError() {
final String path = UUID.randomUUID().toString();
final String uri = "http://localhost:" + this.wireMockRule.port() + "/" + path;
final int status = HttpStatus.NOT_FOUND.value();
WireMock.post(WireMock.urlEqualTo(uri)).willReturn(WireMock.aResponse().withStatus(status));
final AccessTokenConverter converter = Mockito.mock(AccessTokenConverter.class);
this.resourceServerProperties = new ResourceServerProperties(CLIENT_ID, CLIENT_SECRET);
// Some resource no one should ever have
this.resourceServerProperties.setTokenInfoUri(uri);
final PingFederateRemoteTokenServices services
= new PingFederateRemoteTokenServices(this.resourceServerProperties, converter, this.registry);
final String accessToken = UUID.randomUUID().toString();
final Counter restErrorCounter = Mockito.mock(Counter.class);
final Id finalValidationId = Mockito.mock(Id.class);
Mockito
.when(this.validationError.withTag(
Mockito.anyString(),
Mockito.eq(Integer.toString(status)))
)
.thenReturn(finalValidationId);
Mockito.when(this.registry.counter(finalValidationId)).thenReturn(restErrorCounter);
// Should throw exception based on error key being present
try {
services.loadAuthentication(accessToken);
Assert.fail();
} catch (final HttpClientErrorException | HttpServerErrorException e) {
Mockito.verify(restErrorCounter, Mockito.times(1)).increment();
}
}
/**
* This method isn't implemented for Ping Federate currently. Make sure this fails in case we ever implement it
* and need to update the tests.
*/
@Test(expected = UnsupportedOperationException.class)
public void cantReadAccessToken() {
final AccessTokenConverter converter = new DefaultAccessTokenConverter();
final PingFederateRemoteTokenServices services
= new PingFederateRemoteTokenServices(this.resourceServerProperties, converter, this.registry);
services.readAccessToken(UUID.randomUUID().toString());
}
}