Oauth2 jwt token enriched with userinfo enpoint - java

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();
}

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<>());
}
}

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) {
...
}

Related

Create principal from JWT and set it to SecurityContext

In one system, I generated the JWT token as follows:
List securityGroups = Arrays.asList("group1");
Map<String, Object> claims = Map.of("username", "user1", "securityGroups", securityGroups);
String token = Jwts.builder()
.setClaims(claims).setSubject("user1").setAudience("web")
.setIssuedAt(<date now>).setExpiration(<some expiration>)
.signWith(SignatureAlgorithm.HS512, "mysecret").compact();
In another application, I want to decrypt this token and set in the SecurityContext. First, I have security config as follows:
#Configuration
public class SecurityConf extends WebSecurityConfigurerAdapter {
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disabled().authorizeRequests()
.antMatchers("/principal").permitAll()
.anyRequests().authenticated();
}
}
With this, when user requests for an endpoint, say /books, the browser will receive 403 error.
Then I implement the /principal to use this token as an authenticated user in spring.
#RequestMapping("/principal")
public class PrincipalController {
#PostMapping
public void setPrincipal(#RequestBody String token) {
// i'm able to decrypt the token here
// use token to create principal
Authentication authentication = ....
SecurityContextHolder.getContext().setAuthentication(...)
}
}
I'm thinking once I set this in SecurityContext, for succeeding request where I attach the token in the Authorization header, I won't be getting anymore 403 or 401 error since user is authenticated and Spring knows that the token corresponds to the principal in the context. But this part I am not sure how to do it. Please advise.
You can achieve this with spring-security built-it JWT support for a server secured with JWT.
First, for a spring-boot application you'll need a dependency:
for Gradle:
implementation 'org.springframework.boot:spring-boot-starter-oauth2-resource-server:2.7.4'
for Maven:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
<version>2.7.4</version>
</dependency>
Then in any configuration class create beans of JwtDecoder type and a converter to convert some JWT claim to a collection of GrantedAuthority.
In your case it can be done like this:
#Bean
public JwtDecoder jwtDecoder() {
final SecretKey key = new SecretKeySpec("mysecret".getBytes(), JwsAlgorithms.HS512);
final NimbusJwtDecoder decoder = NimbusJwtDecoder.withSecretKey(key).build();
return decoder;
}
#Bean
public JwtAuthenticationConverter jwtAuthenticationConverter() {
final JwtGrantedAuthoritiesConverter grantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter();
grantedAuthoritiesConverter.setAuthoritiesClaimName("securityGroups");
grantedAuthoritiesConverter.setAuthorityPrefix("");
final JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter();
jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(grantedAuthoritiesConverter);
return jwtAuthenticationConverter;
}
Then just let your application know that you want to use this support by additional security filter chain configuration:
#Configuration
public class SecurityConf {
#Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.csrf().disabled().authorizeRequests()
.antMatchers("/principal").permitAll()
.anyRequest().authenticated()
.and()
.oauth2ResourceServer(oauth2Server -> oauth2Server.jwt());
return http.build();
}
}
Note that I don't use WebSecurityConfigurerAdapter because it's deprecated now.
Above configuration will create a filter which will try to authorize users before reaching your controller endpoints and will throw 401 if there's no JWT or it's expired or invalid for other reasons.
So you won't need a separate endpoint like "/principal" for "login" with a token, because Spring will create an Authentication object in a SecurityContextHolder for every request.
Moreover, this configuration will let you authorize users depending on their "securityGroups", so if you decide to configure some role-based or authority-based access to some endpoints Spring will check it for you and return 403 if authorities are insufficient.

spring boot end point works with browser but not with postman

