Polish "Auto-configure a JwtAuthenticationConverter"

The JwtConverter bean is only supplied, if one of the following
properties is there:

* spring.security.oauth2.resourceserver.jwt.authority-prefix
* spring.security.oauth2.resourceserver.jwt.principal-claim-name
* spring.security.oauth2.resourceserver.jwt.authorities-claim-name

See gh-38105
This commit is contained in:
Moritz Halbritter
2024-01-10 14:45:40 +01:00
parent e9bce315ae
commit baf52214a2
5 changed files with 99 additions and 12 deletions

View File

@@ -26,6 +26,7 @@ import java.util.List;
import java.util.Set;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.condition.AnyNestedCondition;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
@@ -168,6 +169,7 @@ class ReactiveOAuth2ResourceServerJwkConfiguration {
@Configuration(proxyBeanMethods = false)
@ConditionalOnMissingBean(ReactiveJwtAuthenticationConverter.class)
@Conditional(JwtConverterPropertiesCondition.class)
static class JwtConverterConfiguration {
private final OAuth2ResourceServerProperties.Jwt properties;
@@ -212,4 +214,27 @@ class ReactiveOAuth2ResourceServerJwkConfiguration {
}
private static class JwtConverterPropertiesCondition extends AnyNestedCondition {
JwtConverterPropertiesCondition() {
super(ConfigurationPhase.REGISTER_BEAN);
}
@ConditionalOnProperty(prefix = "spring.security.oauth2.resourceserver.jwt", name = "authority-prefix")
static class OnAuthorityPrefix {
}
@ConditionalOnProperty(prefix = "spring.security.oauth2.resourceserver.jwt", name = "principal-claim-name")
static class OnPrincipalClaimName {
}
@ConditionalOnProperty(prefix = "spring.security.oauth2.resourceserver.jwt", name = "authorities-claim-name")
static class OnAuthoritiesClaimName {
}
}
}

View File

@@ -26,6 +26,7 @@ import java.util.List;
import java.util.Set;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.condition.AnyNestedCondition;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
@@ -179,6 +180,7 @@ class OAuth2ResourceServerJwtConfiguration {
@Configuration(proxyBeanMethods = false)
@ConditionalOnMissingBean(JwtAuthenticationConverter.class)
@Conditional(JwtConverterPropertiesCondition.class)
static class JwtConverterConfiguration {
private final OAuth2ResourceServerProperties.Jwt properties;
@@ -204,4 +206,27 @@ class OAuth2ResourceServerJwtConfiguration {
}
private static class JwtConverterPropertiesCondition extends AnyNestedCondition {
JwtConverterPropertiesCondition() {
super(ConfigurationPhase.REGISTER_BEAN);
}
@ConditionalOnProperty(prefix = "spring.security.oauth2.resourceserver.jwt", name = "authority-prefix")
static class OnAuthorityPrefix {
}
@ConditionalOnProperty(prefix = "spring.security.oauth2.resourceserver.jwt", name = "principal-claim-name")
static class OnPrincipalClaimName {
}
@ConditionalOnProperty(prefix = "spring.security.oauth2.resourceserver.jwt", name = "authorities-claim-name")
static class OnAuthoritiesClaimName {
}
}
}

View File

@@ -42,7 +42,6 @@ public final class JwtConverterCustomizationsArgumentsProvider implements Argume
String customDelimiter = "[~,#:]";
String customAuthoritiesClaim = "custom_authorities";
String customPrincipalClaim = "custom_principal";
String jwkSetUriProperty = "spring.security.oauth2.resourceserver.jwt.jwk-set-uri=https://jwk-set-uri.com";
String authorityPrefixProperty = "spring.security.oauth2.resourceserver.jwt.authority-prefix=" + customPrefix;
String authoritiesDelimiterProperty = "spring.security.oauth2.resourceserver.jwt.authorities-claim-delimiter="
@@ -51,19 +50,15 @@ public final class JwtConverterCustomizationsArgumentsProvider implements Argume
+ customAuthoritiesClaim;
String principalClaimProperty = "spring.security.oauth2.resourceserver.jwt.principal-claim-name="
+ customPrincipalClaim;
String[] noJwtConverterProps = { jwkSetUriProperty };
String[] customPrefixProps = { jwkSetUriProperty, authorityPrefixProperty };
String[] customDelimiterProps = { jwkSetUriProperty, authoritiesDelimiterProperty };
String[] customDelimiterProps = { jwkSetUriProperty, authorityPrefixProperty, authoritiesDelimiterProperty };
String[] customAuthoritiesClaimProps = { jwkSetUriProperty, authoritiesClaimProperty };
String[] customPrincipalClaimProps = { jwkSetUriProperty, principalClaimProperty };
String[] allJwtConverterProps = { jwkSetUriProperty, authorityPrefixProperty, authoritiesDelimiterProperty,
authoritiesClaimProperty, principalClaimProperty };
String[] jwtScopes = { "custom_scope0", "custom_scope1" };
String subjectValue = UUID.randomUUID().toString();
String customPrincipalValue = UUID.randomUUID().toString();
Jwt.Builder jwtBuilder = Jwt.withTokenValue("token")
.header("alg", "none")
.expiresAt(Instant.MAX)
@@ -73,7 +68,6 @@ public final class JwtConverterCustomizationsArgumentsProvider implements Argume
.notBefore(Instant.MIN)
.subject(subjectValue)
.claim(customPrincipalClaim, customPrincipalValue);
Jwt noAuthoritiesCustomizationsJwt = jwtBuilder.claim("scp", jwtScopes[0] + " " + jwtScopes[1]).build();
Jwt customAuthoritiesDelimiterJwt = jwtBuilder.claim("scp", jwtScopes[0] + "~" + jwtScopes[1]).build();
Jwt customAuthoritiesClaimJwt = jwtBuilder.claim("scp", null)
@@ -82,17 +76,13 @@ public final class JwtConverterCustomizationsArgumentsProvider implements Argume
Jwt customAuthoritiesClaimAndDelimiterJwt = jwtBuilder.claim("scp", null)
.claim(customAuthoritiesClaim, jwtScopes[0] + "~" + jwtScopes[1])
.build();
String[] customPrefixAuthorities = { customPrefix + jwtScopes[0], customPrefix + jwtScopes[1] };
String[] defaultPrefixAuthorities = { "SCOPE_" + jwtScopes[0], "SCOPE_" + jwtScopes[1] };
return Stream.of(
Arguments.of(Named.named("No JWT converter customizations", noJwtConverterProps),
noAuthoritiesCustomizationsJwt, subjectValue, defaultPrefixAuthorities),
Arguments.of(Named.named("Custom prefix for GrantedAuthority", customPrefixProps),
noAuthoritiesCustomizationsJwt, subjectValue, customPrefixAuthorities),
Arguments.of(Named.named("Custom delimiter for JWT scopes", customDelimiterProps),
customAuthoritiesDelimiterJwt, subjectValue, defaultPrefixAuthorities),
customAuthoritiesDelimiterJwt, subjectValue, customPrefixAuthorities),
Arguments.of(Named.named("Custom JWT authority claim name", customAuthoritiesClaimProps),
customAuthoritiesClaimJwt, subjectValue, defaultPrefixAuthorities),
Arguments.of(Named.named("Custom JWT principal claim name", customPrincipalClaimProps),

View File

@@ -633,6 +633,30 @@ class ReactiveOAuth2ResourceServerAutoConfigurationTests {
});
}
@Test
void shouldNotConfigureJwtConverterIfNoPropertiesAreSet() {
this.contextRunner
.run((context) -> assertThat(context).doesNotHaveBean(ReactiveJwtAuthenticationConverter.class));
}
@Test
void shouldConfigureJwtConverterIfPrincipalClaimNameIsSet() {
this.contextRunner.withPropertyValues("spring.security.oauth2.resourceserver.jwt.principal-claim-name=dummy")
.run((context) -> assertThat(context).hasSingleBean(ReactiveJwtAuthenticationConverter.class));
}
@Test
void shouldConfigureJwtConverterIfAuthorityPrefixIsSet() {
this.contextRunner.withPropertyValues("spring.security.oauth2.resourceserver.jwt.authority-prefix=dummy")
.run((context) -> assertThat(context).hasSingleBean(ReactiveJwtAuthenticationConverter.class));
}
@Test
void shouldConfigureJwtConverterIfAuthorityClaimsNameIsSet() {
this.contextRunner.withPropertyValues("spring.security.oauth2.resourceserver.jwt.authorities-claim-name=dummy")
.run((context) -> assertThat(context).hasSingleBean(ReactiveJwtAuthenticationConverter.class));
}
@ParameterizedTest(name = "{0}")
@ArgumentsSource(JwtConverterCustomizationsArgumentsProvider.class)
void autoConfigurationShouldConfigureResourceServerWithJwtConverterCustomizations(String[] properties, Jwt jwt,

View File

@@ -662,6 +662,29 @@ class OAuth2ResourceServerAutoConfigurationTests {
});
}
@Test
void shouldNotConfigureJwtConverterIfNoPropertiesAreSet() {
this.contextRunner.run((context) -> assertThat(context).doesNotHaveBean(JwtAuthenticationConverter.class));
}
@Test
void shouldConfigureJwtConverterIfPrincipalClaimNameIsSet() {
this.contextRunner.withPropertyValues("spring.security.oauth2.resourceserver.jwt.principal-claim-name=dummy")
.run((context) -> assertThat(context).hasSingleBean(JwtAuthenticationConverter.class));
}
@Test
void shouldConfigureJwtConverterIfAuthorityPrefixIsSet() {
this.contextRunner.withPropertyValues("spring.security.oauth2.resourceserver.jwt.authority-prefix=dummy")
.run((context) -> assertThat(context).hasSingleBean(JwtAuthenticationConverter.class));
}
@Test
void shouldConfigureJwtConverterIfAuthorityClaimsNameIsSet() {
this.contextRunner.withPropertyValues("spring.security.oauth2.resourceserver.jwt.authorities-claim-name=dummy")
.run((context) -> assertThat(context).hasSingleBean(JwtAuthenticationConverter.class));
}
@Test
void jwtAuthenticationConverterByJwtConfigIsConditionalOnMissingBean() {
String propertiesPrincipalClaim = "principal_from_properties";