# Securing a Spring Boot REST API with JWT Authentication

Let’s go over a brief intro to JWT and Spring Security so we can follow what we are building with ease.

# JWT

**JWT** stands for **JSON Web Token**. It’s a **compact**, **URL-safe**, and **digitally signed** string that represents claims about a user or system. It’s often used to implement **stateless authentication** in web apps and APIs

### 🧱 Structure of a JWT

A JWT looks like this:

```java
CopyEditeyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJzdWIiOiJhbGljZSIsImlhdCI6MTcxMTk4MjE1MywiZXhwIjoxNzExOTg1NzUzfQ.
GgQ7xZKO9u0CzMo57AbRi5v3RPzS14-mK_l6BkoIbm0
```

It has three parts: **header**, **payload**, and **signature**. It’s a single string made up of **three base64-encoded sections**, separated by dots (`.`), like this:

```bash
<base64url-encoded header>.<base64url-encoded payload>.<base64url-encoded signature>
```

1. **Header** (what algorithm/signature type)
    
2. **Payload** (the claims: user info, timestamps, etc.)
    
3. **Signature** (proof the data hasn’t been tampered with).
    

Let’s break down some of the qualities we mentioned in the definition above little by little:

* Compact: It’s short
    
* URL-safe: Characters used in a JWT token **can safely appear in a URL or HTTP header** without needing to be escaped or encoded. Why This Matters? Because if a token has especial characters it might get corrupted, break the URL or get misinterpret by the server.
    
* Digitally Signed: It’s like a **seal of trust** or a **tamper-proof stamp**.
    
    * The server creates the JWT and signs it using a **secret key** (or a private key).
        
    * That signature becomes the **third part of the JWT**.
        
    * When the client sends the JWT back, the server **verifies** the signature using the same key (or a public key if using RSA).
        

### 🤔 Why **not** authenticate the user (username/password) on **every request**?

Imagine this:

1. The client sends the username and password **every time** it hits the API.
    
2. The server checks the credentials against the DB **on every request**.
    
3. The server needs to fetch the user, hash the password, compare, and then proceed.
    

Here’s **why this is a bad idea**:

| Issue | Why It's a Problem |
| --- | --- |
| 🐢 **Slow performance** | Checking credentials against a DB or identity provider on every request is expensive. |
| 🔒 **Security risk** | Sending credentials repeatedly over the network increases the chance they’re intercepted or leaked. |
| 📦 **No statelessness** | You’re relying on the DB for every request, making it hard to scale horizontally (in the cloud or containers). |
| ❌ **Not how HTTP is meant to work** | HTTP is stateless; sending full credentials over and over breaks that principle unless you're using Basic Auth (which is often combined with HTTPS and short-lived use). |
| 🚫 **Poor user experience** | Harder to manage sessions, logout, and token expiration; risks login throttling or lockouts. |

---

### 🤔 Why **not** authenticate once and store something to identify the user in the server?

**❌ Traditional Server-Side Sessions**

* You log in once, and the server stores your session in memory or Redis
    
* On each request, the server checks the session ID in a cookie
    
* Requires **central session storage**, which **doesn’t scale well** in distributed environments
    

**✅ JWT-Based Authentication**

* You log in once, receive a signed token
    
* Each request includes the token
    
* The server simply **verifies the signature and expiration**
    
* **No need to hit a DB or session store**, making it fast and scalable
    

# Spring Security

Spring Security is a powerful framework for handling **authentication (who are you?)** and **authorization (what can you do?)** in Spring-based applications.

**Spring Security acts as a servlet filter chain that wraps every HTTP request**, intercepts it before it hits your controllers, and handles all security logic (auth, roles, CSRF, etc.) through standard Servlet APIs like `HttpServletRequest`, `HttpServletResponse`, and `Filter`.

> 📘 Docs: [Spring Security Reference](https://docs.spring.io/spring-security/reference/index.html)

Let’s unpack this a bit for better understanding:

## ☕ What is a **Servlet**?

A **Servlet** is a Java class that handles **HTTP requests** and generates **HTTP responses** in a web application.

It’s part of the **Java EE / Jakarta EE standard** and runs inside a **Servlet container** (like **Tomcat**, which Spring Boot uses under the hood).

### 🧱 Think of a Servlet as:

> A Java class that gets triggered when a client (like a browser or API client) hits a specific URL.

In modern apps (like Spring Boot), **you don’t write raw Servlets anymore** — the framework does it for you, and you define `@RestController` endpoints instead.

## 🔎 What is a **Servlet Filter**?

A **Servlet Filter** is a reusable class that can **intercept and modify** both the request and the response **before and after** they reach the Servlet (or controller in Spring).

> Think of a filter as a **security checkpoint** or **middleware layer**.

### 🔁 Filter Workflow

1. The client sends a request.
    
2. The request goes through **one or more Filters**.
    
3. Then it hits the target **Servlet** (or Spring controller).
    
4. The response also passes **back through the filters**.
    

# Configure JWT with Spring Security

Now that we understand the basics let’s get to work :):