I am new to spring security, I have written test endpoint with google oauth2, I can authencitace with web browser and my end point works, but not working with postman,
here is my properties
spring.security.oauth2.client.registration.google.client-id={{CLIENT_ID}}
spring.security.oauth2.client.registration.google.client-secret={{SECRET}}
spring.security.oauth2.client.registration.google.scope=openid,profile,email
Here is my controller
#RestController
public class UserController {
#GetMapping()
public String get() {
return "testing java 18";
}
}
Here is my security config
#Configuration
#EnableWebSecurity(debug = true)
public class SecurityConfig {
#Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(authorize -> authorize
.anyRequest().authenticated()
)
.oauth2Login(withDefaults());
return http.build();
}
}
with above configuration, if I test http://localhost:8080 on browser it redirects to google signing and the text testing java 18 appears.
But when I use postman with below config
Type: `OAuth 2.0`
Add authorization to data to: `Request Header`
Configure New Token:
Token Name: {{SOME_RANDOM_NAME}}
Grant Type: `Authorization Code`
Callback URL: `https://oauth.pstmn.io/v1/callback`
Auth URL: `https://accounts.google.com/o/oauth2/auth`
Access Token URL: `https://oauth2.googleapis.com/token`
Client ID: `{{CLIENT_ID}}` same used in application.properties
Client Secret: `{{CLIENT_SECRET}}` same used in application.properties
Scope: `profile email openid`
State: empty
Client Authentication: `Send as basic auth header`
With above if I hit Get New Access Token, I do get new Access Token and id_token with google signing, after I hit Use Token I and send GET on the endpoint I get 403
Also If I use id_token and change Type to Bearer Token I get the same.
with older version If I use below Securityy config, Endpoint works when I send my id_token as Type to Bearer Token
#Configuration
#EnableWebSecurity(debug = false)
public class SecurityConfig extends WebSecurityConfigurerAdapter{
#Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/**").fullyAuthenticated()
.and()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.oauth2ResourceServer().jwt();
}
}
I tried searching in the forum but didn't; got much.

How to get spring security context in reactive webflux

I have a problem with my spring boot application (version 2.6.3).
I have configured reactive spring security like there:
MyApplication.java:
#SpringBootApplication
#EnableWebFlux
#EnableWebFluxSecurity
#EnableReactiveMethodSecurity
public class MyApplication {
public static void main(String[] args) {
SpringApplication.run(MyApplication.class);
}
#Bean
public SecurityWebFilterChain springSecurityFilterChain(final ServerHttpSecurity http, final ReactiveOpaqueTokenIntrospector reactiveOpaqueTokenIntrospector) {
return http.authorizeExchange()
.anyExchange().authenticated()
.and()
.httpBasic().disable()
.cors().and()
.logout().disable()
.formLogin().disable()
.oauth2ResourceServer()
.opaqueToken()
.introspector(reactiveOpaqueTokenIntrospector)
.and().and()
.csrf()
.disable()
.build();
}
}
And this is my web resource (controller):
MyWebResource.java:
#RestController
public class MyWebResource implements MyWebResourceApi {
#PreAuthorize("hasRole('ROLE_USER')")
#Override
public Mono<String> details(String userId, ServerWebExchange exchange) {
return exchange.getPrincipal().map(Principal::getName);
}
}
It's work fine, when my access token is expired or incorrect the request should be denied. However when PreAuthorized allow request, my user principal will be never resolved in my exchange...
In reactive application authentication information is stored in the Reactive flow and accessible from Mono/Flux. You could use ReactiveSecurityContextHolder to obtain the currently authenticated principal, or an authentication request token.
#PreAuthorize("hasRole('ROLE_USER')")
public Mono<String> details() {
return ReactiveSecurityContextHolder.getContext()
.map(ctx -> ((Principal) ctx.getAuthentication().getPrincipal()).getName());
}
I found this answer looking for a way to add the access token to my webclient requests. If we are using OAuth2 or OpenID Connect and want to access the token instead of the principal's name, then this is not possible via the principal in the security context.
Instead we need to create a ServerOAuth2AuthorizedClientExchangeFilterFunction and register it as a filter function to the WebClient:
ServerOAuth2AuthorizedClientExchangeFilterFunction oauth =
new ServerOAuth2AuthorizedClientExchangeFilterFunction(
clientRegistrations,
authorizedClients);
oauth.setDefaultOAuth2AuthorizedClient(true);
where clientRegistrations is an injectabile bean of type ReactiveClientRegistrationRepository and authorizedClients is an injectable bean of type ServerOAuth2AuthorizedClientRepository.
We can then use the filters method for the builder to add our filter function to the exchangeFilterFunctions:
WebClient.builder()
.filters(exchangeFilterFunctions -> {
exchangeFilterFunctions.add(oauth);
})
.build();
Baeldung has a nice background article about this, which explains it in more detail: https://www.baeldung.com/spring-webclient-oauth2

Spring security on endpoint using Cognito IAM role?

