loading...
Cover image for JWT Bearer Authentication/Authorization with Spring Security 5 in a Spring Boot App

JWT Bearer Authentication/Authorization with Spring Security 5 in a Spring Boot App

d_tomov profile image Dimitar Tomov ・7 min read

These days I’ve been trying to compile a sane and simple example of how to do JWT Bearer Security on a Spring Boot app. Something that is standard of REST web service security these days. I’ve found a few good examples, but nothing which satisfied my interest on the topic. I tried compiling as much information and making it as clean of an example as I could.

Overview

  1. Dependencies
  2. Token Service Interface
  3. JWTTokenService (Implementation) — JWT Generation and Parsing
  4. Authentication/Authorization Filter
  5. Security Configuration
  6. Endpoint example
  7. Notes

1. Dependencies

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!-- JWT Dependencies -->
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt-api</artifactId>
            <version>0.11.1</version>
        </dependency>
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt-impl</artifactId>
            <version>0.11.1</version>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt-jackson</artifactId>
            <version>0.11.1</version>
            <scope>runtime</scope>
        </dependency>

We start the application as a normal Spring Boot App. Add Spring Web for standard REST APIs and Spring Security for security part— download and unzip.

We also need to add the io.jsonwebtoken’s JWT dependencies. Notice two of JWT’s dependencies are copied from maven central as runtime dependencies, that is because they are not needed during the compilation phase, only during runtime of your application. Only dependency you need for compilation is jjwt-api.

2. Token Service Interface

For the example the token operations are separated into a TokenService interface that looks like this:

public interface TokenService {
    String generateToken(User user);

    UserPrincipal parseToken(String token);
}

User is the entity in the application and looks like :

public class User {
    private Integer id;
    private String username;
    private String password;
    private boolean isAdmin;

   <Getters,Setters,Constructors>
}

UserPrincipal is the Principal object which will be inside Spring’s Security Context. Principal is the user currently logged into the application. We’ll look later how to set it inside the Spring Security Context.

UserPrincipal looks like this:

public class UserPrincipal {
    private Integer id;
    private String username;
    private boolean isAdmin;

    <Getters,Setters,Constructors>
}

3. JWTTokenService (Implementation)

We need to implement two methods, one for the token generation, and one for the token parsing. (JWT_SECRET is a String)

Token Generation

    @Override
    public String generateToken(User user) {
        Instant expirationTime = Instant.now().plus(1, ChronoUnit.HOURS);
        Date expirationDate = Date.from(expirationTime);

        Key key = Keys.hmacShaKeyFor(JWT_SECRET.getBytes());

        String compactTokenString = Jwts.builder()
                .claim("id", user.getId())
                .claim("sub", user.getUsername())
                .claim("admin", user.isAdmin())
                .setExpiration(expirationDate)
                .signWith(key, SignatureAlgorithm.HS256)
                .compact();

        return "Bearer " + compactTokenString;
    }

We use the Jwts.builder() to build our token and building it as compact token. In the example we use it to set id, sub and admin claims, but you can add whatever claims you want.

A claim is a piece of information that will be added to the body of your JWTs. There are some standard claims like sub(subject), iss(issuer), … , which you can look at here.

We append “Bearer “ with empty space as a prefix, to specify that the authentication scheme is of type Bearer.

The name “Bearer authentication” can be understood as “give access to the bearer of this token.”

This is one point at which you’ll need to make few decisions:

  1. What would be the expiration time of your tokens?
    • This would depend very much on your use cases. There are use cases for very big expiration windows like 1 week, 1 day, 8 hours. Or very little expiration windows like 1 hour, 30 minutes, 10 minutes.
    • For the example 1 hour is fine.
  2. What would be your secret?  — The secret is what will be used to sign your JWTs, in order for you to be able to verify it later when parsing. The secret's length will depend on the signing algorithm you’ll use and you can generate some random secret from here website.
  3. What will be your secret strategy? Or how are you going to manage secrets.
    • You can use one secret and sign all your JWTs with it. You can have secrets per user and keep them in your database. You can have secrets per token and keep the secret both in the token and in the database. (Note: One of the ideas of JWT is to not look up in the DB to authenticate users)
    • There probably other strategies, but I haven’t seen them, so I am not listing them here.
    • For the example using one secret to sign all my JWTs is fine.
  4. What would be your signing algorithm?  - JWTs use HMAC SHA algorithm, which is an algorithm used for data integrity validation (data authentication). It is standard for JWTs to used HS256, but you can go one up and use HS512. Your secret length depends on the algorithm HS256 -> 256 bit etc...
    • For the example HS256 is fine

Token Parsing

    /**
     * @param token - the compact token stripped from "Bearer " prefix
     */
    @Override
    public UserPrincipal parseToken(String token) {
        byte[] secretBytes = JWT_SECRET.getBytes();

        Jws<Claims> jwsClaims = Jwts.parserBuilder()
                .setSigningKey(secretBytes)
                .build()
                .parseClaimsJws(token);

        String username = jwsClaims.getBody()
                .getSubject();
        Integer userId = jwsClaims.getBody()
                .get("id", Integer.class);
        boolean isAdmin = jwsClaims.getBody().get("admin", Boolean.class);

        return new UserPrincipal(userId, username, isAdmin);
    }

When parsing the token, you need the same secret as the one you signed the JWT during generation. Depending on what secret strategy you picked or business logic, you might need to do some validation here.

Using Jwts.parserBuilder() to parse the token into a Jws object, where you can get whatever claims you put in the token. You know they are there, because JWTs are immutable and if someone forged a token the parsing will fail with invalid signature exception.

4. Authentication/Authorization Filter

public class JwtAuthenticationFilter extends OncePerRequestFilter {
    private final TokenService tokenService;

    public JwtAuthenticationFilter(TokenService tokenService) {
        this.tokenService = tokenService;
    }

    @Override
    protected void doFilterInternal(HttpServletRequest httpServletRequest,
                                    HttpServletResponse httpServletResponse,
                                    FilterChain filterChain) throws IOException, ServletException {
        String authorizationHeader = httpServletRequest.getHeader("Authorization");

        if (authorizationHeaderIsInvalid(authorizationHeader)) {
            filterChain.doFilter(httpServletRequest, httpServletResponse);
            return;
        }

        UsernamePasswordAuthenticationToken token = createToken(authorizationHeader);

        SecurityContextHolder.getContext().setAuthentication(token);
        filterChain.doFilter(httpServletRequest, httpServletResponse);
    }

    private boolean authorizationHeaderIsInvalid(String authorizationHeader) {
        return authorizationHeader == null
                || !authorizationHeader.startsWith("Bearer ");
    }

    private UsernamePasswordAuthenticationToken createToken(String authorizationHeader) {
        String token = authorizationHeader.replace("Bearer ", "");
        UserPrincipal userPrincipal = tokenService.parseToken(token);

        List<GrantedAuthority> authorities = new ArrayList<>();

        if (userPrincipal.isAdmin()) {
            authorities.add(new SimpleGrantedAuthority("ROLE_ADMIN"));
        }

        return new UsernamePasswordAuthenticationToken(userPrincipal, null, authorities);
    }
}

In the example we extend the OncePerRequestFilter, which aims to guarantee a single execution per request dispatch.

A simple check is done if the “Authorization” header (often used for passing Bearer tokens) is present.

Token is stripped of its “Bearer ” prefix and then UserPrincipal returned from the token parsing is passed into a UsernamePasswordAuthenticationToken which will serve as our Authentication/Authorization in the Spring Security Context.

This UsernamePasswordAuthenticationToken should be set inside the SecurityContext, using the SecurityContextHolder, in order to be used later.

img

5. Security Configuration

@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    private TokenService tokenService;

    @Autowired
    public SecurityConfig(TokenService tokenService) {
        this.tokenService = tokenService;
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .csrf().disable()
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .authorizeRequests()
                .mvcMatchers("/users", "/users/login").permitAll()
                .anyRequest().authenticated()
                .and()
                .addFilterBefore(new JwtAuthenticationFilter(tokenService),
                        UsernamePasswordAuthenticationFilter.class);
    }

}
  • @EnableWebSecurity is used to mark this class as a Web Security Configuration
  • @EnableGlobalMethodSecurity(prePostEnabled = true) is used because we want to use the @PreAuthorize annotations on my controller methods — which allows me to check for permissions/roles before method calls.
  • The class extends WebSecurityConfigurerAdapter, which is the base (adapter) class used for security configuration
  • CSRF is disabled — we don’t need cross site request forging protection on a web service, it is usually used in Browser app context.
  • Session management is STATELESS because we don’t need Spring to manage sessions and pull security context from there.
  • After that we configure our endpoint security. (order matters here be careful)

    • We start with permitting all requests to endpoints matched by mvcMatchers() - usually registration and login endpoints.
    • After that we configure every other request (except the permitted ones) to be authenticated. What that means is that Spring will look into the Security Context for some form of authentication in our case - UsernamePasswordAuthenticationToken, if not present will return 403 FORBIDDEN
  • At the end we add the filter and set its order to be just before the UsernamePasswordAuthenticationFilter. Order is needed because we didn’t extend an ordered filter and don’t have an annotation @Order on the filter class.

6. Endpoint example

@RestController
public class HelloController {

    @GetMapping("/hello")
    public String hello() {
        return "hello";
    }

    @GetMapping("/hello/admin")
    @PreAuthorize("hasRole('ADMIN')")
    public String helloAdmin() {
        return "hello admin";
    }

    @GetMapping("/hello/user")
    public String helloUser() {
        UserPrincipal userPrincipal =
                (UserPrincipal) SecurityContextHolder
                        .getContext()
                        .getAuthentication()
                        .getPrincipal();

        return "hello " + userPrincipal.getUsername();
    }
}

We have here three example endpoints:

  • Only requires authentication - /hello
  • Requires authentication and authorization as an ADMIN - /hello/admin
  • Uses the SecurityContextHolder to get the UserPrincipal (custom class you create) from the Security Context - /hello/user

7. Notes

  • This is a simple example of what Bearer JWT Authentication/Authorization would look like using Spring Security 5 (I don’t think there is anything specific to 5, but that is what I used)
  • This example could be extend with a Refresh token flow — I might do that in the future
  • I am using and endpoint(/users/login) which returns the generated token, as an alternative you can use filters.
  • If you don’t agree with something in the example you can always leave a comment and I’ll take it into account

Posted on Apr 27 by:

d_tomov profile

Dimitar Tomov

@d_tomov

Software Engineer - Java, Spring, Security

Discussion

markdown guide
 

Hi,
This solution works like magic.
Thank you very much!