Add Token Introspection Endpoint

Closes gh-52
This commit is contained in:
Gerardo Roza
2021-02-25 17:47:08 -03:00
committed by Joe Grandja
parent 4e2626e8b7
commit 92e8c08ce6
15 changed files with 2184 additions and 3 deletions

View File

@@ -0,0 +1,229 @@
/*
* Copyright 2020-2021 the original author or authors.
*
* 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
*
* https://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 org.springframework.security.config.annotation.web.configurers.oauth2.server.authorization;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.isNull;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.httpBasic;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Rule;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Import;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration;
import org.springframework.security.config.test.SpringTestRule;
import org.springframework.security.oauth2.core.AbstractOAuth2Token;
import org.springframework.security.oauth2.core.OAuth2AccessToken;
import org.springframework.security.oauth2.core.OAuth2RefreshToken;
import org.springframework.security.oauth2.core.OAuth2TokenType;
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames2;
import org.springframework.security.oauth2.jose.TestJwks;
import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
import org.springframework.security.oauth2.server.authorization.TestOAuth2Authorizations;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
import org.springframework.security.oauth2.server.authorization.client.TestRegisteredClients;
import org.springframework.security.oauth2.server.authorization.web.OAuth2TokenIntrospectionEndpointFilter;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import com.nimbusds.jose.jwk.JWKSet;
import com.nimbusds.jose.jwk.source.JWKSource;
import com.nimbusds.jose.proc.SecurityContext;
import java.time.Duration;
import java.time.Instant;
import java.util.Arrays;
import java.util.HashSet;
/**
* Integration tests for the OAuth 2.0 Token Introspection endpoint.
*
* @author Gerardo Roza
*/
public class OAuth2TokenIntrospectionTests {
private static RegisteredClientRepository registeredClientRepository;
private static OAuth2AuthorizationService authorizationService;
private static JWKSource<SecurityContext> jwkSource;
@Rule
public final SpringTestRule spring = new SpringTestRule();
@Autowired
private MockMvc mvc;
@BeforeClass
public static void init() {
registeredClientRepository = mock(RegisteredClientRepository.class);
authorizationService = mock(OAuth2AuthorizationService.class);
JWKSet jwkSet = new JWKSet(TestJwks.DEFAULT_RSA_JWK);
jwkSource = (jwkSelector, securityContext) -> jwkSelector.select(jwkSet);
}
@Before
public void setup() {
reset(registeredClientRepository);
reset(authorizationService);
}
@Test
public void requestWhenIntrospectValidRefreshTokenThenActiveResponse() throws Exception {
this.spring.register(AuthorizationServerConfiguration.class).autowire();
RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
when(registeredClientRepository.findByClientId(eq(registeredClient.getClientId())))
.thenReturn(registeredClient);
OAuth2Authorization authorization = TestOAuth2Authorizations.authorization(registeredClient).build();
OAuth2RefreshToken token = authorization.getRefreshToken().getToken();
OAuth2TokenType tokenType = OAuth2TokenType.REFRESH_TOKEN;
when(authorizationService.findByToken(eq(token.getTokenValue()), isNull())).thenReturn(authorization);
// @formatter:off
this.mvc.perform(
MockMvcRequestBuilders.post(OAuth2TokenIntrospectionEndpointFilter.DEFAULT_TOKEN_INTROSPECTION_ENDPOINT_URI)
.params(getTokenIntrospectionRequestParameters(token, tokenType))
.with(httpBasic(registeredClient.getClientId(), registeredClient.getClientSecret())))
.andExpect(status().isOk())
.andExpect(jsonPath("$.active").value(true))
.andExpect(jsonPath("$.client_id").value("client-1"))
.andExpect(jsonPath("$.iat").isNotEmpty())
.andExpect(jsonPath("$.exp").isNotEmpty())
.andExpect(jsonPath("$.username").value("principal"));
// @formatter:on
verify(registeredClientRepository).findByClientId(eq(registeredClient.getClientId()));
verify(authorizationService).findByToken(eq(token.getTokenValue()), isNull());
}
@Test
public void requestWhenIntrospectValidAccessTokenThenActiveResponse() throws Exception {
this.spring.register(AuthorizationServerConfiguration.class).autowire();
RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
when(registeredClientRepository.findByClientId(eq(registeredClient.getClientId())))
.thenReturn(registeredClient);
Instant issuedAt = Instant.now();
Instant expiresAt = issuedAt.plus(Duration.ofHours(1));
OAuth2AccessToken accessToken = new OAuth2AccessToken(
OAuth2AccessToken.TokenType.BEARER, "token", issuedAt, expiresAt,
new HashSet<>(Arrays.asList("scope1", "Scope2")));
OAuth2TokenType tokenType = OAuth2TokenType.ACCESS_TOKEN;
OAuth2Authorization authorization = TestOAuth2Authorizations.authorization(registeredClient).token(accessToken)
.build();
when(authorizationService.findByToken(eq(accessToken.getTokenValue()), isNull())).thenReturn(authorization);
// @formatter:off
this.mvc.perform(
MockMvcRequestBuilders.post(OAuth2TokenIntrospectionEndpointFilter.DEFAULT_TOKEN_INTROSPECTION_ENDPOINT_URI)
.params(getTokenIntrospectionRequestParameters(accessToken, tokenType))
.with(httpBasic(registeredClient.getClientId(), registeredClient.getClientSecret())))
.andExpect(status().isOk())
.andExpect(jsonPath("$.active").value(true))
.andExpect(jsonPath("$.client_id").value("client-1"))
.andExpect(jsonPath("$.scope").isNotEmpty())
.andExpect(jsonPath("$.token_type").value(OAuth2AccessToken.TokenType.BEARER.getValue()))
.andExpect(jsonPath("$.iat").isNotEmpty())
.andExpect(jsonPath("$.exp").isNotEmpty())
.andExpect(jsonPath("$.username").value("principal"));
// @formatter:on
verify(registeredClientRepository).findByClientId(eq(registeredClient.getClientId()));
verify(authorizationService).findByToken(eq(accessToken.getTokenValue()), isNull());
}
@Test
public void requestWhenIntrospectTokenIssuedToDifferentClientThenActiveResponse() throws Exception {
this.spring.register(AuthorizationServerConfiguration.class).autowire();
RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
when(registeredClientRepository.findByClientId(eq(registeredClient.getClientId())))
.thenReturn(registeredClient);
RegisteredClient registeredClient2 = TestRegisteredClients.registeredClient2().build();
Instant issuedAt = Instant.now();
Instant expiresAt = issuedAt.plus(Duration.ofHours(1));
OAuth2AccessToken accessToken = new OAuth2AccessToken(
OAuth2AccessToken.TokenType.BEARER, "token", issuedAt, expiresAt,
new HashSet<>(Arrays.asList("scope1", "Scope2")));
OAuth2TokenType tokenType = OAuth2TokenType.ACCESS_TOKEN;
OAuth2Authorization authorization = TestOAuth2Authorizations.authorization(registeredClient2).token(accessToken)
.build();
when(authorizationService.findByToken(eq(accessToken.getTokenValue()), isNull())).thenReturn(authorization);
// @formatter:off
this.mvc.perform(
MockMvcRequestBuilders.post(OAuth2TokenIntrospectionEndpointFilter.DEFAULT_TOKEN_INTROSPECTION_ENDPOINT_URI)
.params(getTokenIntrospectionRequestParameters(accessToken, tokenType))
.with(httpBasic(registeredClient.getClientId(), registeredClient.getClientSecret())))
.andExpect(status().isOk())
.andExpect(jsonPath("$.active").value(true))
.andExpect(jsonPath("$.client_id").value("client-1"))
.andExpect(jsonPath("$.scope").isNotEmpty())
.andExpect(jsonPath("$.token_type").value(OAuth2AccessToken.TokenType.BEARER.getValue()))
.andExpect(jsonPath("$.iat").isNotEmpty())
.andExpect(jsonPath("$.exp").isNotEmpty())
.andExpect(jsonPath("$.username").value("principal"));
// @formatter:on
verify(registeredClientRepository).findByClientId(eq(registeredClient.getClientId()));
verify(authorizationService).findByToken(eq(accessToken.getTokenValue()), isNull());
}
private static MultiValueMap<String, String> getTokenIntrospectionRequestParameters(AbstractOAuth2Token token,
OAuth2TokenType tokenType) {
MultiValueMap<String, String> parameters = new LinkedMultiValueMap<>();
parameters.set(OAuth2ParameterNames2.TOKEN, token.getTokenValue());
parameters.set(OAuth2ParameterNames2.TOKEN_TYPE_HINT, tokenType.getValue());
return parameters;
}
@EnableWebSecurity
@Import(OAuth2AuthorizationServerConfiguration.class)
static class AuthorizationServerConfiguration {
@Bean
RegisteredClientRepository registeredClientRepository() {
return registeredClientRepository;
}
@Bean
OAuth2AuthorizationService authorizationService() {
return authorizationService;
}
@Bean
JWKSource<SecurityContext> jwkSource() {
return jwkSource;
}
}
}

