From 688b6ca7334e06a9d1946066f42bce48004bc7d5 Mon Sep 17 00:00:00 2001 From: Josh Cummings <3627351+jzheaux@users.noreply.github.com> Date: Tue, 10 Feb 2026 09:10:23 -0700 Subject: [PATCH] Add Documentation for ExpressionJwtGrantedAuthoritiesConverter Closes gh-18300 --- .../servlet/oauth2/resource-server/jwt.adoc | 18 ++++++++++ ...onJwtGrantedAuthoritiesConverterTests.java | 36 +++++++++++++++++++ ...sionJwtGrantedAuthoritiesConverterTests.kt | 28 +++++++++++++++ 3 files changed, 82 insertions(+) create mode 100644 docs/src/test/java/org/springframework/security/docs/servlet/oauth2/resourceserver/jwtgrantedauthoritiesspelexpression/ExpressionJwtGrantedAuthoritiesConverterTests.java create mode 100644 docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/oauth2/resourceserver/jwtgrantedauthoritiesspelexpression/ExpressionJwtGrantedAuthoritiesConverterTests.kt diff --git a/docs/modules/ROOT/pages/servlet/oauth2/resource-server/jwt.adoc b/docs/modules/ROOT/pages/servlet/oauth2/resource-server/jwt.adoc index 6bdc26ba80..af3667531d 100644 --- a/docs/modules/ROOT/pages/servlet/oauth2/resource-server/jwt.adoc +++ b/docs/modules/ROOT/pages/servlet/oauth2/resource-server/jwt.adoc @@ -961,7 +961,11 @@ By default, Spring Security will wire the `JwtAuthenticationProvider` with a def As part of configuring a `JwtAuthenticationConverter`, you can supply a subsidiary converter to go from `Jwt` to a `Collection` of granted authorities. +[[jwt-granted-authorities-custom-claim-name]] +==== Using a Custom Claim Name + Let's say that your authorization server communicates authorities in a custom claim called `authorities`. + In that case, you can configure the claim that <> should inspect, like so: .Authorities Claim Configuration @@ -1022,6 +1026,9 @@ Xml:: ---- ====== +[[jwt-granted-authorities-custom-scope-prefix]] +==== Using a Custom Scope Prefix + You can also configure the authority prefix to be different as well. Instead of prefixing each authority with `SCOPE_`, you can change it to `ROLE_` like so: @@ -1153,6 +1160,17 @@ class CustomAuthenticationConverterConfig { ---- ====== +[[jwt-granted-authorities-spel-expression]] +==== Using a SpEL Expression + +In circumstances where the location of scopes is nested or complex in some other way, you can use `ExpressionJwtGrantedAuthoritiesConverter` with a SpEL expression to extract the scopes. + +For example, if your JWT has a claim called `nested` and, inside of that, it has a claim called `scopes`, you can do: + +include-code::./ExpressionJwtGrantedAuthoritiesConverterTests[tag=spel-expression,indent=0] + +The SpEL expression result should be a `Collection`. + [[oauth2resourceserver-jwt-validation]] == Configuring Validation diff --git a/docs/src/test/java/org/springframework/security/docs/servlet/oauth2/resourceserver/jwtgrantedauthoritiesspelexpression/ExpressionJwtGrantedAuthoritiesConverterTests.java b/docs/src/test/java/org/springframework/security/docs/servlet/oauth2/resourceserver/jwtgrantedauthoritiesspelexpression/ExpressionJwtGrantedAuthoritiesConverterTests.java new file mode 100644 index 0000000000..1b8a65b3aa --- /dev/null +++ b/docs/src/test/java/org/springframework/security/docs/servlet/oauth2/resourceserver/jwtgrantedauthoritiesspelexpression/ExpressionJwtGrantedAuthoritiesConverterTests.java @@ -0,0 +1,36 @@ +package org.springframework.security.docs.servlet.oauth2.resourceserver.jwtgrantedauthoritiesspelexpression; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; + +import org.junit.jupiter.api.Test; + +import org.springframework.expression.Expression; +import org.springframework.expression.spel.standard.SpelExpressionParser; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.security.oauth2.jwt.TestJwts; +import org.springframework.security.oauth2.server.resource.authentication.ExpressionJwtGrantedAuthoritiesConverter; + +import static org.assertj.core.api.Assertions.assertThat; + +class ExpressionJwtGrantedAuthoritiesConverterTests { + + @Test + public void convertWhenTokenHasCustomClaimNameExpressionThenCustomClaimNameAttributeIsTranslatedToAuthorities() { + // @formatter:off + Jwt jwt = TestJwts.jwt() + .claim("nested", Collections.singletonMap("scopes", Arrays.asList("read", "write"))) + .build(); + // @formatter:on + // tag::spel-expression[] + SpelExpressionParser parser = new SpelExpressionParser(); + Expression expression = parser.parseExpression("[nested][scopes]"); + ExpressionJwtGrantedAuthoritiesConverter converter = new ExpressionJwtGrantedAuthoritiesConverter(expression); + Collection authorities = converter.convert(jwt); + // end::spel-expression[] + assertThat(authorities).extracting(GrantedAuthority::getAuthority) + .containsExactly("SCOPE_read", "SCOPE_write"); + } +} diff --git a/docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/oauth2/resourceserver/jwtgrantedauthoritiesspelexpression/ExpressionJwtGrantedAuthoritiesConverterTests.kt b/docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/oauth2/resourceserver/jwtgrantedauthoritiesspelexpression/ExpressionJwtGrantedAuthoritiesConverterTests.kt new file mode 100644 index 0000000000..399e53d697 --- /dev/null +++ b/docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/oauth2/resourceserver/jwtgrantedauthoritiesspelexpression/ExpressionJwtGrantedAuthoritiesConverterTests.kt @@ -0,0 +1,28 @@ +package org.springframework.security.kt.docs.servlet.oauth2.resourceserver.jwtgrantedauthoritiesspelexpression + +import org.assertj.core.api.Assertions.assertThat +import org.assertj.core.api.Assertions.tuple +import org.junit.jupiter.api.Test +import org.springframework.expression.spel.standard.SpelExpressionParser +import org.springframework.security.core.GrantedAuthority +import org.springframework.security.oauth2.jwt.TestJwts +import org.springframework.security.oauth2.server.resource.authentication.ExpressionJwtGrantedAuthoritiesConverter + +class ExpressionJwtGrantedAuthoritiesConverterTests { + @Test + fun convertWhenTokenHasCustomClaimNameExpressionThenCustomClaimNameAttributeIsTranslatedToAuthorities() { + // @formatter:off + val jwt = TestJwts.jwt() + .claim("nested", mapOf("scopes" to listOf("read", "write"))) + .build() + // @formatter:on + // tag::spel-expression[] + val parser = SpelExpressionParser() + val expression = parser.parseExpression("[nested][scopes]") + val converter = ExpressionJwtGrantedAuthoritiesConverter(expression) + val authorities = converter.convert(jwt) + // end::spel-expression[] + assertThat(authorities).extracting(GrantedAuthority::getAuthority) + .containsExactly(tuple("SCOPE_read"), tuple("SCOPE_write")) + } +}