iPhoneJavaDev
iPhoneJavaDev

Reputation: 825

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.

Upvotes: 0

Views: 2697

Answers (1)

Andrei Titov
Andrei Titov

Reputation: 1658

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.

Upvotes: 1

Related Questions