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");
|
||||
* 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<OAuth2TokenIntrospectionClaimAccessor,
|
||||
* 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 scopes;
|
||||
}
|
||||
return v;
|
||||
});
|
||||
return new OAuth2IntrospectionAuthenticatedPrincipal(claims, authorities);
|
||||
return v;
|
||||
});
|
||||
|
||||
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");
|
||||
* 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);
|
||||
|
|
Loading…
Reference in New Issue