Polish Introspection Authentication Converter
- Added Reactive Support - Separated SCOPE claim and authorities work - Adjusted for style Issue gh-14198
This commit is contained in:
parent
8279b22940
commit
10d88cdf28
|
@ -22,6 +22,7 @@ import java.util.ArrayList;
|
|||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
|
@ -69,7 +70,7 @@ public class SpringOpaqueTokenIntrospector implements OpaqueTokenIntrospector {
|
|||
|
||||
private Converter<String, RequestEntity<?>> requestEntityConverter;
|
||||
|
||||
private Converter<OAuth2TokenIntrospectionClaimAccessor, OAuth2AuthenticatedPrincipal> authenticationConverter;
|
||||
private Converter<OAuth2TokenIntrospectionClaimAccessor, ? extends OAuth2AuthenticatedPrincipal> authenticationConverter = this::defaultAuthenticationConverter;
|
||||
|
||||
/**
|
||||
* Creates a {@code OpaqueTokenAuthenticationProvider} with the provided parameters
|
||||
|
@ -85,7 +86,6 @@ public class SpringOpaqueTokenIntrospector implements OpaqueTokenIntrospector {
|
|||
RestTemplate restTemplate = new RestTemplate();
|
||||
restTemplate.getInterceptors().add(new BasicAuthenticationInterceptor(clientId, clientSecret));
|
||||
this.restOperations = restTemplate;
|
||||
this.authenticationConverter = this.defaultAuthenticationConverter();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -100,7 +100,6 @@ public class SpringOpaqueTokenIntrospector implements OpaqueTokenIntrospector {
|
|||
Assert.notNull(restOperations, "restOperations cannot be null");
|
||||
this.requestEntityConverter = this.defaultRequestEntityConverter(URI.create(introspectionUri));
|
||||
this.restOperations = restOperations;
|
||||
this.authenticationConverter = this.defaultAuthenticationConverter();
|
||||
}
|
||||
|
||||
private Converter<String, RequestEntity<?>> defaultRequestEntityConverter(URI introspectionUri) {
|
||||
|
@ -131,8 +130,8 @@ public class SpringOpaqueTokenIntrospector implements OpaqueTokenIntrospector {
|
|||
}
|
||||
ResponseEntity<Map<String, Object>> responseEntity = makeRequest(requestEntity);
|
||||
Map<String, Object> claims = adaptToNimbusResponse(responseEntity);
|
||||
convertClaimsSet(claims);
|
||||
return this.authenticationConverter.convert(() -> claims);
|
||||
OAuth2TokenIntrospectionClaimAccessor accessor = convertClaimsSet(claims);
|
||||
return this.authenticationConverter.convert(accessor);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -183,7 +182,7 @@ public class SpringOpaqueTokenIntrospector implements OpaqueTokenIntrospector {
|
|||
return claims;
|
||||
}
|
||||
|
||||
private Map<String, Object> convertClaimsSet(Map<String, Object> claims) {
|
||||
private OAuth2TokenIntrospectionClaimAccessor convertClaimsSet(Map<String, Object> claims) {
|
||||
claims.computeIfPresent(OAuth2TokenIntrospectionClaimNames.AUD, (k, v) -> {
|
||||
if (v instanceof String) {
|
||||
return Collections.singletonList(v);
|
||||
|
@ -216,7 +215,28 @@ public class SpringOpaqueTokenIntrospector implements OpaqueTokenIntrospector {
|
|||
claims.computeIfPresent(OAuth2TokenIntrospectionClaimNames.ISS, (k, v) -> v.toString());
|
||||
claims.computeIfPresent(OAuth2TokenIntrospectionClaimNames.NBF,
|
||||
(k, v) -> Instant.ofEpochSecond(((Number) v).longValue()));
|
||||
return claims;
|
||||
claims.computeIfPresent(OAuth2TokenIntrospectionClaimNames.SCOPE,
|
||||
(k, v) -> (v instanceof String s) ? new ArrayListFromString(s.split(" ")) : v);
|
||||
return () -> claims;
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* Sets the {@link Converter Converter<OAuth2TokenIntrospectionClaimAccessor,
|
||||
* OAuth2AuthenticatedPrincipal>} to use. Defaults to
|
||||
* {@link SpringOpaqueTokenIntrospector#defaultAuthenticationConverter}.
|
||||
* </p>
|
||||
* <p>
|
||||
* Use if you need a custom mapping of OAuth 2.0 token claims to the authenticated
|
||||
* principal.
|
||||
* </p>
|
||||
* @param authenticationConverter the converter
|
||||
* @since 6.3
|
||||
*/
|
||||
public void setAuthenticationConverter(
|
||||
Converter<OAuth2TokenIntrospectionClaimAccessor, ? extends OAuth2AuthenticatedPrincipal> authenticationConverter) {
|
||||
Assert.notNull(authenticationConverter, "converter cannot be null");
|
||||
this.authenticationConverter = authenticationConverter;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -229,43 +249,30 @@ public class SpringOpaqueTokenIntrospector implements OpaqueTokenIntrospector {
|
|||
* OAuth2AuthenticatedPrincipal>}
|
||||
* @since 6.3
|
||||
*/
|
||||
private Converter<OAuth2TokenIntrospectionClaimAccessor, OAuth2AuthenticatedPrincipal> defaultAuthenticationConverter() {
|
||||
return (accessor) -> {
|
||||
Map<String, Object> claims = accessor.getClaims();
|
||||
Collection<GrantedAuthority> authorities = new ArrayList<>();
|
||||
|
||||
claims.computeIfPresent(OAuth2TokenIntrospectionClaimNames.SCOPE, (k, v) -> {
|
||||
if (v instanceof String) {
|
||||
Collection<String> scopes = Arrays.asList(((String) v).split(" "));
|
||||
for (String scope : scopes) {
|
||||
authorities.add(new SimpleGrantedAuthority(AUTHORITY_PREFIX + scope));
|
||||
}
|
||||
return scopes;
|
||||
}
|
||||
return v;
|
||||
});
|
||||
|
||||
return new OAuth2IntrospectionAuthenticatedPrincipal(claims, authorities);
|
||||
};
|
||||
private OAuth2IntrospectionAuthenticatedPrincipal defaultAuthenticationConverter(
|
||||
OAuth2TokenIntrospectionClaimAccessor accessor) {
|
||||
Collection<GrantedAuthority> authorities = authorities(accessor.getScopes());
|
||||
return new OAuth2IntrospectionAuthenticatedPrincipal(accessor.getClaims(), authorities);
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* Sets the {@link Converter Converter<OAuth2TokenIntrospectionClaimAccessor,
|
||||
* OAuth2AuthenticatedPrincipal>} to use. Defaults to
|
||||
* {@link SpringOpaqueTokenIntrospector#defaultAuthenticationConverter()}.
|
||||
* </p>
|
||||
* <p>
|
||||
* Use if you need a custom mapping of OAuth 2.0 token claims to the authenticated
|
||||
* principal.
|
||||
* </p>
|
||||
* @param authenticationConverter the converter
|
||||
* @since 6.3
|
||||
*/
|
||||
public void setAuthenticationConverter(
|
||||
Converter<OAuth2TokenIntrospectionClaimAccessor, OAuth2AuthenticatedPrincipal> authenticationConverter) {
|
||||
Assert.notNull(authenticationConverter, "converter cannot be null");
|
||||
this.authenticationConverter = authenticationConverter;
|
||||
private Collection<GrantedAuthority> authorities(List<String> scopes) {
|
||||
if (!(scopes instanceof ArrayListFromString)) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
Collection<GrantedAuthority> authorities = new ArrayList<>();
|
||||
for (String scope : scopes) {
|
||||
authorities.add(new SimpleGrantedAuthority(AUTHORITY_PREFIX + scope));
|
||||
}
|
||||
return authorities;
|
||||
}
|
||||
|
||||
// gh-7563
|
||||
private static final class ArrayListFromString extends ArrayList<String> {
|
||||
|
||||
ArrayListFromString(String... elements) {
|
||||
super(Arrays.asList(elements));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright 2002-2021 the original author or authors.
|
||||
* Copyright 2002-2023 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.
|
||||
|
@ -22,11 +22,13 @@ import java.util.ArrayList;
|
|||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.core.ParameterizedTypeReference;
|
||||
import org.springframework.core.convert.converter.Converter;
|
||||
import org.springframework.core.io.buffer.DataBuffer;
|
||||
import org.springframework.core.io.buffer.DataBufferUtils;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
|
@ -35,6 +37,7 @@ import org.springframework.http.MediaType;
|
|||
import org.springframework.security.core.GrantedAuthority;
|
||||
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||
import org.springframework.security.oauth2.core.OAuth2AuthenticatedPrincipal;
|
||||
import org.springframework.security.oauth2.core.OAuth2TokenIntrospectionClaimAccessor;
|
||||
import org.springframework.security.oauth2.core.OAuth2TokenIntrospectionClaimNames;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.web.reactive.function.BodyInserters;
|
||||
|
@ -61,6 +64,8 @@ public class SpringReactiveOpaqueTokenIntrospector implements ReactiveOpaqueToke
|
|||
|
||||
private final WebClient webClient;
|
||||
|
||||
private Converter<OAuth2TokenIntrospectionClaimAccessor, Mono<? extends OAuth2AuthenticatedPrincipal>> authenticationConverter = this::defaultAuthenticationConverter;
|
||||
|
||||
/**
|
||||
* Creates a {@code OpaqueTokenReactiveAuthenticationManager} with the provided
|
||||
* parameters
|
||||
|
@ -96,6 +101,8 @@ public class SpringReactiveOpaqueTokenIntrospector implements ReactiveOpaqueToke
|
|||
.flatMap(this::makeRequest)
|
||||
.flatMap(this::adaptToNimbusResponse)
|
||||
.map(this::convertClaimsSet)
|
||||
.flatMap(this.authenticationConverter::convert)
|
||||
.cast(OAuth2AuthenticatedPrincipal.class)
|
||||
.onErrorMap((e) -> !(e instanceof OAuth2IntrospectionException), this::onError);
|
||||
// @formatter:on
|
||||
}
|
||||
|
@ -135,7 +142,7 @@ public class SpringReactiveOpaqueTokenIntrospector implements ReactiveOpaqueToke
|
|||
.switchIfEmpty(Mono.error(() -> new BadOpaqueTokenException("Provided token isn't active")));
|
||||
}
|
||||
|
||||
private OAuth2AuthenticatedPrincipal convertClaimsSet(Map<String, Object> claims) {
|
||||
private OAuth2TokenIntrospectionClaimAccessor convertClaimsSet(Map<String, Object> claims) {
|
||||
claims.computeIfPresent(OAuth2TokenIntrospectionClaimNames.AUD, (k, v) -> {
|
||||
if (v instanceof String) {
|
||||
return Collections.singletonList(v);
|
||||
|
@ -168,22 +175,58 @@ public class SpringReactiveOpaqueTokenIntrospector implements ReactiveOpaqueToke
|
|||
claims.computeIfPresent(OAuth2TokenIntrospectionClaimNames.ISS, (k, v) -> v.toString());
|
||||
claims.computeIfPresent(OAuth2TokenIntrospectionClaimNames.NBF,
|
||||
(k, v) -> Instant.ofEpochSecond(((Number) v).longValue()));
|
||||
Collection<GrantedAuthority> authorities = new ArrayList<>();
|
||||
claims.computeIfPresent(OAuth2TokenIntrospectionClaimNames.SCOPE, (k, v) -> {
|
||||
if (v instanceof String) {
|
||||
Collection<String> scopes = Arrays.asList(((String) v).split(" "));
|
||||
for (String scope : scopes) {
|
||||
authorities.add(new SimpleGrantedAuthority(AUTHORITY_PREFIX + scope));
|
||||
}
|
||||
return scopes;
|
||||
}
|
||||
return v;
|
||||
});
|
||||
return new OAuth2IntrospectionAuthenticatedPrincipal(claims, authorities);
|
||||
claims.computeIfPresent(OAuth2TokenIntrospectionClaimNames.SCOPE,
|
||||
(k, v) -> (v instanceof String s) ? new ArrayListFromString(s.split(" ")) : v);
|
||||
return () -> claims;
|
||||
}
|
||||
|
||||
private OAuth2IntrospectionException onError(Throwable ex) {
|
||||
return new OAuth2IntrospectionException(ex.getMessage(), ex);
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* Sets the {@link Converter Converter<OAuth2TokenIntrospectionClaimAccessor,
|
||||
* OAuth2AuthenticatedPrincipal>} to use. Defaults to
|
||||
* {@link SpringReactiveOpaqueTokenIntrospector#defaultAuthenticationConverter}.
|
||||
* </p>
|
||||
* <p>
|
||||
* Use if you need a custom mapping of OAuth 2.0 token claims to the authenticated
|
||||
* principal.
|
||||
* </p>
|
||||
* @param authenticationConverter the converter
|
||||
* @since 6.3
|
||||
*/
|
||||
public void setAuthenticationConverter(
|
||||
Converter<OAuth2TokenIntrospectionClaimAccessor, Mono<? extends OAuth2AuthenticatedPrincipal>> authenticationConverter) {
|
||||
Assert.notNull(authenticationConverter, "authenticationConverter cannot be null");
|
||||
this.authenticationConverter = authenticationConverter;
|
||||
}
|
||||
|
||||
private Mono<OAuth2IntrospectionAuthenticatedPrincipal> defaultAuthenticationConverter(
|
||||
OAuth2TokenIntrospectionClaimAccessor accessor) {
|
||||
Collection<GrantedAuthority> authorities = authorities(accessor.getScopes());
|
||||
return Mono.just(new OAuth2IntrospectionAuthenticatedPrincipal(accessor.getClaims(), authorities));
|
||||
}
|
||||
|
||||
private Collection<GrantedAuthority> authorities(List<String> scopes) {
|
||||
if (!(scopes instanceof ArrayListFromString)) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
Collection<GrantedAuthority> authorities = new ArrayList<>();
|
||||
for (String scope : scopes) {
|
||||
authorities.add(new SimpleGrantedAuthority(AUTHORITY_PREFIX + scope));
|
||||
}
|
||||
return authorities;
|
||||
}
|
||||
|
||||
// gh-7563
|
||||
private static final class ArrayListFromString extends ArrayList<String> {
|
||||
|
||||
ArrayListFromString(String... elements) {
|
||||
super(Arrays.asList(elements));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright 2002-2021 the original author or authors.
|
||||
* Copyright 2002-2023 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.
|
||||
|
@ -33,10 +33,12 @@ import org.junit.jupiter.api.Test;
|
|||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.core.ParameterizedTypeReference;
|
||||
import org.springframework.core.convert.converter.Converter;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.security.oauth2.core.OAuth2AuthenticatedPrincipal;
|
||||
import org.springframework.security.oauth2.core.OAuth2TokenIntrospectionClaimAccessor;
|
||||
import org.springframework.security.oauth2.core.OAuth2TokenIntrospectionClaimNames;
|
||||
import org.springframework.web.reactive.function.client.ClientResponse;
|
||||
import org.springframework.web.reactive.function.client.WebClient;
|
||||
|
@ -44,9 +46,11 @@ import org.springframework.web.reactive.function.client.WebClient;
|
|||
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.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.spy;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
/**
|
||||
* Tests for {@link SpringReactiveOpaqueTokenIntrospector}
|
||||
|
@ -193,6 +197,30 @@ public class SpringReactiveOpaqueTokenIntrospectorTests {
|
|||
// @formatter:on
|
||||
}
|
||||
|
||||
@Test
|
||||
public void setAuthenticationConverterWhenConverterIsNullThenExceptionIsThrown() {
|
||||
WebClient web = mock(WebClient.class);
|
||||
SpringReactiveOpaqueTokenIntrospector introspectionClient = new SpringReactiveOpaqueTokenIntrospector(
|
||||
INTROSPECTION_URL, web);
|
||||
assertThatExceptionOfType(IllegalArgumentException.class)
|
||||
.isThrownBy(() -> introspectionClient.setAuthenticationConverter(null));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void setAuthenticationConverterWhenNonNullConverterGivenThenConverterUsed() {
|
||||
WebClient web = mockResponse(ACTIVE_RESPONSE);
|
||||
Converter<OAuth2TokenIntrospectionClaimAccessor, Mono<? extends OAuth2AuthenticatedPrincipal>> authenticationConverter = mock(
|
||||
Converter.class);
|
||||
OAuth2AuthenticatedPrincipal oAuth2AuthenticatedPrincipal = mock(OAuth2AuthenticatedPrincipal.class);
|
||||
String tokenToIntrospect = "some token";
|
||||
given(authenticationConverter.convert(any())).willReturn((Mono) Mono.just(oAuth2AuthenticatedPrincipal));
|
||||
SpringReactiveOpaqueTokenIntrospector introspectionClient = new SpringReactiveOpaqueTokenIntrospector(
|
||||
INTROSPECTION_URL, web);
|
||||
introspectionClient.setAuthenticationConverter(authenticationConverter);
|
||||
introspectionClient.introspect(tokenToIntrospect).block();
|
||||
verify(authenticationConverter).convert(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void constructorWhenIntrospectionUriIsEmptyThenIllegalArgumentException() {
|
||||
assertThatIllegalArgumentException()
|
||||
|
|
Loading…
Reference in New Issue