View File

@@ -0,0 +1,193 @@
/*
* Copyright 2020-2021 the original author or authors.
*
* 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
*
* https://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 org.springframework.security.oauth2.core.http.converter;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.springframework.security.oauth2.core.OAuth2TokenIntrospectionClaimAccessor.ACTIVE;
import static org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames.CLIENT_ID;
import static org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames.SCOPE;
import static org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames.TOKEN_TYPE;
import static org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames.USERNAME;
import static org.springframework.security.oauth2.jwt.JwtClaimNames.AUD;
import static org.springframework.security.oauth2.jwt.JwtClaimNames.EXP;
import static org.springframework.security.oauth2.jwt.JwtClaimNames.IAT;
import static org.springframework.security.oauth2.jwt.JwtClaimNames.ISS;
import static org.springframework.security.oauth2.jwt.JwtClaimNames.JTI;
import static org.springframework.security.oauth2.jwt.JwtClaimNames.NBF;
import static org.springframework.security.oauth2.jwt.JwtClaimNames.SUB;
import org.assertj.core.api.Condition;
import org.junit.Test;
import org.springframework.core.convert.converter.Converter;
import org.springframework.http.HttpStatus;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.http.converter.HttpMessageNotWritableException;
import org.springframework.mock.http.MockHttpOutputMessage;
import org.springframework.mock.http.client.MockClientHttpResponse;
import org.springframework.security.oauth2.core.OAuth2AccessToken.TokenType;
import org.springframework.security.oauth2.core.http.converter.OAuth2TokenIntrospectionClaimsHttpMessageConverter;
import org.springframework.security.oauth2.core.OAuth2TokenIntrospectionClaims;
import java.time.Instant;
import java.util.Arrays;
import java.util.Collection;
import java.util.Map;
/**
* Tests for {@link OAuth2TokenIntrospectionClaimsHttpMessageConverter}
*
* @author Gerardo Roza
*/
public class OAuth2TokenIntrospectionClaimsHttpMessageConverterTests {
private final OAuth2TokenIntrospectionClaimsHttpMessageConverter messageConverter = new OAuth2TokenIntrospectionClaimsHttpMessageConverter();
@Test
public void supportsWhenOidcProviderConfigurationThenTrue() {
assertThat(this.messageConverter.supports(OAuth2TokenIntrospectionClaims.class)).isTrue();
}
@Test
public void setProviderConfigurationParametersConverterWhenNullThenThrowIllegalArgumentException() {
assertThatIllegalArgumentException()
.isThrownBy(() -> this.messageConverter.setTokenIntrospectionResponseParametersConverter(null));
}
@Test
public void setProviderConfigurationConverterWhenNullThenThrowIllegalArgumentException() {
assertThatIllegalArgumentException()
.isThrownBy(() -> this.messageConverter.setTokenIntrospectionResponseConverter(null));
}
@SuppressWarnings("unchecked")
@Test
public void readInternalWhenValidParametersThenSuccess() throws Exception {
// @formatter:off
String tokenIntrospectionResponseBody = "{\n"
+ " \"active\": true,\n"
+ " \"iss\": \"https://example.com/issuer1\",\n"
+ " \"scope\": \"scope1 Scope2\",\n"
+ " \"client_id\": \"clientId1\",\n"
+ " \"token_type\": \"Bearer\",\n"
+ " \"username\": \"username1\",\n"
+ " \"aud\": [\"audience1\", \"audience2\"],\n"
+ " \"exp\": 1607637467,\n"
+ " \"iat\": 1607633867,\n"
+ " \"nbf\": 1607633867,\n"
+ " \"sub\": \"subject1\",\n"
+ " \"jti\": \"jwtId1\"\n"
+ "}\n";
// @formatter:on
MockClientHttpResponse response = new MockClientHttpResponse(
tokenIntrospectionResponseBody.getBytes(), HttpStatus.OK);
OAuth2TokenIntrospectionClaims tokenIntrospectionResponse = this.messageConverter
.readInternal(OAuth2TokenIntrospectionClaims.class, response);
Map<String, Object> responseParameters = tokenIntrospectionResponse.getClaims();
Condition<Object> collectionContainsCondition = new Condition<>(
collection -> Collection.class.isAssignableFrom(collection.getClass())
&& ((Collection<String>) collection).contains("audience1")
&& ((Collection<String>) collection).contains("audience2"),
"collection contains entries");
// @formatter:off
assertThat(responseParameters)
.containsEntry(ACTIVE, true)
.containsEntry(SCOPE, "scope1 Scope2")
.containsEntry(CLIENT_ID, "clientId1")
.containsEntry(TOKEN_TYPE, "Bearer")
.containsEntry(USERNAME, "username1")
.hasEntrySatisfying(AUD, collectionContainsCondition)
.containsEntry(EXP, 1607637467L)
.containsEntry(IAT, 1607633867L)
.containsEntry(NBF, 1607633867L)
.containsEntry(ISS, "https://example.com/issuer1")
.containsEntry(JTI, "jwtId1")
.containsEntry(SUB, "subject1");
// @formatter:on
}
@Test
public void readInternalWhenFailingConverterThenThrowException() {
String errorMessage = "this is not a valid converter";
this.messageConverter.setTokenIntrospectionResponseConverter(source -> {
throw new RuntimeException(errorMessage);
});
MockClientHttpResponse response = new MockClientHttpResponse("{}".getBytes(), HttpStatus.OK);
assertThatExceptionOfType(HttpMessageNotReadableException.class)
.isThrownBy(() -> this.messageConverter.readInternal(OAuth2TokenIntrospectionClaims.class, response))
.withMessageContaining("An error occurred reading the Token Introspection Response")
.withMessageContaining(errorMessage);
}
@Test
public void writeInternalWhenTokenIntrospectionResponseThenSuccess() {
// @formatter:off
OAuth2TokenIntrospectionClaims providerConfiguration = OAuth2TokenIntrospectionClaims.builder(true)
.issuer("https://example.com/issuer1")
.scope("scope1 Scope2")
.clientId("clientId1")
.tokenType(TokenType.BEARER)
.username("username1")
.audience(Arrays.asList("audience1", "audience2"))
.expirationTime(Instant.ofEpochSecond(1607637467))
.issuedAt(Instant.ofEpochSecond(1607633867))
.notBefore(Instant.ofEpochSecond(1607633867))
.jwtId("jwtId1")
.subject("subject1").build();
// @formatter:on
MockHttpOutputMessage outputMessage = new MockHttpOutputMessage();
this.messageConverter.writeInternal(providerConfiguration, outputMessage);
String providerConfigurationResponse = outputMessage.getBodyAsString();
// @formatter:off
assertThat(providerConfigurationResponse)
.contains("\"iss\":\"https://example.com/issuer1\"")
.contains("\"active\":true")
.contains("\"scope\":\"scope1 Scope2\"")
.contains("\"client_id\":\"clientId1\"")
.contains("\"token_type\":\"Bearer\"")
.contains("\"username\":\"username1\"")
.contains("\"aud\":[\"audience1\",\"audience2\"]")
.contains("\"exp\":1607637467")
.contains("\"iat\":1607633867")
.contains("\"nbf\":1607633867")
.contains("\"jti\":\"jwtId1\"")
.contains("\"sub\":\"subject1\"");
// @formatter:on
}
@Test
public void writeInternalWhenWriteFailsThenThrowsException() {
String errorMessage = "this is not a valid converter";
Converter<OAuth2TokenIntrospectionClaims, Map<String, Object>> failingConverter = source -> {
throw new RuntimeException(errorMessage);
};
this.messageConverter.setTokenIntrospectionResponseParametersConverter(failingConverter);
OAuth2TokenIntrospectionClaims providerConfiguration = OAuth2TokenIntrospectionClaims.builder(true).build();
MockHttpOutputMessage outputMessage = new MockHttpOutputMessage();
assertThatThrownBy(() -> this.messageConverter.writeInternal(providerConfiguration, outputMessage))
.isInstanceOf(HttpMessageNotWritableException.class)
.hasMessageContaining("An error occurred writing the Token Introspection Response")
.hasMessageContaining(errorMessage);
}
}

