Security Handling
Secure a REST API with Spring Security and JWT authentication
What You Will Learn
Security is crucial for any application. In this practical work, you'll learn:
- Authentication - Verifying who the user is (login)
- Authorization - What the user is allowed to do (permissions)
- JWT Tokens - Stateless authentication tokens
- Password Hashing - Securely storing passwords
- Role-Based Access - Different permissions for different users
Understanding Security Concepts
Think of entering a secure building:
- Authentication: Showing your ID badge at the door (proving who you are)
- Authorization: Your badge only opens certain doors (what you can access)
JSON Web Token (JWT) is like a movie ticket:
- It contains information (your seat, movie, time)
- It's signed (can't be forged)
- It has an expiration (valid only for that show)
- You present it to gain access (no need to check a database each time)
JWT Structure:
HEADER.PAYLOAD.SIGNATURE
│ │ │
│ │ └── Cryptographic signature (prevents tampering)
│ │
│ └── Data: {"sub":"john","role":"ADMIN","exp":1234567890}
│
└── Algorithm: {"alg":"HS256","typ":"JWT"}
Example:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJzdWIiOiJqb2huIiwicm9sZSI6IkFETUlOIiwiZXhwIjoxMjM0NTY3ODkwfQ.
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
Prerequisites
- Java 17+ and Maven installed
- Completed Practical Work 4 (REST APIs basics)
- Basic understanding of HTTP headers
Part 1: Project Setup
Step 1.1: Create Project Structure
secure-api/
├── pom.xml
└── src/
└── main/
├── java/com/example/secureapi/
│ ├── SecureApiApplication.java
│ ├── config/
│ ├── controller/
│ ├── dto/
│ ├── entity/
│ ├── repository/
│ ├── security/
│ └── service/
└── resources/
└── application.properties
Step 1.2: Create pom.xml
<?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
http://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.2.3</version>
</parent>
<groupId>com.example</groupId>
<artifactId>secure-api</artifactId>
<version>1.0.0-SNAPSHOT</version>
<properties>
<java.version>17</java.version>
</properties>
<dependencies>
<!-- (#1:Web for REST endpoints) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- (#2:Spring Security) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- (#3:JPA for user storage) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<!-- (#4:H2 database) -->
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<!-- (#5:JWT library) -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.12.5</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.12.5</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.12.5</version>
<scope>runtime</scope>
</dependency>
<!-- (#6:Validation) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
Step 1.3: Create application.properties
# Server
server.port=8080
# Database
spring.datasource.url=jdbc:h2:mem:securedb
spring.datasource.driverClassName=org.h2.Driver
spring.jpa.hibernate.ddl-auto=create-drop
spring.h2.console.enabled=true
spring.h2.console.path=/h2-console
# JWT Configuration (#1:CHANGE THIS IN PRODUCTION!)
jwt.secret=mySecretKeyForJWTTokenGenerationAndValidationMustBeLongEnough256Bits
jwt.expiration=86400000
In production, NEVER hardcode secrets! Use environment variables:
jwt.secret=${JWT_SECRET}
Step 1.4: Create Main Application
Create src/main/java/com/example/secureapi/SecureApiApplication.java:
package com.example.secureapi;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class SecureApiApplication {
public static void main(String[] args) {
SpringApplication.run(SecureApiApplication.class, args);
}
}
Part 2: Create User Entity
Step 2.1: Create Role Enum
Create src/main/java/com/example/secureapi/entity/Role.java:
package com.example.secureapi.entity;
public enum Role {
USER, // Regular user
ADMIN // Administrator with full access
}
Step 2.2: Create User Entity
Create src/main/java/com/example/secureapi/entity/User.java:
package com.example.secureapi.entity;
import jakarta.persistence.*;
@Entity
@Table(name = "users")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(unique = true, nullable = false)
private String username;
@Column(nullable = false)
private String password; // (#1:Will store HASHED password)
@Column(nullable = false)
private String email;
@Enumerated(EnumType.STRING)
private Role role = Role.USER;
// Constructors
public User() {}
public User(String username, String password, String email, Role role) {
this.username = username;
this.password = password;
this.email = email;
this.role = role;
}
// Getters and Setters
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getUsername() { return username; }
public void setUsername(String username) { this.username = username; }
public String getPassword() { return password; }
public void setPassword(String password) { this.password = password; }
public String getEmail() { return email; }
public void setEmail(String email) { this.email = email; }
public Role getRole() { return role; }
public void setRole(Role role) { this.role = role; }
}
Step 2.3: Create User Repository
Create src/main/java/com/example/secureapi/repository/UserRepository.java:
package com.example.secureapi.repository;
import com.example.secureapi.entity.User;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;
public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByUsername(String username);
boolean existsByUsername(String username);
boolean existsByEmail(String email);
}
Part 3: Create JWT Service
Step 3.1: Create JWT Token Provider
Create src/main/java/com/example/secureapi/security/JwtTokenProvider.java:
package com.example.secureapi.security;
import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;
import javax.crypto.SecretKey;
import java.nio.charset.StandardCharsets;
import java.util.Date;
@Component
public class JwtTokenProvider {
private final SecretKey key;
private final long expiration;
public JwtTokenProvider(
@Value("${jwt.secret}") String secret,
@Value("${jwt.expiration}") long expiration) {
// (#1:Create signing key from secret)
this.key = Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8));
this.expiration = expiration;
}
// (#2:Generate JWT token)
public String generateToken(Authentication authentication) {
UserDetails userDetails = (UserDetails) authentication.getPrincipal();
Date now = new Date();
Date expiryDate = new Date(now.getTime() + expiration);
return Jwts.builder()
.subject(userDetails.getUsername()) // (#3:Who this token is for)
.issuedAt(now) // (#4:When it was created)
.expiration(expiryDate) // (#5:When it expires)
.signWith(key) // (#6:Sign with our secret)
.compact();
}
// (#7:Extract username from token)
public String getUsernameFromToken(String token) {
Claims claims = Jwts.parser()
.verifyWith(key)
.build()
.parseSignedClaims(token)
.getPayload();
return claims.getSubject();
}
// (#8:Check if token is valid)
public boolean validateToken(String token) {
try {
Jwts.parser()
.verifyWith(key)
.build()
.parseSignedClaims(token);
return true;
} catch (JwtException | IllegalArgumentException e) {
return false; // Token is invalid or expired
}
}
}
Part 4: Create Security Filter
Step 4.1: Create UserDetailsService
Create src/main/java/com/example/secureapi/security/CustomUserDetailsService.java:
package com.example.secureapi.security;
import com.example.secureapi.entity.User;
import com.example.secureapi.repository.UserRepository;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.*;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class CustomUserDetailsService implements UserDetailsService {
private final UserRepository userRepository;
public CustomUserDetailsService(UserRepository userRepository) {
this.userRepository = userRepository;
}
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userRepository.findByUsername(username)
.orElseThrow(() -> new UsernameNotFoundException("User not found: " + username));
// (#1:Convert our User to Spring Security's UserDetails)
return new org.springframework.security.core.userdetails.User(
user.getUsername(),
user.getPassword(),
List.of(new SimpleGrantedAuthority("ROLE_" + user.getRole().name()))
);
}
}
Step 4.2: Create JWT Authentication Filter
Create src/main/java/com/example/secureapi/security/JwtAuthenticationFilter.java:
package com.example.secureapi.security;
import jakarta.servlet.*;
import jakarta.servlet.http.*;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final JwtTokenProvider tokenProvider;
private final CustomUserDetailsService userDetailsService;
public JwtAuthenticationFilter(JwtTokenProvider tokenProvider,
CustomUserDetailsService userDetailsService) {
this.tokenProvider = tokenProvider;
this.userDetailsService = userDetailsService;
}
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain)
throws ServletException, IOException {
// (#1:Get token from Authorization header)
String token = getTokenFromRequest(request);
// (#2:Validate token and set authentication)
if (token != null && tokenProvider.validateToken(token)) {
String username = tokenProvider.getUsernameFromToken(token);
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
// (#3:Create authentication object)
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
// (#4:Set in SecurityContext - now the request is authenticated!)
SecurityContextHolder.getContext().setAuthentication(authentication);
}
filterChain.doFilter(request, response);
}
private String getTokenFromRequest(HttpServletRequest request) {
String bearerToken = request.getHeader("Authorization");
// (#5:Token format: "Bearer eyJhbGciOiJI..."
if (bearerToken != null && bearerToken.startsWith("Bearer ")) {
return bearerToken.substring(7);
}
return null;
}
}
Part 5: Configure Security
Step 5.1: Create Security Configuration
Create src/main/java/com/example/secureapi/config/SecurityConfig.java:
package com.example.secureapi.config;
import com.example.secureapi.security.JwtAuthenticationFilter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@Configuration
@EnableWebSecurity
@EnableMethodSecurity // (#1:Enable @PreAuthorize annotations)
public class SecurityConfig {
private final JwtAuthenticationFilter jwtAuthenticationFilter;
public SecurityConfig(JwtAuthenticationFilter jwtAuthenticationFilter) {
this.jwtAuthenticationFilter = jwtAuthenticationFilter;
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
// (#2:Disable CSRF for REST API)
.csrf(csrf -> csrf.disable())
// (#3:Stateless session - no server-side sessions)
.sessionManagement(session ->
session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
// (#4:Define which endpoints need authentication)
.authorizeHttpRequests(auth -> auth
// Public endpoints
.requestMatchers("/api/auth/**").permitAll() // (#5:Login/register)
.requestMatchers("/h2-console/**").permitAll()
// Admin only endpoints
.requestMatchers("/api/admin/**").hasRole("ADMIN")
// All other endpoints require authentication
.anyRequest().authenticated()
)
// (#6:Add our JWT filter before Spring's default filter)
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
// (#7:Allow H2 console frames)
.headers(headers -> headers.frameOptions(frame -> frame.sameOrigin()));
return http.build();
}
// (#8:Password encoder - NEVER store plain text passwords!)
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public AuthenticationManager authenticationManager(
AuthenticationConfiguration authConfig) throws Exception {
return authConfig.getAuthenticationManager();
}
}
BCrypt is a password hashing algorithm. It:
- Converts "password123" → "$2a$10$N9qo8uLOickgx2ZMRZoMye..."
- Is one-way (can't reverse it)
- Includes a random "salt" (same password = different hash each time)
- Is intentionally slow (makes brute-force attacks harder)
Part 6: Create Authentication Controller
Step 6.1: Create DTOs
Create src/main/java/com/example/secureapi/dto/LoginRequest.java:
package com.example.secureapi.dto;
import jakarta.validation.constraints.NotBlank;
public class LoginRequest {
@NotBlank(message = "Username is required")
private String username;
@NotBlank(message = "Password is required")
private String password;
// Getters and Setters
public String getUsername() { return username; }
public void setUsername(String username) { this.username = username; }
public String getPassword() { return password; }
public void setPassword(String password) { this.password = password; }
}
Create src/main/java/com/example/secureapi/dto/RegisterRequest.java:
package com.example.secureapi.dto;
import jakarta.validation.constraints.*;
public class RegisterRequest {
@NotBlank(message = "Username is required")
@Size(min = 3, max = 50)
private String username;
@NotBlank(message = "Password is required")
@Size(min = 6, message = "Password must be at least 6 characters")
private String password;
@NotBlank(message = "Email is required")
@Email(message = "Invalid email format")
private String email;
// Getters and Setters
public String getUsername() { return username; }
public void setUsername(String username) { this.username = username; }
public String getPassword() { return password; }
public void setPassword(String password) { this.password = password; }
public String getEmail() { return email; }
public void setEmail(String email) { this.email = email; }
}
Create src/main/java/com/example/secureapi/dto/AuthResponse.java:
package com.example.secureapi.dto;
public class AuthResponse {
private String token;
private String type = "Bearer";
private String username;
private String role;
public AuthResponse(String token, String username, String role) {
this.token = token;
this.username = username;
this.role = role;
}
// Getters
public String getToken() { return token; }
public String getType() { return type; }
public String getUsername() { return username; }
public String getRole() { return role; }
}
Step 6.2: Create Auth Controller
Create src/main/java/com/example/secureapi/controller/AuthController.java:
package com.example.secureapi.controller;
import com.example.secureapi.dto.*;
import com.example.secureapi.entity.*;
import com.example.secureapi.repository.UserRepository;
import com.example.secureapi.security.JwtTokenProvider;
import jakarta.validation.Valid;
import org.springframework.http.*;
import org.springframework.security.authentication.*;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.web.bind.annotation.*;
import java.util.Map;
@RestController
@RequestMapping("/api/auth")
public class AuthController {
private final AuthenticationManager authenticationManager;
private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;
private final JwtTokenProvider tokenProvider;
public AuthController(AuthenticationManager authenticationManager,
UserRepository userRepository,
PasswordEncoder passwordEncoder,
JwtTokenProvider tokenProvider) {
this.authenticationManager = authenticationManager;
this.userRepository = userRepository;
this.passwordEncoder = passwordEncoder;
this.tokenProvider = tokenProvider;
}
// (#1:Register new user)
@PostMapping("/register")
public ResponseEntity<?> register(@Valid @RequestBody RegisterRequest request) {
// Check if username exists
if (userRepository.existsByUsername(request.getUsername())) {
return ResponseEntity.badRequest()
.body(Map.of("error", "Username already exists"));
}
// Check if email exists
if (userRepository.existsByEmail(request.getEmail())) {
return ResponseEntity.badRequest()
.body(Map.of("error", "Email already exists"));
}
// (#2:Create user with HASHED password)
User user = new User();
user.setUsername(request.getUsername());
user.setPassword(passwordEncoder.encode(request.getPassword())); // HASH!
user.setEmail(request.getEmail());
user.setRole(Role.USER);
userRepository.save(user);
return ResponseEntity.status(HttpStatus.CREATED)
.body(Map.of("message", "User registered successfully"));
}
// (#3:Login and get JWT token)
@PostMapping("/login")
public ResponseEntity<?> login(@Valid @RequestBody LoginRequest request) {
try {
// (#4:Authenticate user)
Authentication authentication = authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(
request.getUsername(),
request.getPassword()
)
);
SecurityContextHolder.getContext().setAuthentication(authentication);
// (#5:Generate JWT token)
String token = tokenProvider.generateToken(authentication);
// Get user role
User user = userRepository.findByUsername(request.getUsername()).orElseThrow();
return ResponseEntity.ok(new AuthResponse(token, user.getUsername(), user.getRole().name()));
} catch (BadCredentialsException e) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
.body(Map.of("error", "Invalid username or password"));
}
}
}
Part 7: Create Protected Endpoints
Step 7.1: Create User Controller
Create src/main/java/com/example/secureapi/controller/UserController.java:
package com.example.secureapi.controller;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.web.bind.annotation.*;
import java.util.Map;
@RestController
@RequestMapping("/api/users")
public class UserController {
// (#1:Any authenticated user can access)
@GetMapping("/me")
public Map<String, Object> getCurrentUser(@AuthenticationPrincipal UserDetails user) {
return Map.of(
"username", user.getUsername(),
"authorities", user.getAuthorities()
);
}
// (#2:Just a test endpoint)
@GetMapping("/hello")
public Map<String, String> hello() {
return Map.of("message", "Hello, authenticated user!");
}
}
Step 7.2: Create Admin Controller
Create src/main/java/com/example/secureapi/controller/AdminController.java:
package com.example.secureapi.controller;
import com.example.secureapi.entity.User;
import com.example.secureapi.repository.UserRepository;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import java.util.*;
@RestController
@RequestMapping("/api/admin")
public class AdminController {
private final UserRepository userRepository;
public AdminController(UserRepository userRepository) {
this.userRepository = userRepository;
}
// (#1:Only ADMIN can access - configured in SecurityConfig)
@GetMapping("/users")
public List<Map<String, Object>> getAllUsers() {
return userRepository.findAll().stream()
.map(user -> Map.<String, Object>of(
"id", user.getId(),
"username", user.getUsername(),
"email", user.getEmail(),
"role", user.getRole()
))
.toList();
}
// (#2:Alternative: Use @PreAuthorize annotation)
@PreAuthorize("hasRole('ADMIN')")
@GetMapping("/dashboard")
public Map<String, Object> adminDashboard() {
long userCount = userRepository.count();
return Map.of(
"message", "Welcome to Admin Dashboard",
"totalUsers", userCount
);
}
}
Part 8: Add Sample Data
Step 8.1: Create Data Initializer
Create src/main/java/com/example/secureapi/config/DataInitializer.java:
package com.example.secureapi.config;
import com.example.secureapi.entity.*;
import com.example.secureapi.repository.UserRepository;
import org.springframework.boot.CommandLineRunner;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;
@Component
public class DataInitializer implements CommandLineRunner {
private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;
public DataInitializer(UserRepository userRepository, PasswordEncoder passwordEncoder) {
this.userRepository = userRepository;
this.passwordEncoder = passwordEncoder;
}
@Override
public void run(String... args) {
// Create admin user
if (!userRepository.existsByUsername("admin")) {
User admin = new User();
admin.setUsername("admin");
admin.setPassword(passwordEncoder.encode("admin123"));
admin.setEmail("admin@example.com");
admin.setRole(Role.ADMIN);
userRepository.save(admin);
System.out.println("Admin user created: admin / admin123");
}
// Create regular user
if (!userRepository.existsByUsername("user")) {
User user = new User();
user.setUsername("user");
user.setPassword(passwordEncoder.encode("user123"));
user.setEmail("user@example.com");
user.setRole(Role.USER);
userRepository.save(user);
System.out.println("Regular user created: user / user123");
}
}
}
Part 9: Test the API
Step 9.1: Start the Application
cd secure-api
mvn spring-boot:run
Step 9.2: Test Authentication Flow
1. Try accessing protected endpoint (should fail):
curl http://localhost:8080/api/users/hello
Response: 403 Forbidden (no token)
2. Register a new user:
curl -X POST http://localhost:8080/api/auth/register \
-H "Content-Type: application/json" \
-d '{"username":"john","password":"password123","email":"john@example.com"}'
3. Login to get JWT token:
curl -X POST http://localhost:8080/api/auth/login \
-H "Content-Type: application/json" \
-d '{"username":"john","password":"password123"}'
Response:
{
"token": "eyJhbGciOiJIUzI1NiJ9...",
"type": "Bearer",
"username": "john",
"role": "USER"
}
4. Access protected endpoint with token:
# Replace TOKEN with the actual token from login response
curl http://localhost:8080/api/users/hello \
-H "Authorization: Bearer TOKEN"
Response: {"message":"Hello, authenticated user!"}
5. Try admin endpoint with regular user (should fail):
curl http://localhost:8080/api/admin/users \
-H "Authorization: Bearer TOKEN"
Response: 403 Forbidden (not an admin)
6. Login as admin:
curl -X POST http://localhost:8080/api/auth/login \
-H "Content-Type: application/json" \
-d '{"username":"admin","password":"admin123"}'
7. Access admin endpoint with admin token:
curl http://localhost:8080/api/admin/users \
-H "Authorization: Bearer ADMIN_TOKEN"
Response: List of all users
Deliverables Checklist
- Application starts without errors
- Can register new users
- Can login and receive JWT token
- Protected endpoints require valid token
- Invalid tokens are rejected
- Admin endpoints only accessible to admins
- Regular users cannot access admin endpoints
- Passwords are stored hashed (check H2 console)
Security Best Practices Summary
- NEVER store plain text passwords - always hash with BCrypt
- Keep JWT secrets long and secure (use env variables)
- Set reasonable token expiration times
- Use HTTPS in production
- Validate all user input
- Don't expose sensitive info in error messages
- Implement rate limiting for login attempts
- Log security events for monitoring
Bonus Challenges
Implement a refresh token mechanism so users don't need to login again when their JWT expires.
Add forgot password functionality with email verification.
Add rate limiting to the login endpoint to prevent brute force attacks.