Spring Boot

Capstone – Article 7: Login API & JWT Authentication (Expense Tracker)

By Utility Zone · 2026-01-27T18:32:20.558224

1. Introduction

This article completes the authentication core of the Expense Tracker.

By the end of this article, you will have:

  • A login API
  • JWT token generation
  • Token validation on requests
  • Secured endpoints

This is one of the most important capstone articles.


2. Authentication Flow (High Level)

  1. User sends email + password
  2. Backend validates credentials
  3. Backend generates JWT
  4. Client sends JWT in Authorization header
  5. Backend validates JWT for every request

No sessions. Fully stateless.


3. Adding JWT Dependencies

Add to pom.xml:

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

4. JWT Configuration

4.1 Add JWT Properties

In application.properties:

jwt.secret=change-this-secret-key
jwt.expiration=3600000

✔ Secret should be externalized in real deployments
✔ 1 hour expiry for learning


5. Creating Login DTOs

5.1 LoginRequest

package com.example.expensetracker.dto;

import jakarta.validation.constraints.NotBlank;

public class LoginRequest {

    @NotBlank
    private String email;

    @NotBlank
    private String password;

    // getters and setters
}

5.2 LoginResponse

package com.example.expensetracker.dto;

public class LoginResponse {

    private String token;
    private String type = "Bearer";

    public LoginResponse(String token) {
        this.token = token;
    }

    // getters
}

6. JWT Utility Class

package com.example.expensetracker.security;

import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import java.util.Date;

@Component
public class JwtUtil {

    @Value("${jwt.secret}")
    private String secret;

    @Value("${jwt.expiration}")
    private long expiration;

    public String generateToken(String email) {
        return Jwts.builder()
                .setSubject(email)
                .setIssuedAt(new Date())
                .setExpiration(new Date(System.currentTimeMillis() + expiration))
                .signWith(Keys.hmacShaKeyFor(secret.getBytes()), SignatureAlgorithm.HS256)
                .compact();
    }

    public String extractEmail(String token) {
        return Jwts.parserBuilder()
                .setSigningKey(secret.getBytes())
                .build()
                .parseClaimsJws(token)
                .getBody()
                .getSubject();
    }
}

7. Creating Login API

7.1 AuthController

package com.example.expensetracker.controller;

import com.example.expensetracker.dto.*;
import com.example.expensetracker.entity.User;
import com.example.expensetracker.repository.UserRepository;
import com.example.expensetracker.security.JwtUtil;
import jakarta.validation.Valid;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/auth")
public class AuthController {

    private final UserRepository userRepository;
    private final PasswordEncoder passwordEncoder;
    private final JwtUtil jwtUtil;

    public AuthController(UserRepository userRepository,
                          PasswordEncoder passwordEncoder,
                          JwtUtil jwtUtil) {
        this.userRepository = userRepository;
        this.passwordEncoder = passwordEncoder;
        this.jwtUtil = jwtUtil;
    }

    @PostMapping("/login")
    public LoginResponse login(@Valid @RequestBody LoginRequest request) {

        User user = userRepository.findByEmail(request.getEmail())
                .orElseThrow(() -> new RuntimeException("Invalid credentials"));

        if (!passwordEncoder.matches(request.getPassword(), user.getPassword())) {
            throw new RuntimeException("Invalid credentials");
        }

        String token = jwtUtil.generateToken(user.getEmail());
        return new LoginResponse(token);
    }
}

8. JWT Authentication Filter

package com.example.expensetracker.security;

import jakarta.servlet.*;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.filter.OncePerRequestFilter;

import java.io.IOException;

public class JwtAuthFilter extends OncePerRequestFilter {

    private final JwtUtil jwtUtil;

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

    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                    jakarta.servlet.http.HttpServletResponse response,
                                    FilterChain filterChain)
            throws ServletException, IOException {

        String header = request.getHeader("Authorization");

        if (header != null && header.startsWith("Bearer ")) {
            String token = header.substring(7);
            String email = jwtUtil.extractEmail(token);

            JwtUserAuthentication authentication =
                    new JwtUserAuthentication(email);

            SecurityContextHolder.getContext().setAuthentication(authentication);
        }

        filterChain.doFilter(request, response);
    }
}

9. Security Configuration

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    private final JwtUtil jwtUtil;

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

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {

        http
            .csrf(csrf -> csrf.disable())
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/auth/**").permitAll()
                .anyRequest().authenticated()
            )
            .addFilterBefore(new JwtAuthFilter(jwtUtil),
                    UsernamePasswordAuthenticationFilter.class);

        return http.build();
    }
}

10. Testing JWT Flow

Step 1: Login

POST /auth/login

Response:

{
  "token": "eyJhbGciOiJIUzI1NiJ9..."
}

Step 2: Access Secured API

Header:

Authorization: Bearer <token>

Without token → 401
With token → Access granted


11. Common JWT Mistakes

❌ Hardcoding secret
❌ Long token expiry
❌ Not validating token
❌ Mixing session + JWT


12. Git Commit (Important)

git add .
git commit -m "Add login API and JWT authentication"

13. What You Should Have Now

At this point:

  • Login API works
  • JWT is generated
  • APIs are secured
  • Authentication is stateless

This is interview gold.


14. What’s Next?

➡ Capstone – Article 8: Role-Based Authorization

  • USER vs ADMIN
  • Securing endpoints by role

Type Next when you’re ready 🚀