I have a spring application that exposes some webflux endpoints, I use a jwt token to authorize the post calls but we I also need the information given by the userinfo endpoint. I have a SecurityWebFilterChain bean right now and we are using an oauth2ResourceServer configuration then calling the userinfoendpoint for further checks. What is the best way to validate a jwt token then get the userinfo enpoint information for further validations?
ps: the authorization server is a third part one.
Security configuration without the external call for the user-info
@Bean
public SecurityWebFilterChain filterChain(ServerHttpSecurity http) {
http
.cors()
.and()
.httpBasic().disable()
.formLogin().disable()
.csrf().disable()
.logout().disable()
.oauth2Client()
.and()
.authorizeExchange()
.pathMatchers(HttpMethod.POST).authenticated()
.anyExchange().permitAll()
.and().oauth2ResourceServer().jwt()
;
return http.build();
}
CodePudding user response:
The UserInfo Endpoint is part of OpenID Connect 1.0, and returns user information for an access token. It is not automatically called from a resource server (http.oauth2ResourceServer()
) by Spring Security.
Based on your security configuration, it looks like you're wanting to use both OAuth2 Client (http.oauth2Client()
) and OAuth2 Resource Server (http.oauth2ResourceServer()
) in the same application. OAuth2 Client isn't designed for this use case (calling UserInfo from a resource server), and therefore would require customization to be adapted for such a case. Instead, you can simply use a RestTemplate
or WebClient
to call the UserInfo endpoint yourself.
You can do this in a custom Converter<Jwt, Collection<GrantedAuthority>>
(or in this case the reactive version) like so:
@Configuration
@EnableWebFluxSecurity
public class SecurityConfiguration {
@Bean
public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
// @formatter:off
http
.authorizeExchange((authorize) -> authorize
.anyExchange().authenticated()
)
.oauth2ResourceServer((oauth2) -> oauth2
.jwt((jwt) -> jwt
.jwtAuthenticationConverter(jwtAuthenticationConverter())
)
);
// @formatter:on
return http.build();
}
private Converter<Jwt, Mono<AbstractAuthenticationToken>> jwtAuthenticationConverter() {
ReactiveJwtAuthenticationConverter converter = new ReactiveJwtAuthenticationConverter();
converter.setJwtGrantedAuthoritiesConverter(jwtGrantedAuthoritiesConverter());
return converter;
}
private Converter<Jwt, Flux<GrantedAuthority>> jwtGrantedAuthoritiesConverter() {
JwtGrantedAuthoritiesConverter delegate = new JwtGrantedAuthoritiesConverter();
return (jwt) -> getUserInfo(jwt.getTokenValue())
.flatMapIterable((userInfo) -> {
Collection<GrantedAuthority> authorities = delegate.convert(jwt);
// TODO: Add authority from userInfo...
return authorities;
});
}
private Mono<Map<String, String>> getUserInfo(String accessToken) {
// TODO: Call user info and extract one or more claims from response
return Mono.just(new HashMap<>());
}
}
CodePudding user response:
It is way more efficient to decode the data from a JWT than querying an external endpoint.
As a consequence, the best option is to configure the authorization-server (even if it is 3rd party) to enrich the JWTs (access and ID tokens) with the data you need for authorization. Most OIDC authorization-servers support it, just refer to its documentation (Keycloak, Auth0, Cognito, ...).
Once all the claims you need are in access-tokens, you can read it on resource-server from the JwtAuthenticationToken
instance (or OAuth2AuthenticationToken
for client app with oauth2login
) in the security-context. This allows to write stuff like:
@PostMapping("/answers/{subject}")
@PreAuthorize("#userSubject == #auth.token.claims['sub']")
public ResponseEntity<String> addAnswer(@PathVariable("subject") String userSubject, @RequestBody @Valid AnswerDto, JwtAuthenticationToken auth) {
...
}