Enterprise Java

Mock JWT Decoding with JwtDecoder in JUnit Tests

JSON web tokens (JWTs) are widely used for microservices and Spring security authentication. When testing Spring applications, mocking JWT decoding is often necessary. Let us delve into understanding how to test using Junit and mock JWT decoding with JwtDecoder.

1. Introduction to JWT and JwtDecoder

1.1 What is JWT?

JWT (JSON Web Token) is a compact, URL-safe token format used for securely transmitting information between parties. It is widely used for authentication and authorization in web applications. The JWT token follows the following structure:

1
2
3
4
5
-- Structure of a JWT Token
Header.Payload.Signature
 
-- Sample of a JWT token
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VyMSIsImV4cCI6MTY5MjM4Mjg4MH0.Df5ZyV8T1j1WQQ4u4xNhPvDTrMQ1rP_7M5kg5lPaP_U

1.1.1 Understanding the JWT Token

In the JWT token, each part of the token is separated by a dot (.):

  • Header: Contains metadata about the token, including the signing algorithm and token type. It includes:
    • alg: The signing algorithm used (e.g., HS256).
    • typ: The type of token (e.g., JWT).
    1
    {"alg":"HS256","typ":"JWT"}
  • Payload: Holds the claims, which are pieces of information about the user or token. It includes:
    • sub: Subject (e.g., the user identifier).
    • exp: Expiration time (e.g., Unix timestamp).
    1
    {"sub":"user1","exp":1692382880}
  • Signature: Ensures the token’s integrity and authenticity using a secret key or public-private key pair. It is generated by:
    • Encoding the header and payload.
    • Signing the encoded data using a secret key or private key.
    1
    2
    3
    4
    5
    HMACSHA256(
    base64UrlEncode(header) + "." +
    base64UrlEncode(payload),
    secret-key
    )

1.2 What is JwtDecoder?

JwtDecoder is an interface in Spring Security that decodes JWTs to extract claims.

1
2
3
// Code Syntax
JwtDecoder jwtDecoder = NimbusJwtDecoder.withSecretKey(secretKey).build();
Jwt jwt = jwtDecoder.decode(token);

The code snippet demonstrates the syntax for decoding a JWT using JwtDecoder in Spring Security. The first line initializes a JwtDecoder instance using NimbusJwtDecoder, which is configured with a secret key for HMAC-based JWT validation. The method withSecretKey(secretKey) specifies the secret key used for verifying the JWT signature, and the build() method finalizes the decoder’s configuration. In the second line, the decode(token) method of JwtDecoder is called to parse and validate the given JWT token. If the token is valid, it returns a Jwt object containing the token’s claims (such as subject, expiration time, and custom attributes). If the token is invalid, expired, or tampered with, an exception is thrown. This syntax is commonly used in Spring Security’s OAuth2 resource server implementations to authenticate and authorize API requests.

1.2.1 How does JwtDecoder work?

The JwtDecoder plays a crucial role in JWT-based authentication by verifying and extracting information from tokens. It ensures that the received JWT is valid, untampered, and correctly signed before allowing access to protected resources. Without proper decoding and validation, unauthorized users could manipulate tokens and gain access to sensitive data.

The verification process follows these steps:

  • It verifies the JWT’s signature to ensure it hasn’t been tampered with.
  • It decodes the token to extract claims such as subject, issuer, and expiration time.
  • It can work with:
    • A secret key (for HMAC-signed tokens like HS256).
    • A public key (for RSA-signed tokens like RS256).
    • A JWK Set URI (to dynamically fetch public keys from an authorization server).

1.2.2 Advantages of JwtDecoder

Key advantages of JwtDecoder include:

  • Provides secure JWT verification without manually handling cryptographic logic.
  • Supports different types of JWT signing mechanisms (HMAC, RSA, EC).
  • Integrates seamlessly with Spring Security’s OAuth2 Resource Server.

2. Code Example

2.1 Adding Dependencies

To use JWT in Spring Security, add dependencies in pom.xml file:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<!-- Spring Security Starter for basic authentication and authorization -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
 
<!-- JJWT (Java JWT) library for creating and parsing JWT tokens -->
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.11.5</version>
</dependency>
 
<!-- Spring Security OAuth2 Resource Server for handling JWT authentication -->
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-oauth2-resource-server</artifactId>
</dependency>
 
<!-- JUnit Jupiter API for writing and running unit tests -->
<dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter-api</artifactId>
    <scope>test</scope>
</dependency>

2.2 Mocking JwtDecoder in Junit

We use Mockito to mock the JwtDecoder behavior in JUnit tests.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
import static org.mockito.Mockito.*;
import static org.junit.jupiter.api.Assertions.*;
 
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.jwt.JwtException;
 
