Skip to main content

JWT in Spring Boot: The Three Dependencies You Need

Β· 6 min read
Mahmut Salman
Software Developer

Want to implement JWT authentication in Spring Boot? You need three dependencies, not one. Here's why and what each one does.

The Three JWT Dependencies​

Add these to your pom.xml:

<dependencies>
<!-- JWT API - The interface definitions -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.12.3</version>
</dependency>

<!-- JWT Implementation - The actual code -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.12.3</version>
<scope>runtime</scope>
</dependency>

<!-- JWT Jackson - JSON processing -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.12.3</version>
<scope>runtime</scope>
</dependency>
</dependencies>

Why Three Dependencies?​

1. jjwt-api (The Interface)​

What it is: Interface definitions and method signatures

What you import in your code:

import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.Claims;

Purpose: Compile-time dependency - lets you write JWT code

Scope: compile (default)

2. jjwt-impl (The Implementation)​

What it is: Actual implementation of the JWT API

What it does:

  • Provides the real code behind the interfaces
  • Handles JWT creation and parsing
  • Manages signatures and validation

Purpose: Runtime dependency - actually executes JWT operations

Scope: runtime (not needed during compilation)

3. jjwt-jackson (The JSON Processor)​

What it is: JSON serialization/deserialization for JWT

What it does:

  • Converts Java objects to JSON (in JWT payload)
  • Parses JSON back to Java objects
  • Handles JWT claims processing

Purpose: Runtime dependency - processes JWT JSON data

Scope: runtime (not needed during compilation)


The Dependency Chain​

Your Code
↓
jjwt-api (compile time)
↓
jjwt-impl (runtime) β†’ Executes JWT operations
↓
jjwt-jackson (runtime) β†’ Handles JSON in JWT

Think of it like:

  • API = The contract (interface)
  • Impl = The worker (implementation)
  • Jackson = The translator (JSON processor)

What Each Scope Means​

compile (jjwt-api)​

<dependency>
<artifactId>jjwt-api</artifactId>
<scope>compile</scope> <!-- or omit (default) -->
</dependency>

Needed for:

  • βœ… Compiling your code
  • βœ… Running your app

runtime (jjwt-impl, jjwt-jackson)​

<dependency>
<artifactId>jjwt-impl</artifactId>
<scope>runtime</scope>
</dependency>

Needed for:

  • ❌ Not for compiling
  • βœ… Running your app

Why runtime?

  • You don't directly import classes from these JARs
  • They're discovered and used automatically at runtime
  • Keeps your code independent of specific implementations

Basic JWT Usage Example​

Once dependencies are added, you can create and validate JWTs:

Creating a JWT​

import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.security.Keys;
import java.security.Key;
import java.util.Date;

public class JwtUtil {

private static final Key SECRET_KEY = Keys.secretKeyFor(SignatureAlgorithm.HS256);

public String generateToken(String username) {
return Jwts.builder()
.setSubject(username) // Who the token is for
.setIssuedAt(new Date()) // When issued
.setExpiration(new Date(System.currentTimeMillis() + 86400000)) // 24h
.signWith(SECRET_KEY) // Sign with secret
.compact(); // Build the JWT string
}
}

What happens:

  1. jjwt-api β†’ Provides Jwts.builder() method
  2. jjwt-impl β†’ Actually builds the JWT
  3. jjwt-jackson β†’ Converts claims to JSON

Validating a JWT​

import io.jsonwebtoken.Claims;

public class JwtUtil {

public Claims validateToken(String token) {
return Jwts.parserBuilder()
.setSigningKey(SECRET_KEY)
.build()
.parseClaimsJws(token)
.getBody();
}

public String extractUsername(String token) {
Claims claims = validateToken(token);
return claims.getSubject();
}
}

What happens:

  1. jjwt-api β†’ Provides parsing methods
  2. jjwt-impl β†’ Actually parses and validates
  3. jjwt-jackson β†’ Converts JSON to Claims object

What JWT Looks Like​

The Token Structure​

eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJqb2huIiwiaWF0IjoxNjk4NzY1NDMyLCJleHAiOjE2OTg4NTE4MzJ9.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

Three parts separated by dots:

HEADER.PAYLOAD.SIGNATURE

1. Header (Algorithm & Token Type)​

{
"alg": "HS256",
"typ": "JWT"
}

Base64 encoded: eyJhbGciOiJIUzI1NiJ9

2. Payload (Claims/Data)​

{
"sub": "john",
"iat": 1698765432,
"exp": 1698851832
}

Base64 encoded: eyJzdWIiOiJqb2huIiwiaWF0IjoxNjk4NzY1NDMyLCJleHAiOjE2OTg4NTE4MzJ9

3. Signature (Verification)​

HMACSHA256(
base64UrlEncode(header) + "." + base64UrlEncode(payload),
secret
)

Base64 encoded: SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c


Common Claims in JWT​

