Implementing JWT Authentication and Authorization in Spring Boot Applications
JWT (JSON Web Token) is an open standard for securely transmitting information between parties as a JSON object. It is commonly used for authentication and authorization in web applications. Tokens can be sent via URL parameters, POST requests, or HTTP headers. The payload contains all necessary user information, reducing the need for frequent database queries.
JWT is primarily used for authorization, where each request includes a token after user login, granting access to permitted routes, services, and resources. Single sign-on is a key feature of JWT. It also facilitates information exchange through signing with public/private keys.
Advantages include cross-language compatibility due to JSON format, ability to store non-sensitive business logic data in the payload, compact size for efficient transmission, and statelessness on the server side for easy scalability. Disadvantages involve security risks if excessive sensitive data is stored in tokens, and potential performance overhead when stored in cookies.
JWT Structure
A JWT consists of three parts separated by dots: header, payload, and signature.
Header
The header specifies the token type and encryption algorithm, typically HMAC SHA256.
{
"typ": "JWT",
"alg": "HS256"
}
Payload
The payload contains claims, which are statements about an entity. There are three types: registered claims, public claims, and private claims.
Registered claims include:
iss: Issuersub: Subjectaud: Audienceexp: Expiration timeiat: Issued at timenbf: Not before timejti: JWT ID
Public claims are defined by the parties using JWT, while private claims are custom claims for sharing information.
Example payload:
{
"iss": "auth-server",
"iat": 1708699474,
"exp": 1708799474,
"aud": "client-app",
"sub": "user@example.com",
"role": "admin",
"department": "engineering"
}
Signature
The signature is created by encoding the header and payload with Base64URL, concatenating them with a dot, and signing with a secret key using the specified algorithm.
Basic JWT Implementation
Typically, JWTs are included in the Authorization header with the Bearer scheme. The flow involves the server issuing a token upon user authentication, which the client includes in subsequent requests for validation.
Dependencies
Add the following dependency to your Maven project:
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>4.4.0</version>
</dependency>
Token Generation
import com.auth0.jwt.JWT;
import com.auth0.jwt.algorithms.Algorithm;
import java.time.Instant;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
public class TokenGenerator {
private static final String SECRET_KEY = "secure-secret-key";
public static String generateToken() {
Date expiryDate = new Date(System.currentTimeMillis() + 30000); // 30 seconds
Map<String, Object> headerClaims = new HashMap<>();
headerClaims.put("typ", "JWT");
headerClaims.put("alg", "HS256");
return JWT.create()
.withHeader(headerClaims)
.withIssuer("auth-service")
.withIssuedAt(Date.from(Instant.now()))
.withExpiresAt(expiryDate)
.withSubject("user@domain.com")
.withAudience("api-service")
.withClaim("userRole", "editor")
.withClaim("userId", 42)
.sign(Algorithm.HMAC256(SECRET_KEY));
}
}
Token Validation
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTVerificationException;
import com.auth0.jwt.interfaces.DecodedJWT;
public class TokenValidator {
private static final String SECRET_KEY = "secure-secret-key";
public static void validateToken(String token) {
try {
JWTVerifier verifier = JWT.require(Algorithm.HMAC256(SECRET_KEY))
.build();
DecodedJWT decoded = verifier.verify(token);
System.out.println("User ID: " + decoded.getClaim("userId"));
System.out.println("User Role: " + decoded.getClaim("userRole"));
} catch (JWTVerificationException e) {
System.err.println("Token validation failed: " + e.getMessage());
}
}
}
Common exceptions include TokenExpiredException, SignatureVerificationException, AlgorithmMismatchException, and IncorrectClaimException.
Spring Boot Integration
Implement JWT authentication in a Spring Boot application with an interceptor for validation.
Project Dependencies
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.1.5</version>
</parent>
<groupId>com.example</groupId>
<artifactId>jwt-demo</artifactId>
<version>1.0.0</version>
<properties>
<java.version>17</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>4.4.0</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
</project>
JWT Utility Class
package com.example.util;
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTCreator;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.interfaces.DecodedJWT;
import java.util.Calendar;
import java.util.Map;
public class JwtHelper {
private static final String SECRET = "application-secret";
public static String createToken(Map<String, String> claims) {
Calendar expiration = Calendar.getInstance();
expiration.add(Calendar.HOUR, 24);
JWTCreator.Builder tokenBuilder = JWT.create();
claims.forEach(tokenBuilder::withClaim);
return tokenBuilder.withExpiresAt(expiration.getTime())
.sign(Algorithm.HMAC256(SECRET));
}
public static DecodedJWT verifyToken(String token) {
return JWT.require(Algorithm.HMAC256(SECRET))
.build()
.verify(token);
}
}
Controller Implementation
package com.example.controller;
import com.example.util.JwtHelper;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.HashMap;
import java.util.Map;
@RestController
@RequestMapping("/api")
public class AuthController {
@PostMapping("/authenticate")
public ResponseEntity<Map<String, Object>> authenticate(
@RequestParam String username,
@RequestParam String password) {
Map<String, Object> response = new HashMap<>();
// Validate credentials (simplified for demonstration)
if ("admin".equals(username) && "password123".equals(password)) {
Map<String, String> claims = new HashMap<>();
claims.put("username", username);
claims.put("role", "administrator");
String token = JwtHelper.createToken(claims);
response.put("success", true);
response.put("token", token);
response.put("message", "Authentication successful");
} else {
response.put("success", false);
response.put("message", "Invalid credentials");
}
return ResponseEntity.ok(response);
}
@GetMapping("/protected")
public ResponseEntity<Map<String, Object>> getProtectedData(
@RequestHeader("Authorization") String authHeader) {
Map<String, Object> response = new HashMap<>();
try {
String token = authHeader.replace("Bearer ", "");
JwtHelper.verifyToken(token);
response.put("success", true);
response.put("data", "Sensitive information accessed successfully");
} catch (Exception e) {
response.put("success", false);
response.put("error", "Token validation failed");
}
return ResponseEntity.ok(response);
}
}
Interceptor Configuration
package com.example.interceptor;
import com.example.util.JwtHelper;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.http.MediaType;
import org.springframework.web.servlet.HandlerInterceptor;
import java.io.PrintWriter;
import java.util.HashMap;
import java.util.Map;
public class JwtInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) throws Exception {
String authHeader = request.getHeader("Authorization");
Map<String, Object> result = new HashMap<>();
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
result.put("success", false);
result.put("message", "Missing or invalid authorization header");
sendResponse(response, result);
return false;
}
try {
String token = authHeader.substring(7);
JwtHelper.verifyToken(token);
return true;
} catch (Exception e) {
result.put("success", false);
result.put("message", "Token validation failed: " + e.getMessage());
sendResponse(response, result);
return false;
}
}
private void sendResponse(HttpServletResponse response,
Map<String, Object> data) throws Exception {
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
PrintWriter writer = response.getWriter();
writer.write(new ObjectMapper().writeValueAsString(data));
writer.flush();
}
}
Register the interceptor:
package com.example.config;
import com.example.interceptor.JwtInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new JwtInterceptor())
.addPathPatterns("/api/protected")
.excludePathPatterns("/api/authenticate");
}
}