From dd5edeb255bc8fde105bf180b83f4b1480431134 Mon Sep 17 00:00:00 2001 From: Josh Cummings Date: Tue, 28 May 2024 12:42:53 -0600 Subject: [PATCH] Preserve ArrayListFromString Type Closes gh-15165 --- .../SpringOpaqueTokenIntrospector.java | 18 ++++++++++++++++-- .../SpringReactiveOpaqueTokenIntrospector.java | 18 ++++++++++++++++-- .../SpringOpaqueTokenIntrospectorTests.java | 18 +++++++++++++++++- ...ngReactiveOpaqueTokenIntrospectorTests.java | 18 +++++++++++++++++- 4 files changed, 66 insertions(+), 6 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 c289ff294a..0fcc662b4f 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-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -183,7 +183,7 @@ public class SpringOpaqueTokenIntrospector implements OpaqueTokenIntrospector { return claims; } - private OAuth2TokenIntrospectionClaimAccessor convertClaimsSet(Map claims) { + private ArrayListFromStringClaimAccessor convertClaimsSet(Map claims) { Map converted = new LinkedHashMap<>(claims); converted.computeIfPresent(OAuth2TokenIntrospectionClaimNames.AUD, (k, v) -> { if (v instanceof String) { @@ -277,4 +277,18 @@ public class SpringOpaqueTokenIntrospector implements OpaqueTokenIntrospector { } + // gh-15165 + private interface ArrayListFromStringClaimAccessor extends OAuth2TokenIntrospectionClaimAccessor { + + @Override + default List getScopes() { + Object value = getClaims().get(OAuth2TokenIntrospectionClaimNames.SCOPE); + if (value instanceof ArrayListFromString list) { + return list; + } + return OAuth2TokenIntrospectionClaimAccessor.super.getScopes(); + } + + } + } diff --git a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/introspection/SpringReactiveOpaqueTokenIntrospector.java b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/introspection/SpringReactiveOpaqueTokenIntrospector.java index 98fae18dfd..1c12e6aa4c 100644 --- a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/introspection/SpringReactiveOpaqueTokenIntrospector.java +++ b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/introspection/SpringReactiveOpaqueTokenIntrospector.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -143,7 +143,7 @@ public class SpringReactiveOpaqueTokenIntrospector implements ReactiveOpaqueToke .switchIfEmpty(Mono.error(() -> new BadOpaqueTokenException("Provided token isn't active"))); } - private OAuth2TokenIntrospectionClaimAccessor convertClaimsSet(Map claims) { + private ArrayListFromStringClaimAccessor convertClaimsSet(Map claims) { Map converted = new LinkedHashMap<>(claims); converted.computeIfPresent(OAuth2TokenIntrospectionClaimNames.AUD, (k, v) -> { if (v instanceof String) { @@ -231,4 +231,18 @@ public class SpringReactiveOpaqueTokenIntrospector implements ReactiveOpaqueToke } + // gh-15165 + private interface ArrayListFromStringClaimAccessor extends OAuth2TokenIntrospectionClaimAccessor { + + @Override + default List getScopes() { + Object value = getClaims().get(OAuth2TokenIntrospectionClaimNames.SCOPE); + if (value instanceof ArrayListFromString list) { + return list; + } + return OAuth2TokenIntrospectionClaimAccessor.super.getScopes(); + } + + } + } 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 12c4c17d82..01555f01fd 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-2023 the original author or authors. + * Copyright 2002-2024 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.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.RequestEntity; import org.springframework.http.ResponseEntity; +import org.springframework.security.core.authority.AuthorityUtils; import org.springframework.security.oauth2.core.OAuth2AuthenticatedPrincipal; import org.springframework.security.oauth2.core.OAuth2TokenIntrospectionClaimAccessor; import org.springframework.security.oauth2.core.OAuth2TokenIntrospectionClaimNames; @@ -245,6 +246,21 @@ public class SpringOpaqueTokenIntrospectorTests { assertThat(scope).containsExactly("read", "write", "dolphin"); } + // gh-15165 + @Test + public void introspectWhenActiveThenMapsAuthorities() { + RestOperations restOperations = mock(RestOperations.class); + OpaqueTokenIntrospector introspectionClient = new SpringOpaqueTokenIntrospector(INTROSPECTION_URL, + restOperations); + given(restOperations.exchange(any(RequestEntity.class), eq(STRING_OBJECT_MAP))).willReturn(ACTIVE); + OAuth2AuthenticatedPrincipal principal = introspectionClient.introspect("token"); + assertThat(principal.getAuthorities()).isNotEmpty(); + Collection scope = principal.getAttribute("scope"); + assertThat(scope).containsExactly("read", "write", "dolphin"); + Collection authorities = AuthorityUtils.authorityListToSet(principal.getAuthorities()); + assertThat(authorities).containsExactly("SCOPE_read", "SCOPE_write", "SCOPE_dolphin"); + } + @Test public void constructorWhenIntrospectionUriIsNullThenIllegalArgumentException() { assertThatIllegalArgumentException() diff --git a/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/introspection/SpringReactiveOpaqueTokenIntrospectorTests.java b/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/introspection/SpringReactiveOpaqueTokenIntrospectorTests.java index 9b5f0386dd..ae0f01afd7 100644 --- a/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/introspection/SpringReactiveOpaqueTokenIntrospectorTests.java +++ b/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/introspection/SpringReactiveOpaqueTokenIntrospectorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -20,6 +20,7 @@ import java.io.IOException; import java.time.Instant; import java.util.Arrays; import java.util.Base64; +import java.util.Collection; import java.util.HashMap; import java.util.Map; import java.util.Optional; @@ -37,6 +38,7 @@ 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.core.authority.AuthorityUtils; import org.springframework.security.oauth2.core.OAuth2AuthenticatedPrincipal; import org.springframework.security.oauth2.core.OAuth2TokenIntrospectionClaimAccessor; import org.springframework.security.oauth2.core.OAuth2TokenIntrospectionClaimNames; @@ -197,6 +199,20 @@ public class SpringReactiveOpaqueTokenIntrospectorTests { // @formatter:on } + // gh-15165 + @Test + public void introspectWhenActiveThenMapsAuthorities() { + WebClient webClient = mockResponse(ACTIVE_RESPONSE); + SpringReactiveOpaqueTokenIntrospector introspectionClient = new SpringReactiveOpaqueTokenIntrospector( + INTROSPECTION_URL, webClient); + OAuth2AuthenticatedPrincipal principal = introspectionClient.introspect("token").block(); + assertThat(principal.getAuthorities()).isNotEmpty(); + Collection scope = principal.getAttribute("scope"); + assertThat(scope).containsExactly("read", "write", "dolphin"); + Collection authorities = AuthorityUtils.authorityListToSet(principal.getAuthorities()); + assertThat(authorities).containsExactly("SCOPE_read", "SCOPE_write", "SCOPE_dolphin"); + } + @Test public void setAuthenticationConverterWhenConverterIsNullThenExceptionIsThrown() { WebClient web = mock(WebClient.class);