Jwts.builder()
.setSubject("john") // Subject (username/user ID)
.setIssuedAt(new Date()) // When issued
.setExpiration(new Date(expiryTime)) // When expires
.setIssuer("my-app") // Who issued
.claim("role", "ADMIN") // Custom claim
.claim("email", "john@email.com") // Custom claim
.signWith(key)
.compact();

Standard claims:

  • sub (subject) β†’ User identifier
  • iat (issued at) β†’ Timestamp
  • exp (expiration) β†’ Expiry timestamp
  • iss (issuer) β†’ Who created it
  • aud (audience) β†’ Who it's for

Custom claims: Any data you want!


Dependency Verification​

Check Dependencies Are Downloaded​

mvn dependency:tree | grep jjwt

Expected output:

[INFO] +- io.jsonwebtoken:jjwt-api:jar:0.12.3:compile
[INFO] +- io.jsonwebtoken:jjwt-impl:jar:0.12.3:runtime
[INFO] +- io.jsonwebtoken:jjwt-jackson:jar:0.12.3:runtime

Check JAR Files Exist​

ls ~/.m2/repository/io/jsonwebtoken/

Should see:

jjwt-api/
jjwt-impl/
jjwt-jackson/

Why Not Just One Dependency?​

Historical reasons:

  • Modular design (separation of concerns)
  • Choose your JSON processor (Jackson, Gson, or custom)
  • Keep API stable while updating implementation
  • Support different Java versions

Alternatives to jjwt-jackson:

<!-- Option 1: Jackson (recommended) -->
<dependency>
<artifactId>jjwt-jackson</artifactId>
</dependency>

<!-- Option 2: Gson -->
<dependency>
<artifactId>jjwt-gson</artifactId>
</dependency>

<!-- Option 3: org.json -->
<dependency>
<artifactId>jjwt-orgjson</artifactId>
</dependency>

Most projects use Jackson (it's what Spring Boot uses internally)


Common Issues​

Issue 1: Missing Implementation​

Error:

java.lang.ClassNotFoundException: io.jsonwebtoken.impl.DefaultJwtBuilder

Solution: Add jjwt-impl:

<dependency>
<artifactId>jjwt-impl</artifactId>
<scope>runtime</scope>
</dependency>

Issue 2: Missing JSON Processor​

Error:

java.lang.ClassNotFoundException: com.fasterxml.jackson.databind.ObjectMapper

Solution: Add jjwt-jackson:

<dependency>
<artifactId>jjwt-jackson</artifactId>
<scope>runtime</scope>
</dependency>

Issue 3: Version Mismatch​

Error:

Incompatible version: jjwt-api 0.12.3 requires jjwt-impl 0.12.3

Solution: Use same version for all three:

<properties>
<jjwt.version>0.12.3</jjwt.version>
</properties>

<dependency>
<artifactId>jjwt-api</artifactId>
<version>${jjwt.version}</version>
</dependency>

Best Practices​

βœ… Do This​

1. Use the same version for all three

<properties>
<jjwt.version>0.12.3</jjwt.version>
</properties>

2. Use runtime scope for impl and jackson

<scope>runtime</scope>

3. Store secrets securely

// ❌ Don't hardcode
private static final String SECRET = "mySecret123";

// βœ… Use environment variables
private static final String SECRET = System.getenv("JWT_SECRET");

4. Set reasonable expiration times

// Access token: short-lived (15 minutes)
.setExpiration(new Date(System.currentTimeMillis() + 900000))

// Refresh token: long-lived (7 days)
.setExpiration(new Date(System.currentTimeMillis() + 604800000))

❌ Avoid This​

1. Don't use weak secrets

// ❌ Too short
Keys.hmacShaKeyFor("secret".getBytes())

// βœ… Use proper key generation
Keys.secretKeyFor(SignatureAlgorithm.HS256)

2. Don't store sensitive data in JWT

// ❌ Bad - passwords in JWT!
.claim("password", user.getPassword())

// βœ… Good - only non-sensitive data
.claim("role", user.getRole())

3. Don't skip validation

// ❌ Parse without validation
Jwts.parser().parse(token)

// βœ… Parse with signature verification
Jwts.parserBuilder()
.setSigningKey(SECRET_KEY)
.build()
.parseClaimsJws(token)

Summary​

The Three Dependencies​

jjwt-api      β†’ Interface (compile time)
jjwt-impl β†’ Implementation (runtime)
jjwt-jackson β†’ JSON processor (runtime)

Quick Start​

1. Add to pom.xml:

<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.12.3</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.12.3</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.12.3</version>
<scope>runtime</scope>
</dependency>

2. Create JWT:

String token = Jwts.builder()
.setSubject(username)
.setExpiration(expiryDate)
.signWith(secretKey)
.compact();

3. Validate JWT:

Claims claims = Jwts.parserBuilder()
.setSigningKey(secretKey)
.build()
.parseClaimsJws(token)
.getBody();

Remember: All three dependencies work together. The API defines methods, the implementation executes them, and Jackson handles the JSON. Missing any one = errors! ⚠️

Tags: #spring-boot #jwt #authentication #security #maven