What are we building? We will create a security layer for our REST API that will use JWT and Spring Security to authenticate the user. We will secure all of our endpoints so that they only work if we provide the JWT token.

Here are the main things we will set up:

* Provide an endpoint for the user to authenticate using a username and password (`/auth/login`)
    
* If credentials are valid, the server returns a **JWT**
    
* The client sends that token in future requests via an `Authorization: Bearer <token>` header
    
* The server validates the token and allows or denies access
    

### 🧱 1. Add relevant dependencies on your project:

* `spring-boot-starter-security`
    
* `jjwt` (JSON Web Token library)
    

#### `pom.xml` snippet:

```xml
<properties>
    <jwt.version>0.12.6</jwt.version>
</properties>

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
    </dependency>
    <dependency>
        <groupId>io.jsonwebtoken</groupId>
        <artifactId>jjwt-api</artifactId>
        <version>${jwt.version}</version>
    </dependency>
    <dependency>
        <groupId>io.jsonwebtoken</groupId>
        <artifactId>jjwt-impl</artifactId>
        <version>${jwt.version}</version>
        <scope>runtime</scope>
    </dependency>
    <dependency>
        <groupId>io.jsonwebtoken</groupId>
        <artifactId>jjwt-jackson</artifactId>
        <version>${jwt.version}</version>
        <scope>runtime</scope>
    </dependency>
</dependencies>
```

---

### 🔐 2. Configure the Secret Key

Add a Base64-encoded secret key to `application.properties`:

```bash
jwt.secret=k+gM7KJzkFqj5YfLUMNSgG6XnMvC7YyykWoM8NRCT7U=
```

> Tip: You can generate this types of keys by running: openssl rand -base64 32
> 
> ---

### ⚙️ 3. Create a JWT Configuration Bean

Let’s define a configuration class that loads the JWT secret from properties, decodes it, and exposes it as a Spring bean. This allows consistent, secure key reuse across the app for signing and validating tokens.

```bash
@Configuration
public class JwtConfig {
    @Value("${jwt.secret}")
    private String secret;

    @Bean
    public Key jwtSigningKey() {
        return Keys.hmacShaKeyFor(Decoders.BASE64.decode(secret));
    }
}
```

---

### 🔧 4. Create a JWT Utility Class

Next, let’s create`JwtUtil` class to encapsulate all JWT-related operations. This utility will centralize JWT logic, keeping the code clean and reusable across the application. The basic things it’ll do:

1. Use the signing key to generate tokens with a subject and expiration
    
2. Verify if a token is valid
    
3. Extract the username from a valid token.
    

```bash
@Component
public class JwtUtil {
    private final Key key;

    public JwtUtil(Key key) { //This is injected by SpringBoot
        this.key = key;
    }

    public String generateToken(String username) {
        return Jwts.builder()
                .setSubject(username)
                .setIssuedAt(new Date())
                .setExpiration(new Date(System.currentTimeMillis() + 3600000))
                .signWith(key)
                .compact();
    }

    public boolean isTokenValid(String token) {
        try {
            Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token);
            return true;
        } catch (Exception e) {
            return false;
        }
    }

    public String extractUsername(String token) {
        return Jwts.parserBuilder().setSigningKey(key).build()
                .parseClaimsJws(token).getBody().getSubject();
    }
}
```

---

### 🚪 5. Add an Authentication Controller

Now, let’s mplement an `AuthController` that expose a `/auth/login` endpoint to handle user authentication. It accepts a username and password, and if they match predefined credentials, it generates a JWT using the `JwtUtil` and returns it to the client. This token can then be used to access secured endpoints, serving as the entry point for the authentication flow in the application.

```bash
@RestController
@RequestMapping("/auth")
public class AuthController {
    private final JwtUtil jwtUtil;

    public AuthController(JwtUtil jwtUtil) {
        this.jwtUtil = jwtUtil;
    }

    @PostMapping("/login")
    public ResponseEntity<Map<String, String>> login(@RequestParam String username, @RequestParam String password) {
        if ("user".equals(username) && "password".equals(password)) {
            String token = jwtUtil.generateToken(username);
            return ResponseEntity.ok(Map.of("token", token));
        }
        return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
    }
}
```

> 💡 **Pro tips:**
> 
> * We hardcoded the username and password directly in the `AuthController` to keep the example simple and focused on demonstrating JWT generation. This approach is fine for learning and prototyping, but it's not secure or scalable for real applications. To make this authentication process more robust and production-ready, we can store users in a database, use a `UserDetailsService` to load user information, and securely hash passwords using `BCryptPasswordEncoder`. Additionally, implementing role-based access control and using a dedicated authentication service layer can further separate concerns and improve security and maintainability.
>     
> * While using `@RequestParam` for username and password is fine for demos, it's not recommended for production. In real-world applications, credentials should be sent in the **request body as JSON** using a `POST` request, not as query parameters. This approach is more secure, avoids logging sensitive data in URLs, and aligns with best practices for RESTful APIs.
>     

---

### 🧰 6. Create a JWT Authentication Filter

