hotmeatballsoup
hotmeatballsoup

Reputation: 595

Spring Security filters for JWT-based authentication, verification and authorization scheme, by example

Java + Spring (and Spring Security) here, interested in implementing a JWT-based auth mechanism for my web service using bearer tokens. My understanding of the proper way of using Spring Security for authentication and authorization is through the use of provided (or custom) filters as follows:

So to begin with, if anything I have stated above is a Spring Security (or web security in general) anti-pattern or is misled, please begin by providing course correction and steering me in the right direction!

Assuming I'm more or less understanding the "auth flow" above correctly...

Are there any specific Spring Security filters that take care of all of this for me already, or that can be extended and have a few methods overridden to behave this way? Or anything that comes really close? Looking at the list of authentication-specific Spring Security filters I see:

As for token verification and authorization, I (much to my surprise) don't see anything in the Spring Security landscape that could qualify.

Unless anyone knows of JWT-specific filters that I can use or subclass easily, I think I need to implement my own custom filters, in which case I'm wondering how to conigure Spring Security to use them and not use any of these other authentication filters (such as UsernamePasswordAuthenticationFilter) as part of the filter chain.

Upvotes: 5

Views: 8297

Answers (1)

jzheaux
jzheaux

Reputation: 7707

As I understand it, you want to:

  1. Authenticate users via a username and password and respond with a JWT
  2. On subsequent requests, authenticate users using that JWT

username/password -> JWT isn't an established authentication mechanism on its own, which is why Spring Security doesn't yet have direct support.

You can get it on your own pretty easily, though.

First, create a /token endpoint that produces a JWT:

@RestController
public class TokenController {

    @Value("${jwt.private.key}")
    RSAPrivateKey key;

    @PostMapping("/token")
    public String token(Authentication authentication) {
        Instant now = Instant.now();
        long expiry = 36000L;
        // @formatter:off
        String scope = authentication.getAuthorities().stream()
                .map(GrantedAuthority::getAuthority)
                .collect(Collectors.joining(" "));
        JWTClaimsSet claims = new JWTClaimsSet.Builder()
                .issuer("self")
                .issueTime(new Date(now.toEpochMilli()))
                .expirationTime(new Date(now.plusSeconds(expiry).toEpochMilli()))
                .subject(authentication.getName())
                .claim("scope", scope)
                .build();
        // @formatter:on
        JWSHeader header = new JWSHeader.Builder(JWSAlgorithm.RS256).build();
        SignedJWT jwt = new SignedJWT(header, claims);
        return sign(jwt).serialize();
    }

    SignedJWT sign(SignedJWT jwt) {
        try {
            jwt.sign(new RSASSASigner(this.key));
            return jwt;
        }
        catch (Exception ex) {
            throw new IllegalArgumentException(ex);
        }
    }

}

Second, configure Spring Security to allow HTTP Basic (for the /token endpoint) and JWT (for the rest):

@Configuration
public class RestConfig extends WebSecurityConfigurerAdapter {

    @Value("${jwt.public.key}")
    RSAPublicKey key;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // @formatter:off
        http.authorizeRequests((authz) -> authz.anyRequest().authenticated())
            .csrf((csrf) -> csrf.ignoringAntMatchers("/token"))
            .httpBasic(Customizer.withDefaults())
            .oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt)
            .sessionManagement((session) -> session
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS))
            .exceptionHandling((exceptions) -> exceptions
                .authenticationEntryPoint(new BearerTokenAuthenticationEntryPoint())
                .accessDeniedHandler(new BearerTokenAccessDeniedHandler())
            );
        // @formatter:on
    }

    @Bean
    UserDetailsService users() {
        // @formatter:off
        return new InMemoryUserDetailsManager(
            User.withUsername("user")
                .password("{noop}password")
                .authorities("app")
                .build());
        // @formatter:on
    }

    @Bean
    JwtDecoder jwtDecoder() {
        return NimbusJwtDecoder.withPublicKey(this.key).build();
    }

}

I think there's appetite to add support for something like this in spring-authorization-server to reduce the /token boilerplate, if you're interested in contributing your efforts!

Upvotes: 1

Related Questions