← Back to Advanced Java
Practical Work 8

Security Handling

Secure a REST API with Spring Security and JWT authentication

Duration4-5 hours
DifficultyIntermediate
Session8 - Security Handling

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

Authentication vs Authorization

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)
What is JWT?

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
Security Warning

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();
    }
}
What is BCrypt?

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

Challenge 1: Refresh Tokens

Implement a refresh token mechanism so users don't need to login again when their JWT expires.

Challenge 2: Password Reset

Add forgot password functionality with email verification.

Challenge 3: Rate Limiting

Add rate limiting to the login endpoint to prevent brute force attacks.