I use springboot3 with spring-boot-starter-oauth2-resource-server. I can receive Jwt object using @AuthenticatedPrincipal in controller and I have access to jwt data in method access-control annotations like @PreAuthorize:
@PreAuthorize("principal.claims['id']=='100'")
@GetMapping
public Jwt test(@AuthenticationPrincipal Jwt principal) {
return principal;
}
Is there any way to deal with custom principal object in @PreAuthorize and @AuthenticationPrincipal like here:
@PreAuthorize("principal.id==100")
@GetMapping
public MyPrincipal test(@AuthenticationPrincipal MyPrincipal principal) {
return principal;
}
MyPrincipal:
public class JwtPrincipal implements MyPrincipal {
private final Integer id;
public JwtPrincipal(Jwt jwt) {
this.id = Integer.parseInt(jwt.getClaim("id"));
}
@Override
public Integer getId() {
return id;
}}
I would like to add such ability if no easy way to implement it in current Spring Security implementation. From developer side it would be enough to add MyPrincipal, JwtPrincipal, and a bean:
@Bean
public JwtPrincipalConverter jwtPrincipalConverter() {
return JwtPrincipal::new;
}
P.S. Correct answer from spring security author can be found here
You can't change the type of the JwtAuthenticationToken
principal, and you can't change the default authentication converter return type. What you can change is this authentication converter.
As a reminder, the contract for the authentication converter in a resource server with a JWT decoder is Converter<Jwt, AbstractAuthenticationToken>
. So, you can use an authentication converter returning anything that extends AbstractAuthenticationToken
.
In your case, extend this AbstractAuthenticationToken
(AbstractOAuth2TokenAuthenticationToken
is probably the most adapted in the type tree), set your JwtPrincipal
as principal, and write an authentication converter building the two.
You may then either:
@Component
or define a @Bean
in your security conf). Boot should pick it and replace the default which is JwtAuthenticationConverter
http.oauth2ResourceServer(oauth2 ->
oauth2.jwt(resourceServer ->
resourceServer.jwtAuthenticationConverter(jwt ->
new YourAuthenticationWithJwtPrincipal(jwt) )));
Note that you can also (and probably should) define the authentication converter as a @Bean
or @Component
(like I do below) and inject it when defining the security filter chain (rather than inlining its definition as I did above).
spring-boot-starter-oauth2-resource-server
public interface MyPrincipal extends Principal, OAuth2Token {
Integer getId();
}
@Data
@RequiredArgsConstructor
public class JwtPrincipal implements MyPrincipal {
private final Jwt jwt;
private final String principalClaimName;
@Override
public Integer getId() {
return Integer.parseInt(jwt.getClaim("id"));
}
@Override
public String getTokenValue() {
return jwt.getTokenValue();
}
@Override
public String getName() {
return jwt.getClaimAsString(principalClaimName);
}
}
@EqualsAndHashCode(callSuper = true)
public class YourAuthentcationWithJwtPrincipal
extends AbstractOAuth2TokenAuthenticationToken<JwtPrincipal>
implements OAuth2AuthenticatedPrincipal {
private static final long serialVersionUID = -171062889286337491L;
public YourAuthentcationWithJwtPrincipal(Jwt jwt, String principalClaimName,
Collection<GrantedAuthority> authorities) {
super(new JwtPrincipal(jwt, principalClaimName), authorities);
}
@Override
public Map<String, Object> getAttributes() {
return getTokenAttributes();
}
@Override
public Map<String, Object> getTokenAttributes() {
return getToken().getJwt().getClaims();
}
@Override
public String getName() {
return getToken().getName();
}
}
@Component
public class YourAuthenticationConverter implements Converter<Jwt, AbstractAuthenticationToken> {
private final JwtGrantedAuthoritiesConverter authoritiesConverter;
private final String principalClaimName;
public YourAuthenticationConverter(
@Value("${spring.security.oauth2.resourceserver.jwt.authorities-claim-name:'scope'}") String authoritiesClaimName,
@Value("${spring.security.oauth2.resourceserver.jwt.authorities-claim-delimiter:' '}") String authoritiesClaimDelimiter,
@Value("${spring.security.oauth2.resourceserver.jwt.authority-prefix:'SCOPE_'}") String authorityPrefix,
@Value("${spring.security.oauth2.resourceserver.jwt.principal-claim-name:'sub'}") String principalClaimName) {
this.authoritiesConverter = new JwtGrantedAuthoritiesConverter();
this.authoritiesConverter.setAuthoritiesClaimName(authoritiesClaimName);
this.authoritiesConverter.setAuthoritiesClaimDelimiter(authoritiesClaimDelimiter);
this.authoritiesConverter.setAuthorityPrefix(authorityPrefix);
this.principalClaimName = principalClaimName;
}
@Override
public AbstractAuthenticationToken convert(@NonNull Jwt jwt) {
return new YourAuthentcationWithJwtPrincipal(
jwt,
principalClaimName,
authoritiesConverter.convert(jwt));
}
}
spring:
security:
oauth2:
resourceserver:
jwt:
issuer-uri: ${issuer}
authorities-claim-name: groups
authorities-claim-delimiter: ","
authority-prefix: "ROLE_"
principal-claim-name: "preferred_username"
With this setup, a SecurityFilterChain
bean is still auto-configured by spring-boot-starter-oauth2-resource-server
, the "official" Boot starter, which scans for a Converter<Jwt, AbstractAuthenticationToken>
.
Because it implements this interface and is decorated with @Component
, YourAuthenticationConverter
replaces the default authentication converter.
spring-addons-starter-oidc
This starter I wrote primarily aims at easing security configuration, but it also provides a few alternate implementations. These are completely optional and not used by default, but can be of great help in your case:
OAuthentication
, an alternative to resource servers' Authentication
implementations. You have a hand on what is used as principal.OpenidToken
, to be used as OAuthentication
principal, or base class of what you want to be the principal. Its API is already super rich (it exposes all JWT and OpenID standard claims with the right types).@Bean
JwtAbstractAuthenticationTokenConverter authenticationConverter(
SpringAddonsOidcProperties addonsProperties,
Converter<Map<String, Object>, Collection<? extends GrantedAuthority>> authoritiesConverter) {
return jwt -> {
final var token = new JwtPrincipal(jwt.getClaims(),
addonsProperties.getOps().get(0).getUsernameClaim(), jwt.getTokenValue());
return new OAuthentication<JwtPrincipal>(token, authoritiesConverter.convert(token));
};
}
public interface MyPrincipal extends Principal {
Integer getId();
}
public static class JwtPrincipal extends OpenidToken implements MyPrincipal {
private static final long serialVersionUID = -7265502984357862154L;
public JwtPrincipal(Map<String, Object> claims, String usernameClaim, String tokenValue) {
super(claims, usernameClaim, tokenValue);
}
@Override
public Integer getId() {
return Integer.parseInt(getClaim("id"));
}
}
com:
c4-soft:
springaddons:
oidc:
ops:
- iss: ${issuer}
username-claim: preferred_username
authorities:
# this is a JsonPath which, as a difference from the official starter,
# allows to parse nested claims like Keycloak's realm roles or Auth0 private claims
- path: $.realm_access.roles