I am trying to restrict specific endpoints on a Spring boot service depending on what role they have set in the OAuth2 credentials.
This is the endpoint
#RestController
#RequestMapping("/api/admin")
public class AdminController {
#GetMapping(produces = "application/json")
public TestResponse get() {
return new TestResponse("Admin API Response");
}
}
This is then secured using SecurityConfiguration bean
#Configuration
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf()
.and()
.authorizeRequests()
.antMatchers("/login", "/", "/home", "/logout", "/ping").permitAll()
.antMatchers("/api/admin").hasRole("arn:aws:iam::xxxxxx:role/spring-sso-test-ADMIN")
.antMatchers("/api/user").hasRole("arn:aws:iam::xxxxxx:role/spring-sso-test-USER")
.and()
.oauth2Login()
.and()
.logout()
.logoutSuccessUrl("/logout");
}
}
I debugged the Principal and can see the correct IAM role in the list of attributes cognito:roles list
However when I hit the endpoint I get a HTTP 403 Unauthorized. Meaning that the user has authenticated successfully, but Spring does not recognize or understand the attributes or how to map them?
I tried using the #Secured annotation but that didn't change anything.
#Secured("arn:aws:iam::xxxxxx:role/spring-sso-test-ADMIN")
#GetMapping(produces = "application/json")
public TestResponse get() {
return new TestResponse("Admin API Response");
}
How do I allow this to work using an IAM role defined in AWS Cognito?
When you use the hasRole DSL method, Spring Security adds the ROLE_ prefix to your authority. So, the authority arn:aws:iam::xxxxxx:role/spring-sso-test-ADMIN will become ROLE_arn:aws:iam::xxxxxx:role/spring-sso-test-ADMIN.
You should use the hasAuthority method instead.
Additionally, you should take the cognito:roles from the attributes and add in the authorities, since it's the property that Spring Security will query to get the authorities.
To map the authorities you can use a OAuth2UserService:
#Bean
SecurityFilterChain app(HttpSecurity http) throws Exception {
http
.oauth2Login(oauth2 -> oauth2
.userInfoEndpoint(userInfo -> userInfo
.oidcUserService(this.oidcUserService())
...
)
);
return http.build();
}
private OAuth2UserService<OidcUserRequest, OidcUser> oidcUserService() {
// your custom implementation
}
More details in the documentation.

Spring Filter to add roles to a request from token

I'm looking for the proper way to add role based authentication where I extract out roles from a JWT.
Ideally, I would like to extract roles from the JWT after authentication has taken place. This will work by inspecting the web token for some fields related to the roles which we are getting from our authentication system, keycloak.
My question is: is it possible to append roles to a request and then use http configuration to require one of these extracted roles?
Below is some relevant code that will help explain what I'm doing.
In my WebSecurityConfigurer, I make the access token available, scoped per request.
#Bean
#Scope(scopeName = WebApplicationContext.SCOPE_REQUEST, proxyMode = ScopedProxyMode.TARGET_CLASS)
public AccessToken accessToken() {
try {
HttpServletRequest request =
((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes())
.getRequest();
return ((KeycloakSecurityContext) ((KeycloakAuthenticationToken) request
.getUserPrincipal())
.getCredentials()).getToken();
} catch (Exception exc) {
return null;
}
}
Then I override some of the configuration of the http in the configure method.
http
// Disable session management
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
// Allow calls to OPTIONS
.authorizeRequests().antMatchers(HttpMethod.OPTIONS, "/**").permitAll()
.and()
// Authenticate every other call
.authorizeRequests().anyRequest().authenticated()
.and()
.csrf().disable();
Ideally, what I'd like to achieve is something like:
http.antMatchers("/foo").hasRole("jwt_extracted_role")
I am currently creating custom filters which extract roles from the token and then check for the correct roles, but this is maybe more cumbersome than it needs to be.
Any pointers on which methods of which configuration classes I should be looking to override to extract the roles from the request's, and add them to the request?
I ended up solving this by overriding the KeycloakAuthenticationProvider and providing my override class as a bean in the WebSecurityConfig. My class is below:
public class ResourceAwareAuthenticationProvider extends KeycloakAuthenticationProvider {
#Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
... here I add granted authorities from the token's credentials ...
}
}
Then in my class WebSecurityConfigurer extends KeycloakWebSecurityConfigurerAdapter I override the AuthenticationProvider:
#Bean
#Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return new ProviderManager(Lists.newArrayList(new ResourceAwareAuthenticationProvider()));
}
This allows me to do configuration like:
http.authorizeRequests()
.antMatchers("/**").hasAuthority("my-resource-authority")

Categories