View File

@@ -245,6 +245,22 @@ public class InMemoryOAuth2AuthorizationServiceTests {
assertThat(authorization).isEqualTo(result);
}
@Test
public void findByTokenWhenWrongTokenTypeThenNotFound() {
OAuth2RefreshToken refreshToken = new OAuth2RefreshToken("refresh-token", Instant.now());
OAuth2Authorization authorization = OAuth2Authorization.withRegisteredClient(REGISTERED_CLIENT)
.id(ID)
.principalName(PRINCIPAL_NAME)
.authorizationGrantType(AUTHORIZATION_GRANT_TYPE)
.refreshToken(refreshToken)
.build();
this.authorizationService.save(authorization);
OAuth2Authorization result = this.authorizationService.findByToken(
refreshToken.getTokenValue(), OAuth2TokenType.ACCESS_TOKEN);
assertThat(result).isNull();
}
@Test
public void findByTokenWhenTokenDoesNotExistThenNull() {
OAuth2Authorization result = this.authorizationService.findByToken(

View File

@@ -0,0 +1,249 @@
/*
* Copyright 2020-2021 the original author or authors.
*
* 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
*
* https://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 org.springframework.security.oauth2.server.authorization.authentication;
import static java.time.Duration.ofHours;
import static java.time.Instant.now;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.isNull;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import static org.springframework.security.oauth2.core.OAuth2TokenIntrospectionClaimAccessor.ACTIVE;
import static org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames.CLIENT_ID;
import static org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames.SCOPE;
import static org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames.TOKEN_TYPE;
import static org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames.USERNAME;
import static org.springframework.security.oauth2.jwt.JwtClaimNames.EXP;
import static org.springframework.security.oauth2.jwt.JwtClaimNames.IAT;
import org.junit.Before;
import org.junit.Test;
import org.springframework.security.authentication.TestingAuthenticationToken;
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
import org.springframework.security.oauth2.core.OAuth2AccessToken;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
import org.springframework.security.oauth2.core.OAuth2RefreshToken;
import org.springframework.security.oauth2.core.OAuth2TokenType;
import org.springframework.security.oauth2.jwt.JwtClaimNames;
import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
import org.springframework.security.oauth2.server.authorization.TestOAuth2Authorizations;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
import org.springframework.security.oauth2.server.authorization.client.TestRegisteredClients;
import java.time.Instant;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
/**
* Tests for {@link OAuth2TokenIntrospectionAuthenticationProvider}.
*
* @author Gerardo Roza
*/
public class OAuth2TokenIntrospectionAuthenticationProviderTests {
private OAuth2AuthorizationService authorizationService;
private OAuth2TokenIntrospectionAuthenticationProvider authenticationProvider;
@Before
public void setUp() {
this.authorizationService = mock(OAuth2AuthorizationService.class);
this.authenticationProvider = new OAuth2TokenIntrospectionAuthenticationProvider(this.authorizationService);
}
@Test
public void constructorWhenAuthorizationServiceNullThenThrowIllegalArgumentException() {
assertThatThrownBy(() -> new OAuth2TokenIntrospectionAuthenticationProvider(null))
.isInstanceOf(IllegalArgumentException.class).hasMessage("authorizationService cannot be null");
}
@Test
public void supportsWhenTypeOAuth2TokenIntrospectionAuthenticationTokenThenReturnTrue() {
assertThat(this.authenticationProvider.supports(OAuth2TokenIntrospectionAuthenticationToken.class)).isTrue();
}
@Test
public void authenticateWhenClientPrincipalNotOAuth2ClientAuthenticationTokenThenThrowOAuth2AuthenticationException() {
RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
TestingAuthenticationToken clientPrincipal = new TestingAuthenticationToken(
registeredClient.getClientId(), registeredClient.getClientSecret());
OAuth2TokenIntrospectionAuthenticationToken authentication = new OAuth2TokenIntrospectionAuthenticationToken(
"token", clientPrincipal, OAuth2TokenType.ACCESS_TOKEN.getValue());
assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication))
.isInstanceOf(OAuth2AuthenticationException.class)
.extracting(ex -> ((OAuth2AuthenticationException) ex).getError()).extracting("errorCode")
.isEqualTo(OAuth2ErrorCodes.INVALID_CLIENT);
}
@Test
public void authenticateWhenClientPrincipalNotAuthenticatedThenThrowOAuth2AuthenticationException() {
RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
OAuth2ClientAuthenticationToken clientPrincipal = new OAuth2ClientAuthenticationToken(
registeredClient.getClientId(), registeredClient.getClientSecret(), ClientAuthenticationMethod.BASIC,
null);
OAuth2TokenIntrospectionAuthenticationToken authentication = new OAuth2TokenIntrospectionAuthenticationToken(
"token", clientPrincipal, OAuth2TokenType.ACCESS_TOKEN.getValue());
assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication))
.isInstanceOf(OAuth2AuthenticationException.class)
.extracting(ex -> ((OAuth2AuthenticationException) ex).getError()).extracting("errorCode")
.isEqualTo(OAuth2ErrorCodes.INVALID_CLIENT);
}
@Test
public void authenticateWhenInvalidOAuth2TokenTypeThenThrowOAuth2AuthenticationException() {
RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
OAuth2ClientAuthenticationToken clientPrincipal = new OAuth2ClientAuthenticationToken(registeredClient);
OAuth2TokenIntrospectionAuthenticationToken authentication = new OAuth2TokenIntrospectionAuthenticationToken(
"token", clientPrincipal, "unsupportedOAuth2TokenType");
OAuth2TokenIntrospectionAuthenticationToken authenticationResult = (OAuth2TokenIntrospectionAuthenticationToken) this.authenticationProvider
.authenticate(authentication);
assertThat(authenticationResult.isAuthenticated()).isTrue();
assertThat(authenticationResult.isTokenActive()).isFalse();
}
@Test
public void authenticateWhenTokenNotFoundThenAuthenticatedButTokenNotActive() {
RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
OAuth2ClientAuthenticationToken clientPrincipal = new OAuth2ClientAuthenticationToken(registeredClient);
OAuth2TokenIntrospectionAuthenticationToken authentication = new OAuth2TokenIntrospectionAuthenticationToken(
"token", clientPrincipal, OAuth2TokenType.ACCESS_TOKEN.getValue());
OAuth2TokenIntrospectionAuthenticationToken authenticationResult = (OAuth2TokenIntrospectionAuthenticationToken) this.authenticationProvider
.authenticate(authentication);
assertThat(authenticationResult.isAuthenticated()).isTrue();
assertThat(authenticationResult.isTokenActive()).isFalse();
}
@Test
public void authenticateWhenInvalidatedTokenThenAuthenticatedButTokenNotActive() {
RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
OAuth2Authorization authorization = TestOAuth2Authorizations.authorization(registeredClient).build();
OAuth2AccessToken accessToken = authorization.getAccessToken().getToken();
authorization = OAuth2AuthenticationProviderUtils.invalidate(authorization, accessToken);
when(this.authorizationService.findByToken(eq(accessToken.getTokenValue()), isNull()))
.thenReturn(authorization);
OAuth2ClientAuthenticationToken clientPrincipal = new OAuth2ClientAuthenticationToken(registeredClient);
OAuth2TokenIntrospectionAuthenticationToken authentication = new OAuth2TokenIntrospectionAuthenticationToken(
accessToken.getTokenValue(), clientPrincipal, OAuth2TokenType.ACCESS_TOKEN.getValue());
OAuth2TokenIntrospectionAuthenticationToken authenticationResult = (OAuth2TokenIntrospectionAuthenticationToken) this.authenticationProvider
.authenticate(authentication);
assertThat(authenticationResult.isAuthenticated()).isTrue();
assertThat(authenticationResult.isTokenActive()).isFalse();
}
@Test
public void authenticateWhenValidAccessTokenThenActiveWithScopes() {
RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
OAuth2AccessToken accessToken = new OAuth2AccessToken(
OAuth2AccessToken.TokenType.BEARER, "access-token", Instant.now(), Instant.now().plusSeconds(300),
new HashSet<>(Arrays.asList("scope1", "Scope2")));
OAuth2Authorization authorization = TestOAuth2Authorizations.authorization(registeredClient)
.accessToken(accessToken).build();
when(this.authorizationService.findByToken(eq(accessToken.getTokenValue()), isNull()))
.thenReturn(authorization);
OAuth2ClientAuthenticationToken clientPrincipal = new OAuth2ClientAuthenticationToken(registeredClient);
OAuth2TokenIntrospectionAuthenticationToken authentication = new OAuth2TokenIntrospectionAuthenticationToken(
accessToken.getTokenValue(), clientPrincipal, OAuth2TokenType.ACCESS_TOKEN.getValue());
OAuth2TokenIntrospectionAuthenticationToken authenticationResult = (OAuth2TokenIntrospectionAuthenticationToken) this.authenticationProvider
.authenticate(authentication);
assertThat(authenticationResult.isAuthenticated()).isTrue();
assertThat(authenticationResult.isTokenActive()).isTrue();
assertThat(authenticationResult.getClaims()).containsEntry(ACTIVE, true)
.containsEntry(IAT, accessToken.getIssuedAt()).containsEntry(EXP, accessToken.getExpiresAt())
.containsEntry(TOKEN_TYPE, OAuth2AccessToken.TokenType.BEARER).containsEntry(CLIENT_ID, "client-1")
.containsEntry(USERNAME, "principal").containsKey(SCOPE)
.containsAllEntriesOf(authorization.getAccessToken().getClaims());
}
@Test
public void authenticateWhenValidRefreshTokenThenActive() {
RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
OAuth2Authorization authorization = TestOAuth2Authorizations.authorization(registeredClient).build();
OAuth2RefreshToken refreshToken = authorization.getRefreshToken().getToken();
when(this.authorizationService.findByToken(eq(refreshToken.getTokenValue()), isNull()))
.thenReturn(authorization);
OAuth2ClientAuthenticationToken clientPrincipal = new OAuth2ClientAuthenticationToken(registeredClient);
OAuth2TokenIntrospectionAuthenticationToken authentication = new OAuth2TokenIntrospectionAuthenticationToken(
refreshToken.getTokenValue(), clientPrincipal, OAuth2TokenType.REFRESH_TOKEN.getValue());
OAuth2TokenIntrospectionAuthenticationToken authenticationResult = (OAuth2TokenIntrospectionAuthenticationToken) this.authenticationProvider
.authenticate(authentication);
assertThat(authenticationResult.isAuthenticated()).isTrue();
assertThat(authenticationResult.isTokenActive()).isTrue();
assertThat(authenticationResult.getClaims()).containsEntry(ACTIVE, true)
.containsEntry(IAT, refreshToken.getIssuedAt()).containsEntry(EXP, refreshToken.getExpiresAt())
.containsEntry(USERNAME, "principal").containsEntry(CLIENT_ID, "client-1").hasSize(5);
}
@Test
public void authenticateWhenExpiredTokenThenAuthenticatedButTokenNotActive() {
RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
Instant expiresAt = Instant.now().minus(ofHours(1));
Instant issuedAt = expiresAt.minus(ofHours(1));
OAuth2AccessToken accessToken = new OAuth2AccessToken(
OAuth2AccessToken.TokenType.BEARER, "access-token", issuedAt, expiresAt);
OAuth2Authorization authorization = TestOAuth2Authorizations.authorization(registeredClient).token(accessToken)
.build();
when(this.authorizationService.findByToken(eq(accessToken.getTokenValue()), isNull()))
.thenReturn(authorization);
OAuth2ClientAuthenticationToken clientPrincipal = new OAuth2ClientAuthenticationToken(registeredClient);
OAuth2TokenIntrospectionAuthenticationToken authentication = new OAuth2TokenIntrospectionAuthenticationToken(
accessToken.getTokenValue(), clientPrincipal, OAuth2TokenType.ACCESS_TOKEN.getValue());
OAuth2TokenIntrospectionAuthenticationToken authenticationResult = (OAuth2TokenIntrospectionAuthenticationToken) this.authenticationProvider
.authenticate(authentication);
assertThat(authenticationResult.isAuthenticated()).isTrue();
assertThat(authenticationResult.isTokenActive()).isFalse();
assertThat(authenticationResult.getClaims()).isNull();
}
@Test
public void authenticateWhenInvalidNotBeforeClaimThenAuthenticatedButTokenNotActive() {
RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
Instant issuedAt = Instant.now();
Instant expiresAt = issuedAt.plus(ofHours(1));
OAuth2AccessToken accessToken = new OAuth2AccessToken(
OAuth2AccessToken.TokenType.BEARER, "access-token", issuedAt, expiresAt);
OAuth2Authorization authorization = TestOAuth2Authorizations.authorization(registeredClient)
.token(
accessToken,
(metadata) -> metadata.put(
OAuth2Authorization.Token.CLAIMS_METADATA_NAME,
Collections.singletonMap(JwtClaimNames.NBF, now().plus(ofHours(1)))))
.build();
when(this.authorizationService.findByToken(eq(accessToken.getTokenValue()), isNull()))
.thenReturn(authorization);
OAuth2ClientAuthenticationToken clientPrincipal = new OAuth2ClientAuthenticationToken(registeredClient);
OAuth2TokenIntrospectionAuthenticationToken authentication = new OAuth2TokenIntrospectionAuthenticationToken(
accessToken.getTokenValue(), clientPrincipal, OAuth2TokenType.ACCESS_TOKEN.getValue());
OAuth2TokenIntrospectionAuthenticationToken authenticationResult = (OAuth2TokenIntrospectionAuthenticationToken) this.authenticationProvider
.authenticate(authentication);
assertThat(authenticationResult.isAuthenticated()).isTrue();
assertThat(authenticationResult.isTokenActive()).isFalse();
assertThat(authenticationResult.getClaims()).isNull();
}
}