import java.time.Instant;
 
public class JwtDecoderTest {
 
    @Test
    void testValidJwtDecoder() {
        // Mock JwtDecoder
        JwtDecoder jwtDecoder = mock(JwtDecoder.class);
 
        // Create a mock valid JWT token
        Jwt validJwt = Jwt.withTokenValue("valid-token")
                .header("alg", "HS256")
                .claim("sub", "test-user")
                .claim("role", "ADMIN")
                .issuedAt(Instant.now())
                .expiresAt(Instant.now().plusSeconds(3600)) // Expires in 1 hour
                .build();
 
        // Define behavior for valid token
        when(jwtDecoder.decode("valid-token")).thenReturn(validJwt);
 
        // Test JWT decoding
        Jwt decodedJwt = jwtDecoder.decode("valid-token");
 
        // Assertions for valid JWT
        assertNotNull(decodedJwt);
        assertEquals("test-user", decodedJwt.getClaim("sub"));
        assertEquals("ADMIN", decodedJwt.getClaim("role"));
    }
 
    @Test
    void testExpiredJwtDecoder() {
        // Mock JwtDecoder
        JwtDecoder jwtDecoder = mock(JwtDecoder.class);
 
        // Create a mock expired JWT token
        Jwt expiredJwt = Jwt.withTokenValue("expired-token")
                .header("alg", "HS256")
                .claim("sub", "expired-user")
                .claim("role", "USER")
                .issuedAt(Instant.now().minusSeconds(7200)) // Issued 2 hours ago
                .expiresAt(Instant.now().minusSeconds(3600)) // Expired 1 hour ago
                .build();
 
        // Define behavior for expired token
        when(jwtDecoder.decode("expired-token")).thenReturn(expiredJwt);
 
        // Test JWT decoding
        Jwt decodedJwt = jwtDecoder.decode("expired-token");
 
        // Assertions for expired JWT (claims exist, but it should be considered expired)
        assertNotNull(decodedJwt);
        assertEquals("expired-user", decodedJwt.getClaim("sub"));
        assertEquals("USER", decodedJwt.getClaim("role"));
 
        // Additional check (if implementing real JWT validation, this should throw an exception)
        assertTrue(decodedJwt.getExpiresAt().isBefore(Instant.now()), "Token should be expired");
    }
 
    @Test
    void testInvalidJwtDecoder() {
        // Mock JwtDecoder
        JwtDecoder jwtDecoder = mock(JwtDecoder.class);
 
        // Define behavior for invalid token (simulate decoding failure)
        when(jwtDecoder.decode("invalid-token")).thenThrow(new JwtException("Invalid JWT token"));
 
        // Test invalid JWT decoding
        Exception exception = assertThrows(JwtException.class, () -> jwtDecoder.decode("invalid-token"));
 
        // Assertion for invalid JWT exception message
        assertEquals("Invalid JWT token", exception.getMessage());
    }
}

2.2.1 Code Explanation and Output

The JwtDecoderTest class demonstrates unit testing of JWT decoding using a mocked JwtDecoder in Java with JUnit. It includes three test cases to handle different JWT scenarios: valid, expired, and invalid tokens. In the first test, testValidJwtDecoder(), a valid JWT token is created with claims such as sub (subject) and role (ADMIN), along with an expiration time set to one hour ahead. The mock JwtDecoder is configured to return this token when decoding “valid-token”, and assertions verify that the correct claims are extracted. The second test, testExpiredJwtDecoder(), generates a JWT that expired one hour ago, verifying that the claims still exist but the expiration check confirms its invalidity. The final test, testInvalidJwtDecoder(), simulates an invalid JWT scenario by making the mock JwtDecoder throw a JwtException when attempting to decode “invalid-token”, ensuring that the application correctly handles decoding failures. This approach ensures robust JWT validation while leveraging Mockito for efficient unit testing.

Once the test runs are successfully executed, the output will be:

1
2
BUILD SUCCESSFUL in 1s
3 tests passed

If an assertion fails, JUnit will generate an error message like:

1
2
3
org.opentest4j.AssertionFailedError:
Expected :test-user
Actual   :wrong-user

3. Conclusion

Mocking JWT decoding with JwtDecoder is crucial for writing secure and efficient tests without relying on external authentication services. Using Mockito, we can simulate JWT validation, extract claims, and test authentication logic in Spring Security applications effectively.

Yatin Batra

An experience full-stack engineer well versed with Core Java, Spring/Springboot, MVC, Security, AOP, Frontend (Angular & React), and cloud technologies (such as AWS, GCP, Jenkins, Docker, K8).
Subscribe
Notify of
guest


This site uses Akismet to reduce spam. Learn how your comment data is processed.

0 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Back to top button