Next, we’ll create a `JwtAuthFilter` that extends `OncePerRequestFilter` to intercept incoming HTTP requests. It checks for the presence of a JWT in the `Authorization` header, validates the token using `JwtUtil`, and, if valid, sets the authentication context with the extracted username. This allows Spring Security to recognize the request as authenticated without needing to hit a database or session store.

```bash
public class JwtAuthFilter extends OncePerRequestFilter {
    private final JwtUtil jwtUtil;

    public JwtAuthFilter(JwtUtil jwtUtil) {
        this.jwtUtil = jwtUtil;
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {
        String authHeader = request.getHeader("Authorization");

        if (authHeader != null && authHeader.startsWith("Bearer ")) {
            String token = authHeader.substring(7);
            if (jwtUtil.isTokenValid(token)) {
                String username = jwtUtil.extractUsername(token);
                UsernamePasswordAuthenticationToken authentication =
                        new UsernamePasswordAuthenticationToken(username, null, List.of());
                SecurityContextHolder.getContext().setAuthentication(authentication);
            }
        }

        filterChain.doFilter(request, response); //Tells the filter chain to continue processing the request and response.
    }
}
```

---

### 🔐 7. Set your Security Configuration with Spring

Finally, let’s configure Spring Security to define how our application handles authentication and authorization. Since we are using token-based security with JWT we want to instruct Spring to do the following:

1- Disable CSRF protection (as it's unnecessary for stateless APIs),

2- Allow unauthenticated access to the `/auth/**` endpoints, and require authentication for all other routes.

3- Register our custom `JwtAuthFilter` and position it before Spring’s default `UsernamePasswordAuthenticationFilter`, ensuring that incoming requests are intercepted and validated using JWT before reaching protected resources.

```bash
@Configuration
@EnableWebSecurity
public class SecurityConfig {
    private final JwtUtil jwtUtil;

    public SecurityConfig(JwtUtil jwtUtil) {
        this.jwtUtil = jwtUtil;
    }

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        JwtAuthFilter jwtFilter = new JwtAuthFilter(jwtUtil);

        return http
                .csrf(AbstractHttpConfigurer::disable)
                .authorizeHttpRequests(auth -> auth
                        .requestMatchers("/auth/**").permitAll()
                        .anyRequest().authenticated())
                .addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class)
                .build();
    }
}
```

---

### ✅ 8. Test It

**Login:**

```bash
curl -X POST "http://localhost:8080/auth/login?username=user&password=password"
```

**Access a protected endpoint:**

```bash
curl -H "Authorization: Bearer <token>" http://localhost:8080/hello
```

---

🎉 Congrats — you’ve just built a stateless, token-based authentication system using Spring Boot and JWT! You now understand how authentication works under the hood, how to secure your endpoints, and how to structure your app for scalability and maintainability. This is a solid foundation for any modern backend application — great job! 💪🔥

## 🔒 **Pro Tips for Using JWT in Production**

### ✅ **1\. Always Use HTTPS**

JWTs are bearer tokens — **whoever holds them can use them**. Use **HTTPS** to prevent token interception via man-in-the-middle attacks. Never expose your API over plain HTTP in production.

### 🔐 **2\. Use Strong, Secure Keys**

* For HMAC (e.g., `HS256`), use at least a **256-bit** key (32+ bytes)
    
* For asymmetric signing (`RS256`), **store private keys securely** (e.g., AWS Secrets Manager, Vault)
    
* Avoid hardcoding secrets or storing them in Git. Use environment variables or a secrets manager.
    

### ⏳ **3\. Set Short Token Expiration Times**

* Keep JWTs **short-lived** (e.g., 15 minutes to 1 hour). ⏱️ The shorter the lifetime, the lower the risk if a token is compromised.
    
* Pair them with **refresh tokens** to maintain longer sessions securely
    

### 🛂 **4\. Validate Every Part of the JWT**

Always check:

* **Signature** (valid + untampered)
    
* **Expiration time** (`exp`)
    
* **Issuer / audience** (`iss`, `aud`) if used
    
* **User claims** if authorizing roles/permissions
    

### 🧼 **5\. Avoid Storing JWTs in LocalStorage (in SPAs)**

* Prefer **httpOnly secure cookies** for browser-based apps to reduce XSS risk
    
* If you must use `localStorage`, **guard against XSS** attacks
    

### ⚠️ 6\*\*. Never Include Sensitive Data in the JWT Payload\*\*

### 👀 **7\. Monitor and Log Auth Failures:**

Track repeated failed token validations — they could be **token reuse or tampering attempts**.

### 🔑 **8\. Rotate Keys Regularly**

Especially if you're using asymmetric algorithms (RS256/ES256), rotate your keys safely and support **key ID (**`kid`) headers to handle multiple signing keys.

### 👤 **9\. Use** `@PreAuthorize` and Roles

Once your app has users and roles:

* Use `@PreAuthorize("hasRole('ADMIN')")` to secure methods
    
* Add roles in JWT claims and validate them
    

**Finally, the code backing this article can be found on** [GitHub](https://github.com/mdjc/blog-posts-app/commit/cdaa7f80071f6d2898ecd749fd4d1afbe73523ac)**. And some minor adjustments were needed to resolve deprecation warnings.**