View File

@@ -0,0 +1,98 @@
/*
* Copyright 2020-2021 the original author or authors.
*
* 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
*
* https://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 org.springframework.security.oauth2.server.authorization.authentication;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import org.junit.Test;
import org.springframework.security.oauth2.core.OAuth2TokenType;
import org.springframework.security.oauth2.server.authorization.client.TestRegisteredClients;
import java.util.HashMap;
import java.util.Map;
/**
* Tests for {@link OAuth2TokenIntrospectionAuthenticationToken}.
*
* @author Gerardo Roza
*/
public class OAuth2TokenIntrospectionAuthenticationTokenTests {
private String tokenValue = "tokenValue";
private OAuth2ClientAuthenticationToken clientPrincipal = new OAuth2ClientAuthenticationToken(
TestRegisteredClients.registeredClient().build());
private String tokenTypeHint = OAuth2TokenType.ACCESS_TOKEN.getValue();
private Map<String, Object> claims = new HashMap<>();
@Test
public void constructorWhenTokenValueNullThenThrowIllegalArgumentException() {
assertThatThrownBy(
() -> new OAuth2TokenIntrospectionAuthenticationToken(null, this.clientPrincipal, this.tokenTypeHint))
.isInstanceOf(IllegalArgumentException.class).hasMessage("token cannot be empty");
}
@Test
public void constructorWhenClientPrincipalNullThenThrowIllegalArgumentException() {
assertThatThrownBy(
() -> new OAuth2TokenIntrospectionAuthenticationToken(this.tokenValue, null, this.tokenTypeHint))
.isInstanceOf(IllegalArgumentException.class).hasMessage("clientPrincipal cannot be null");
}
@Test
public void constructorWhenTokenAndClientPrincipalNullThenThrowIllegalArgumentException() {
assertThatThrownBy(() -> new OAuth2TokenIntrospectionAuthenticationToken(null, this.claims))
.isInstanceOf(IllegalArgumentException.class).hasMessage("clientPrincipal cannot be null");
}
@Test
public void constructorWhenTokenValueProvidedThenCreated() {
OAuth2TokenIntrospectionAuthenticationToken authentication = new OAuth2TokenIntrospectionAuthenticationToken(
this.tokenValue, this.clientPrincipal, this.tokenTypeHint);
assertThat(authentication.getTokenValue()).isEqualTo(this.tokenValue);
assertThat(authentication.getPrincipal()).isEqualTo(this.clientPrincipal);
assertThat(authentication.getTokenTypeHint()).isEqualTo(this.tokenTypeHint);
assertThat(authentication.getClaims()).isNull();
assertThat(authentication.isTokenActive()).isFalse();
assertThat(authentication.getCredentials().toString()).isEmpty();
assertThat(authentication.isAuthenticated()).isFalse();
}
@Test
public void constructorWhenTokenProvidedThenCreated() {
OAuth2TokenIntrospectionAuthenticationToken authentication = new OAuth2TokenIntrospectionAuthenticationToken(
this.clientPrincipal, this.claims);
assertThat(authentication.getTokenValue()).isNull();
assertThat(authentication.getPrincipal()).isEqualTo(this.clientPrincipal);
assertThat(authentication.getClaims()).isEqualTo(this.claims);
assertThat(authentication.isTokenActive()).isTrue();
assertThat(authentication.getTokenTypeHint()).isNull();
assertThat(authentication.getCredentials().toString()).isEmpty();
assertThat(authentication.isAuthenticated()).isTrue();
}
@Test
public void constructorWhenNullTokenProvidedThenCreatedAsTokenNotActive() {
OAuth2TokenIntrospectionAuthenticationToken authentication = new OAuth2TokenIntrospectionAuthenticationToken(
this.clientPrincipal, null);
assertThat(authentication.getTokenValue()).isNull();
assertThat(authentication.getPrincipal()).isEqualTo(this.clientPrincipal);
assertThat(authentication.getClaims()).isNull();
assertThat(authentication.isTokenActive()).isFalse();
assertThat(authentication.getTokenTypeHint()).isNull();
assertThat(authentication.getCredentials().toString()).isEmpty();
assertThat(authentication.isAuthenticated()).isTrue();
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2020 the original author or authors.
* Copyright 2020-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -66,7 +66,7 @@ public class ProviderSettingsTests {
.setting("name1", "value1")
.settings(settings -> settings.put("name2", "value2"));
assertThat(providerSettings.settings()).hasSize(6);
assertThat(providerSettings.settings()).hasSize(7);
assertThat(providerSettings.<String>setting("name1")).isEqualTo("value1");
assertThat(providerSettings.<String>setting("name2")).isEqualTo("value2");
}

View File

@@ -0,0 +1,278 @@
/*
* Copyright 2020-2021 the original author or authors.
*
* 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
*
* https://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 org.springframework.security.oauth2.server.authorization.web;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.assertj.core.api.Assertions.entry;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.when;
import static org.springframework.security.oauth2.core.OAuth2TokenIntrospectionClaimAccessor.ACTIVE;
import static org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames.CLIENT_ID;
import static org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames.SCOPE;
import static org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames.TOKEN_TYPE;
import static org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames2.TOKEN;
import static org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames2.TOKEN_TYPE_HINT;
import static org.springframework.security.oauth2.jwt.JwtClaimNames.EXP;
import static org.springframework.security.oauth2.jwt.JwtClaimNames.IAT;
import javax.servlet.FilterChain;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.assertj.core.api.Condition;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.springframework.http.HttpStatus;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.mock.http.client.MockClientHttpResponse;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.oauth2.core.OAuth2AccessToken;
import org.springframework.security.oauth2.core.OAuth2Error;
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
import org.springframework.security.oauth2.core.OAuth2TokenIntrospectionClaims;
import org.springframework.security.oauth2.core.OAuth2TokenType;
import org.springframework.security.oauth2.core.http.converter.OAuth2ErrorHttpMessageConverter;
import org.springframework.security.oauth2.core.http.converter.OAuth2TokenIntrospectionClaimsHttpMessageConverter;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientAuthenticationToken;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2TokenIntrospectionAuthenticationToken;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
import org.springframework.security.oauth2.server.authorization.client.TestRegisteredClients;
import java.time.Duration;
import java.time.Instant;
import java.util.HashMap;
import java.util.Map;
/**
* Tests for {@link OAuth2TokenIntrospectionEndpointFilter}.
*
* @author Gerardo Roza
*/
public class OAuth2TokenIntrospectionEndpointFilterTests {
private AuthenticationManager authenticationManager;
private OAuth2TokenIntrospectionEndpointFilter filter;
private final HttpMessageConverter<OAuth2Error> errorHttpResponseConverter = new OAuth2ErrorHttpMessageConverter();
private final HttpMessageConverter<OAuth2TokenIntrospectionClaims> tokenIntrospectionHttpResponseConverter = new OAuth2TokenIntrospectionClaimsHttpMessageConverter();
private final Condition<Object> scopesMatchesInAnyOrder = new Condition<>(
scopes -> scopes.equals("scope1 Scope2") || scopes.equals("Scope2 scope1"), "scopes match");
private final String tokenValue = "token.123";
@Before
public void setUp() {
this.authenticationManager = mock(AuthenticationManager.class);
this.filter = new OAuth2TokenIntrospectionEndpointFilter(this.authenticationManager);
}
@After
public void cleanup() {
SecurityContextHolder.clearContext();
}
@Test
public void constructorWhenAuthenticationManagerNullThenThrowIllegalArgumentException() {
assertThatThrownBy(() -> new OAuth2TokenIntrospectionEndpointFilter(null))
.isInstanceOf(IllegalArgumentException.class).hasMessage("authenticationManager cannot be null");
}
@Test
public void doFilterWhenNotIntrospectionRequestThenNotProcessed() throws Exception {
String requestUri = "/path";
MockHttpServletRequest request = new MockHttpServletRequest("POST", requestUri);
request.setServletPath(requestUri);
MockHttpServletResponse response = new MockHttpServletResponse();
FilterChain filterChain = mock(FilterChain.class);
this.filter.doFilter(request, response, filterChain);
verify(filterChain).doFilter(any(HttpServletRequest.class), any(HttpServletResponse.class));
}
@Test
public void doFilterWhenIntrospectionRequestGetThenNotProcessed() throws Exception {
String requestUri = OAuth2TokenIntrospectionEndpointFilter.DEFAULT_TOKEN_INTROSPECTION_ENDPOINT_URI;
MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri);
request.setServletPath(requestUri);
MockHttpServletResponse response = new MockHttpServletResponse();
FilterChain filterChain = mock(FilterChain.class);
this.filter.doFilter(request, response, filterChain);
verify(filterChain).doFilter(any(HttpServletRequest.class), any(HttpServletResponse.class));
}
@Test
public void doFilterWhenIntrospectionRequestMissingTokenParamThenInvalidRequestError() throws Exception {
MockHttpServletRequest request = createTokenIntrospectionRequest(
this.tokenValue, OAuth2TokenType.ACCESS_TOKEN.getValue());
request.removeParameter(TOKEN);
doFilterWhenTokenIntrospectionRequestInvalidParameterThenError(
TOKEN, OAuth2ErrorCodes.INVALID_REQUEST, request);
}
@Test
public void doFilterWhenTokenRequestMultipleTokenParamThenInvalidRequestError() throws Exception {
MockHttpServletRequest request = createTokenIntrospectionRequest(
this.tokenValue, OAuth2TokenType.ACCESS_TOKEN.getValue());
request.addParameter(TOKEN, "token.456");
doFilterWhenTokenIntrospectionRequestInvalidParameterThenError(
TOKEN, OAuth2ErrorCodes.INVALID_REQUEST, request);
}
@Test
public void doFilterWhenTokenRequestMultipleTokenTypeHintParamThenInvalidRequestError() throws Exception {
MockHttpServletRequest request = createTokenIntrospectionRequest(
this.tokenValue, OAuth2TokenType.ACCESS_TOKEN.getValue());
request.addParameter(TOKEN_TYPE_HINT, OAuth2TokenType.REFRESH_TOKEN.getValue());
doFilterWhenTokenIntrospectionRequestInvalidParameterThenError(
TOKEN_TYPE_HINT, OAuth2ErrorCodes.INVALID_REQUEST, request);
}
@Test
public void doFilterWhenIntrospectWithNullClaimsThenNotActiveTokenOkReponse() throws Exception {
MockHttpServletRequest request = createTokenIntrospectionRequest(
this.tokenValue, OAuth2TokenType.ACCESS_TOKEN.getValue());
MockHttpServletResponse response = new MockHttpServletResponse();
FilterChain filterChain = mock(FilterChain.class);
Authentication clientPrincipal = setupSecurityContext();
OAuth2TokenIntrospectionAuthenticationToken tokenIntrospectionAuthentication = new OAuth2TokenIntrospectionAuthenticationToken(
clientPrincipal, null);
when(this.authenticationManager.authenticate(any())).thenReturn(tokenIntrospectionAuthentication);
this.filter.doFilter(request, response, filterChain);
verifyNoInteractions(filterChain);
assertNotActiveTokenResponse(response);
}
@Test
public void doFilterWhenIntrospectWithClaimsThenActiveTokenReponse() throws Exception {
MockHttpServletRequest request = createTokenIntrospectionRequest(
this.tokenValue, OAuth2TokenType.ACCESS_TOKEN.getValue());
MockHttpServletResponse response = new MockHttpServletResponse();
FilterChain filterChain = mock(FilterChain.class);
Authentication clientPrincipal = setupSecurityContext();
Instant issuedAt = Instant.now();
Instant expiresAt = issuedAt.plus(Duration.ofHours(1));
String clientId = "clientId";
Map<String, Object> tokenIntrospectionClaims = new HashMap<>();
tokenIntrospectionClaims.put(ACTIVE, true);
tokenIntrospectionClaims.put(CLIENT_ID, clientId);
tokenIntrospectionClaims.put(IAT, issuedAt);
tokenIntrospectionClaims.put(EXP, expiresAt);
tokenIntrospectionClaims.put(TOKEN_TYPE, OAuth2AccessToken.TokenType.BEARER);
tokenIntrospectionClaims.put(SCOPE, "scope1 Scope2");
OAuth2TokenIntrospectionAuthenticationToken tokenIntrospectionAuthentication = new OAuth2TokenIntrospectionAuthenticationToken(
clientPrincipal, tokenIntrospectionClaims);
when(this.authenticationManager.authenticate(any())).thenReturn(tokenIntrospectionAuthentication);
this.filter.doFilter(request, response, filterChain);
verifyNoInteractions(filterChain);
assertThat(response.getStatus()).isEqualTo(HttpStatus.OK.value());
OAuth2TokenIntrospectionClaims tokenIntrospectionResponse = readTokenIntrospectionResponse(response);
Map<String, Object> responseMap = tokenIntrospectionResponse.getClaims();
// @formatter:off
assertThat(responseMap).contains(
entry(ACTIVE, true),
entry(CLIENT_ID, clientId),
entry(TOKEN_TYPE, OAuth2AccessToken.TokenType.BEARER.getValue()),
entry(EXP, expiresAt.getEpochSecond()),
entry(IAT, issuedAt.getEpochSecond()))
.hasEntrySatisfying(SCOPE, scopesMatchesInAnyOrder)
.hasSize(6);
// @formatter: on
}
private void assertNotActiveTokenResponse(MockHttpServletResponse response) throws Exception {
assertThat(response.getStatus()).isEqualTo(HttpStatus.OK.value());
OAuth2TokenIntrospectionClaims tokenIntrospectionResponse = readTokenIntrospectionResponse(response);
assertThat(tokenIntrospectionResponse.getClaims()).containsEntry("active", false).hasSize(1);
}
private OAuth2Error readError(MockHttpServletResponse response) throws Exception {
MockClientHttpResponse httpResponse = new MockClientHttpResponse(response.getContentAsByteArray(),
HttpStatus.valueOf(response.getStatus()));
return this.errorHttpResponseConverter.read(OAuth2Error.class, httpResponse);
}
private OAuth2TokenIntrospectionClaims readTokenIntrospectionResponse(MockHttpServletResponse response) throws Exception {
MockClientHttpResponse httpResponse = new MockClientHttpResponse(response.getContentAsByteArray(),
HttpStatus.valueOf(response.getStatus()));
return this.tokenIntrospectionHttpResponseConverter.read(OAuth2TokenIntrospectionClaims.class, httpResponse);
}
private void doFilterWhenTokenIntrospectionRequestInvalidParameterThenError(String parameterName, String errorCode,
MockHttpServletRequest request) throws Exception {
MockHttpServletResponse response = new MockHttpServletResponse();
FilterChain filterChain = mock(FilterChain.class);
setupSecurityContext();
this.filter.doFilter(request, response, filterChain);
verifyNoInteractions(filterChain);
assertThat(response.getStatus()).isEqualTo(HttpStatus.BAD_REQUEST.value());
OAuth2Error error = readError(response);
assertThat(error.getErrorCode()).isEqualTo(errorCode);
assertThat(error.getDescription()).isEqualTo("OAuth 2.0 Token Introspection Parameter: " + parameterName);
}
private static Authentication setupSecurityContext() {
RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
Authentication clientPrincipal = new OAuth2ClientAuthenticationToken(registeredClient);
SecurityContext securityContext = SecurityContextHolder.createEmptyContext();
securityContext.setAuthentication(clientPrincipal);
SecurityContextHolder.setContext(securityContext);
return clientPrincipal;
}
private static MockHttpServletRequest createTokenIntrospectionRequest(String token, String tokenTypeHint) {
String requestUri = OAuth2TokenIntrospectionEndpointFilter.DEFAULT_TOKEN_INTROSPECTION_ENDPOINT_URI;
MockHttpServletRequest request = new MockHttpServletRequest("POST", requestUri);
request.setServletPath(requestUri);
request.addParameter(TOKEN, token);
request.addParameter(TOKEN_TYPE_HINT, tokenTypeHint);
return request;
}
}