From 8279b229406f2d4b638e51b6722c00497f2f1510 Mon Sep 17 00:00:00 2001 From: ahmd-nabil Date: Sat, 2 Dec 2023 01:26:40 +0200 Subject: [PATCH] Add Introspection Authentication Converter Closes #14198 Signed-off-by: ahmd-nabil --- .../SpringOpaqueTokenIntrospector.java | 73 +++++++++++++++---- .../SpringOpaqueTokenIntrospectorTests.java | 32 +++++++- 2 files changed, 89 insertions(+), 16 deletions(-) diff --git a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/introspection/SpringOpaqueTokenIntrospector.java b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/introspection/SpringOpaqueTokenIntrospector.java index 2500c3f17e..20ae00337d 100644 --- a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/introspection/SpringOpaqueTokenIntrospector.java +++ b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/introspection/SpringOpaqueTokenIntrospector.java @@ -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> requestEntityConverter; + private Converter 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> defaultRequestEntityConverter(URI introspectionUri) { @@ -127,7 +131,8 @@ public class SpringOpaqueTokenIntrospector implements OpaqueTokenIntrospector { } ResponseEntity> responseEntity = makeRequest(requestEntity); Map 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 claims) { + private Map convertClaimsSet(Map 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 authorities = new ArrayList<>(); - claims.computeIfPresent(OAuth2TokenIntrospectionClaimNames.SCOPE, (k, v) -> { - if (v instanceof String) { - Collection 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 defaultAuthenticationConverter() { + return (accessor) -> { + Map claims = accessor.getClaims(); + Collection authorities = new ArrayList<>(); + + claims.computeIfPresent(OAuth2TokenIntrospectionClaimNames.SCOPE, (k, v) -> { + if (v instanceof String) { + Collection 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); + }; + } + + /** + *

+ * Sets the {@link Converter Converter<OAuth2TokenIntrospectionClaimAccessor, + * OAuth2AuthenticatedPrincipal>} to use. Defaults to + * {@link SpringOpaqueTokenIntrospector#defaultAuthenticationConverter()}. + *

+ *

+ * Use if you need a custom mapping of OAuth 2.0 token claims to the authenticated + * principal. + *

+ * @param authenticationConverter the converter + * @since 6.3 + */ + public void setAuthenticationConverter( + Converter authenticationConverter) { + Assert.notNull(authenticationConverter, "converter cannot be null"); + this.authenticationConverter = authenticationConverter; } } diff --git a/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/introspection/SpringOpaqueTokenIntrospectorTests.java b/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/introspection/SpringOpaqueTokenIntrospectorTests.java index f46dbd1edb..12c4c17d82 100644 --- a/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/introspection/SpringOpaqueTokenIntrospectorTests.java +++ b/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/introspection/SpringOpaqueTokenIntrospectorTests.java @@ -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> requestEntityConverter = mock(Converter.class); + RequestEntity requestEntity = mock(RequestEntity.class); + Converter 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> response(String content) { HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON);