JWT Setup in Spring Boot

Dependencies

We use jjwt (Java JWT) 0.12 to create and parse tokens. (Spring's own spring-boot-starter-oauth2-resource-server can also validate JWTs; the advanced level uses it with an authorization server.)

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-api</artifactId>
    <version>0.12.6</version>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-impl</artifactId>
    <version>0.12.6</version>
    <scope>runtime</scope>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-jackson</artifactId>
    <version>0.12.6</version>
    <scope>runtime</scope>
</dependency>

Configuration

Example

Generate a secret once and pass it as an environment variable:

openssl rand -base64 32
export JWT_SECRET='q3Jx9mZt0Vb7Lw2pR8sYc4Kd1Hf6Ng5Ue0Ai3Tj7Ws='

Example output

q3Jx9mZt0Vb7Lw2pR8sYc4Kd1Hf6Ng5Ue0Ai3Tj7Ws=

Config

application.yml

shopease:
  jwt:
    secret: ${JWT_SECRET}
    access-ttl: 15m
    issuer: https://api.shopease.in

JwtService

package in.shopease.security;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.security.Keys;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Service;

import javax.crypto.SecretKey;
import java.time.Duration;
import java.time.Instant;
import java.util.Date;

@Service
public class JwtService {

    private final SecretKey key;
    private final Duration accessTtl;
    private final String issuer;

    public JwtService(@Value("${shopease.jwt.secret}") String secret,
                      @Value("${shopease.jwt.access-ttl}") Duration accessTtl,
                      @Value("${shopease.jwt.issuer}") String issuer) {
        this.key = Keys.hmacShaKeyFor(Decoders.BASE64.decode(secret)); // HS256, needs >= 32 bytes
        this.accessTtl = accessTtl;
        this.issuer = issuer;
    }

    public String generateAccessToken(UserDetails user) {
        Instant now = Instant.now();
        return Jwts.builder()
                .issuer(issuer)
                .subject(user.getUsername())
                .claim("roles", user.getAuthorities().stream().map(GrantedAuthority::getAuthority).toList())
                .issuedAt(Date.from(now))
                .expiration(Date.from(now.plus(accessTtl)))
                .signWith(key)
                .compact();
    }

    /** Verifies signature, expiry and issuer. Throws JwtException (e.g. ExpiredJwtException) if invalid. */
    public Claims parse(String token) {
        return Jwts.parser()
                .verifyWith(key)
                .requireIssuer(issuer)
                .build()
                .parseSignedClaims(token)
                .getPayload();
    }

    public long accessTtlSeconds() {
        return accessTtl.toSeconds();
    }
}

Tip: jjwt 0.12 renamed several methods: setSubject → subject, parserBuilder() → parser(), setSigningKey → verifyWith, parseClaimsJws → parseSignedClaims, getBody → getPayload. Old tutorials use the old names.