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)
- User sends email + password
- Backend validates credentials
- Backend generates JWT
- Client sends JWT in
Authorizationheader - 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 🚀