Add Introspection Authentication Converter
Closes #14198 Signed-off-by: ahmd-nabil <ahm3dnabil99@gmail.com>
This commit is contained in:
parent
1ce1ff92de
commit
8279b22940
|
@ -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");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with 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.GrantedAuthority;
|
||||||
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||||
import org.springframework.security.oauth2.core.OAuth2AuthenticatedPrincipal;
|
import org.springframework.security.oauth2.core.OAuth2AuthenticatedPrincipal;
|
||||||
|
import org.springframework.security.oauth2.core.OAuth2TokenIntrospectionClaimAccessor;
|
||||||
import org.springframework.security.oauth2.core.OAuth2TokenIntrospectionClaimNames;
|
import org.springframework.security.oauth2.core.OAuth2TokenIntrospectionClaimNames;
|
||||||
import org.springframework.util.Assert;
|
import org.springframework.util.Assert;
|
||||||
import org.springframework.util.LinkedMultiValueMap;
|
import org.springframework.util.LinkedMultiValueMap;
|
||||||
|
@ -68,6 +69,8 @@ public class SpringOpaqueTokenIntrospector implements OpaqueTokenIntrospector {
|
||||||
|
|
||||||
private Converter<String, RequestEntity<?>> requestEntityConverter;
|
private Converter<String, RequestEntity<?>> requestEntityConverter;
|
||||||
|
|
||||||
|
private Converter<OAuth2TokenIntrospectionClaimAccessor, OAuth2AuthenticatedPrincipal> authenticationConverter;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a {@code OpaqueTokenAuthenticationProvider} with the provided parameters
|
* Creates a {@code OpaqueTokenAuthenticationProvider} with the provided parameters
|
||||||
* @param introspectionUri The introspection endpoint uri
|
* @param introspectionUri The introspection endpoint uri
|
||||||
|
@ -82,11 +85,11 @@ public class SpringOpaqueTokenIntrospector implements OpaqueTokenIntrospector {
|
||||||
RestTemplate restTemplate = new RestTemplate();
|
RestTemplate restTemplate = new RestTemplate();
|
||||||
restTemplate.getInterceptors().add(new BasicAuthenticationInterceptor(clientId, clientSecret));
|
restTemplate.getInterceptors().add(new BasicAuthenticationInterceptor(clientId, clientSecret));
|
||||||
this.restOperations = restTemplate;
|
this.restOperations = restTemplate;
|
||||||
|
this.authenticationConverter = this.defaultAuthenticationConverter();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a {@code OpaqueTokenAuthenticationProvider} with the provided parameters
|
* Creates a {@code OpaqueTokenAuthenticationProvider} with the provided parameters
|
||||||
*
|
|
||||||
* The given {@link RestOperations} should perform its own client authentication
|
* The given {@link RestOperations} should perform its own client authentication
|
||||||
* against the introspection endpoint.
|
* against the introspection endpoint.
|
||||||
* @param introspectionUri The introspection endpoint uri
|
* @param introspectionUri The introspection endpoint uri
|
||||||
|
@ -97,6 +100,7 @@ public class SpringOpaqueTokenIntrospector implements OpaqueTokenIntrospector {
|
||||||
Assert.notNull(restOperations, "restOperations cannot be null");
|
Assert.notNull(restOperations, "restOperations cannot be null");
|
||||||
this.requestEntityConverter = this.defaultRequestEntityConverter(URI.create(introspectionUri));
|
this.requestEntityConverter = this.defaultRequestEntityConverter(URI.create(introspectionUri));
|
||||||
this.restOperations = restOperations;
|
this.restOperations = restOperations;
|
||||||
|
this.authenticationConverter = this.defaultAuthenticationConverter();
|
||||||
}
|
}
|
||||||
|
|
||||||
private Converter<String, RequestEntity<?>> defaultRequestEntityConverter(URI introspectionUri) {
|
private Converter<String, RequestEntity<?>> defaultRequestEntityConverter(URI introspectionUri) {
|
||||||
|
@ -127,7 +131,8 @@ public class SpringOpaqueTokenIntrospector implements OpaqueTokenIntrospector {
|
||||||
}
|
}
|
||||||
ResponseEntity<Map<String, Object>> responseEntity = makeRequest(requestEntity);
|
ResponseEntity<Map<String, Object>> responseEntity = makeRequest(requestEntity);
|
||||||
Map<String, Object> claims = adaptToNimbusResponse(responseEntity);
|
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;
|
return claims;
|
||||||
}
|
}
|
||||||
|
|
||||||
private OAuth2AuthenticatedPrincipal convertClaimsSet(Map<String, Object> claims) {
|
private Map<String, Object> convertClaimsSet(Map<String, Object> claims) {
|
||||||
claims.computeIfPresent(OAuth2TokenIntrospectionClaimNames.AUD, (k, v) -> {
|
claims.computeIfPresent(OAuth2TokenIntrospectionClaimNames.AUD, (k, v) -> {
|
||||||
if (v instanceof String) {
|
if (v instanceof String) {
|
||||||
return Collections.singletonList(v);
|
return Collections.singletonList(v);
|
||||||
|
@ -211,7 +216,24 @@ public class SpringOpaqueTokenIntrospector implements OpaqueTokenIntrospector {
|
||||||
claims.computeIfPresent(OAuth2TokenIntrospectionClaimNames.ISS, (k, v) -> v.toString());
|
claims.computeIfPresent(OAuth2TokenIntrospectionClaimNames.ISS, (k, v) -> v.toString());
|
||||||
claims.computeIfPresent(OAuth2TokenIntrospectionClaimNames.NBF,
|
claims.computeIfPresent(OAuth2TokenIntrospectionClaimNames.NBF,
|
||||||
(k, v) -> Instant.ofEpochSecond(((Number) v).longValue()));
|
(k, v) -> Instant.ofEpochSecond(((Number) v).longValue()));
|
||||||
|
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<OAuth2TokenIntrospectionClaimAccessor,
|
||||||
|
* OAuth2AuthenticatedPrincipal>}
|
||||||
|
* @since 6.3
|
||||||
|
*/
|
||||||
|
private Converter<OAuth2TokenIntrospectionClaimAccessor, OAuth2AuthenticatedPrincipal> defaultAuthenticationConverter() {
|
||||||
|
return (accessor) -> {
|
||||||
|
Map<String, Object> claims = accessor.getClaims();
|
||||||
Collection<GrantedAuthority> authorities = new ArrayList<>();
|
Collection<GrantedAuthority> authorities = new ArrayList<>();
|
||||||
|
|
||||||
claims.computeIfPresent(OAuth2TokenIntrospectionClaimNames.SCOPE, (k, v) -> {
|
claims.computeIfPresent(OAuth2TokenIntrospectionClaimNames.SCOPE, (k, v) -> {
|
||||||
if (v instanceof String) {
|
if (v instanceof String) {
|
||||||
Collection<String> scopes = Arrays.asList(((String) v).split(" "));
|
Collection<String> scopes = Arrays.asList(((String) v).split(" "));
|
||||||
|
@ -222,7 +244,28 @@ public class SpringOpaqueTokenIntrospector implements OpaqueTokenIntrospector {
|
||||||
}
|
}
|
||||||
return v;
|
return v;
|
||||||
});
|
});
|
||||||
|
|
||||||
return new OAuth2IntrospectionAuthenticatedPrincipal(claims, authorities);
|
return new OAuth2IntrospectionAuthenticatedPrincipal(claims, 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with 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.RequestEntity;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.security.oauth2.core.OAuth2AuthenticatedPrincipal;
|
import org.springframework.security.oauth2.core.OAuth2AuthenticatedPrincipal;
|
||||||
|
import org.springframework.security.oauth2.core.OAuth2TokenIntrospectionClaimAccessor;
|
||||||
import org.springframework.security.oauth2.core.OAuth2TokenIntrospectionClaimNames;
|
import org.springframework.security.oauth2.core.OAuth2TokenIntrospectionClaimNames;
|
||||||
import org.springframework.web.client.RestOperations;
|
import org.springframework.web.client.RestOperations;
|
||||||
|
|
||||||
|
@ -293,6 +294,35 @@ public class SpringOpaqueTokenIntrospectorTests {
|
||||||
verify(requestEntityConverter).convert(tokenToIntrospect);
|
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) {
|
private static ResponseEntity<Map<String, Object>> response(String content) {
|
||||||
HttpHeaders headers = new HttpHeaders();
|
HttpHeaders headers = new HttpHeaders();
|
||||||
headers.setContentType(MediaType.APPLICATION_JSON);
|
headers.setContentType(MediaType.APPLICATION_JSON);
|
||||||
|
|
Loading…
Reference in New Issue