Add Introspection Authentication Converter

Closes #14198

Signed-off-by: ahmd-nabil <ahm3dnabil99@gmail.com>
This commit is contained in:
ahmd-nabil 2023-12-02 01:26:40 +02:00 committed by Josh Cummings
parent 1ce1ff92de
commit 8279b22940
2 changed files with 89 additions and 16 deletions

View File

@ -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.
@ -39,6 +39,7 @@ import org.springframework.http.client.support.BasicAuthenticationInterceptor;
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.util.LinkedMultiValueMap;
@ -68,6 +69,8 @@ public class SpringOpaqueTokenIntrospector implements OpaqueTokenIntrospector {
private Converter<String, RequestEntity<?>> requestEntityConverter;
private Converter<OAuth2TokenIntrospectionClaimAccessor, OAuth2AuthenticatedPrincipal> authenticationConverter;
/**
* Creates a {@code OpaqueTokenAuthenticationProvider} with the provided parameters
* @param introspectionUri The introspection endpoint uri
@ -82,11 +85,11 @@ public class SpringOpaqueTokenIntrospector implements OpaqueTokenIntrospector {
RestTemplate restTemplate = new RestTemplate();
restTemplate.getInterceptors().add(new BasicAuthenticationInterceptor(clientId, clientSecret));
this.restOperations = restTemplate;
this.authenticationConverter = this.defaultAuthenticationConverter();
}
/**
* Creates a {@code OpaqueTokenAuthenticationProvider} with the provided parameters
*
* The given {@link RestOperations} should perform its own client authentication
* against the introspection endpoint.
* @param introspectionUri The introspection endpoint uri
@ -97,6 +100,7 @@ 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) {
@ -127,7 +131,8 @@ public class SpringOpaqueTokenIntrospector implements OpaqueTokenIntrospector {
}
ResponseEntity<Map<String, Object>> responseEntity = makeRequest(requestEntity);
Map<String, Object> claims = adaptToNimbusResponse(responseEntity);
return convertClaimsSet(claims);
convertClaimsSet(claims);
return this.authenticationConverter.convert(() -> claims);
}
/**
@ -178,7 +183,7 @@ public class SpringOpaqueTokenIntrospector implements OpaqueTokenIntrospector {
return claims;
}
private OAuth2AuthenticatedPrincipal convertClaimsSet(Map<String, Object> claims) {
private Map<String, Object> convertClaimsSet(Map<String, Object> claims) {
claims.computeIfPresent(OAuth2TokenIntrospectionClaimNames.AUD, (k, v) -> {
if (v instanceof String) {
return Collections.singletonList(v);
@ -211,18 +216,56 @@ public class SpringOpaqueTokenIntrospector implements OpaqueTokenIntrospector {
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 claims;
}
/**
* If {@link SpringOpaqueTokenIntrospector#authenticationConverter} is not explicitly
* set, this default converter will be used. transforms an
* {@link OAuth2TokenIntrospectionClaimAccessor} into an
* {@link OAuth2AuthenticatedPrincipal} by extracting claims, mapping scopes to
* authorities, and creating a principal.
* @return {@link Converter Converter&lt;OAuth2TokenIntrospectionClaimAccessor,
* OAuth2AuthenticatedPrincipal&gt;}
* @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 scopes;
}
return v;
});
return new OAuth2IntrospectionAuthenticatedPrincipal(claims, authorities);
return v;
});
return new OAuth2IntrospectionAuthenticatedPrincipal(claims, authorities);
};
}
/**
* <p>
* Sets the {@link Converter Converter&lt;OAuth2TokenIntrospectionClaimAccessor,
* OAuth2AuthenticatedPrincipal&gt;} 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;
}
}

View File

@ -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.
@ -40,6 +40,7 @@ import org.springframework.http.MediaType;
import org.springframework.http.RequestEntity;
import org.springframework.http.ResponseEntity;
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.client.RestOperations;
@ -293,6 +294,35 @@ public class SpringOpaqueTokenIntrospectorTests {
verify(requestEntityConverter).convert(tokenToIntrospect);
}
@Test
public void setAuthenticationConverterWhenConverterIsNullThenExceptionIsThrown() {
RestOperations restOperations = mock(RestOperations.class);
SpringOpaqueTokenIntrospector introspectionClient = new SpringOpaqueTokenIntrospector(INTROSPECTION_URL,
restOperations);
assertThatExceptionOfType(IllegalArgumentException.class)
.isThrownBy(() -> introspectionClient.setAuthenticationConverter(null));
}
@Test
public void setAuthenticationConverterWhenNonNullConverterGivenThenConverterUsed() {
RestOperations restOperations = mock(RestOperations.class);
Converter<String, RequestEntity<?>> requestEntityConverter = mock(Converter.class);
RequestEntity requestEntity = mock(RequestEntity.class);
Converter<OAuth2TokenIntrospectionClaimAccessor, OAuth2AuthenticatedPrincipal> authenticationConverter = mock(
Converter.class);
OAuth2AuthenticatedPrincipal oAuth2AuthenticatedPrincipal = mock(OAuth2AuthenticatedPrincipal.class);
String tokenToIntrospect = "some token";
given(requestEntityConverter.convert(tokenToIntrospect)).willReturn(requestEntity);
given(restOperations.exchange(requestEntity, STRING_OBJECT_MAP)).willReturn(response(ACTIVE_RESPONSE));
given(authenticationConverter.convert(any())).willReturn(oAuth2AuthenticatedPrincipal);
SpringOpaqueTokenIntrospector introspectionClient = new SpringOpaqueTokenIntrospector(INTROSPECTION_URL,
restOperations);
introspectionClient.setRequestEntityConverter(requestEntityConverter);
introspectionClient.setAuthenticationConverter(authenticationConverter);
introspectionClient.introspect(tokenToIntrospect);
verify(authenticationConverter).convert(any());
}
private static ResponseEntity<Map<String, Object>> response(String content) {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);