Reputation: 447
We have a project where we have:
+------------------------------------+
| | 1. gets the token
| Authorization Server (Auth) | <------------------+
| - spring-security-oauth2:2.0.14 | |
| | |
+------------------------------------+ +
user
+
+------------------------------------+ |
| | 2. uses the token |
| Resource Server (RS) | to access resourcs|
| - spring-security-oauth2:5.1.0 | <------------------+
| |
+------------------------------------+
We have been working in an environment with no Webflux and everything was working as expected. Worth mentioning that this JWT has the following claims: exp, user_name, authorities, jti, client_id, scope.
All the resources have an extra variable:
@GetMapping("/{id}/car")
public SomeDto someResourceMethod(@PathVariable("id") CarId carId, Principal principal)
In "Spring Web" (not using Reactor): The Principal
is instantiated to OAuth2Authentication
and there we have the operation getName
on it.
In "Spring Webflux" (using Reactor): The Principal
is instantiated to JwtAuthenticationToken
and we have now the getSubject
operation, but it's null since the claim sub
is null.
How do you think we should approach this? Create a new JwtDecoder
? We are using Spring 5.1 and the decoder is NimbusReactiveJwtDecoder
.
Upvotes: 2
Views: 3298
Reputation: 7762
At this point, Reactive Resource Server supports JWT claims according to RFC 7519, which is why you see the behavior change.
Yes, you can create your own decoder, which is probably the least invasive way:
public class CustomDecoder implements ReactiveJwtDecoder {
private final ReactiveJwtDecoder nimbus;
// ...
public Mono<Jwt> decode(String token) {
return this.nimbus.decode(token)
.map(this::mapJwt);
}
private Jwt mapJwt(Jwt jwt) {
Map<String, Object> claims = jwt.getClaims();
// ... map claims accordingly
return new Jwt(...);
}
}
You may also be able to customize the authentication manager, which is introduced in RC2:
public class CustomReactiveAuthenticationManager
implements ReactiveAuthenticationManager {
private final ReactiveAuthenticationManager delegate;
// ...
public Mono<Authentication> authenticate(Authentication authentication) {
return this.delegate.authenticate(authentication)
.map(this::mapAuthentication);
}
private Authentication mapAuthentication(Authentication authentication) {
// ... create a custom authentication where getName does what you need
}
}
OR, if you are able to do some refactoring of your method signatures, then another option would be to use @AuthenticatedPrincipal
:
@GetMapping("/{id}/car")
public SomeDto someResourceMethod(
@PathVariable("id") CarId carId,
@AuthenticatedPrincipal Jwt jwt) {
String name = jwt.getClaims().get("user_name");
// ...
}
or perhaps more succinctly
@Target({ ElementType.PARAMETER })
@Retention(RetentionPolicy.RUNTIME)
@AuthenticationPrincipal(expression = "getClaims().get('user_name')")
public @interface CurrentUsername {}
@GetMapping("/{id}/car")
public SomeDto someResourceMethod(
@PathVariable("id") CarId carId,
@CurrentUsername String name) {
// ...
}
You might also think about logging an enhancement on Spring Security to consider making the user attribute name configurable.
EDIT: I updated the EL expression since by default @AuthenticatedPrincipal
calls authentication.getPrincipal()
first.
UPDATE: In Spring Security 5.4+, the claim that holds the principal name is configurable, like so:
@Bean
JwtAuthenticationConverter jwtAuthenticationConverter() {
JwtAuthenticationConverter authenticationConverter =
new JwtAuthenticationConverter();
authenticationConverter.setPrincipalClaimName("user_name");
return authenticationConverter;
}
which then allows the OP to use Principal#getName
as with the other principal types.
Upvotes: 3