A focused, production-oriented reference implementation of stateless JWT-based authentication and authorization built on Spring Boot 3 and Spring Security 6. The project demonstrates the complete security pipeline — from user registration and BCrypt-hashed credential storage through JWT issuance, per-request token validation, and protected resource access — without relying on HTTP sessions.
- Overview
- Architecture
- Technology Stack
- Features
- Project Structure
- Security Implementation
- API Documentation
- Database Design
- Configuration
- Getting Started
- Build & Deployment
- Error Handling
- Testing
- Engineering Highlights
- Future Improvements
Business Purpose
This application provides a self-contained, stateless authentication service that any front-end or service consumer can integrate with. Clients register an account, authenticate to receive a signed JWT, and then present that token on every subsequent request to access protected resources.
Problem Being Solved
Traditional session-based authentication requires server-side session storage, which complicates horizontal scaling and increases infrastructure coupling. This project eliminates session state entirely by encoding all identity information inside a cryptographically signed JWT, allowing any node in a cluster to authenticate a request independently.
Key Capabilities
| Capability | Detail |
|---|---|
| User self-registration | Password hashing via BCrypt (strength factor 12) before persistence |
| Stateless JWT login | HMAC-SHA-256 signed tokens issued on successful credential verification |
| Per-request JWT validation | Custom OncePerRequestFilter intercepts and validates every inbound token |
| Protected resource access | All endpoints except /register and /login require a valid Bearer token |
| In-memory student resource | A protected /students resource illustrates authorized data retrieval |
┌─────────────┐ HTTP ┌──────────────────────────────────────────────┐
│ │ ──────────► │ Spring Boot Application │
│ Client │ │ │
│ (Browser / │ ◄────────── │ ┌──────────┐ ┌──────────┐ ┌────────────┐ │
│ API Tool) │ JSON/JWT │ │Controller│ │ Service │ │ Repository │ │
└─────────────┘ │ └──────────┘ └──────────┘ └────────────┘ │
│ │ │
│ ┌──────▼──────┐ │
│ │ PostgreSQL │ │
│ └─────────────┘ │
└──────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────┐
│ Presentation Layer │
│ StudentController · UserController │
├──────────────────────────────────────────────────────┤
│ Security Layer │
│ JwtFilter · SecurityConfig · UserPrincipal │
├──────────────────────────────────────────────────────┤
│ Service Layer │
│ UserService · JWTService · MyUserDetailsService │
├──────────────────────────────────────────────────────┤
│ Repository Layer │
│ UserRepo │
├──────────────────────────────────────────────────────┤
│ Domain Layer │
│ Users · Student · UserPrincipal │
├──────────────────────────────────────────────────────┤
│ Infrastructure Layer │
│ PostgreSQL · Spring Data JPA │
└──────────────────────────────────────────────────────┘
sequenceDiagram
participant C as Client
participant JF as JwtFilter
participant SC as SecurityContext
participant Ctrl as Controller
participant Svc as Service
C->>JF: HTTP Request (Authorization: Bearer <token>)
JF->>JF: Extract token from Authorization header
JF->>JF: extractUserName(token)
JF->>Svc: loadUserByUsername(username)
Svc-->>JF: UserDetails
JF->>JF: validateToken(token, userDetails)
alt Token valid
JF->>SC: Set UsernamePasswordAuthenticationToken
JF->>Ctrl: Forward request
Ctrl-->>C: 200 OK + Response Body
else Token invalid / missing
JF->>Ctrl: Forward request (no SecurityContext set)
Ctrl-->>C: 401 Unauthorized (Spring Security)
end
sequenceDiagram
participant C as Client
participant UC as UserController
participant US as UserService
participant AM as AuthenticationManager
participant MUDS as MyUserDetailsService
participant JWTs as JWTService
participant DB as PostgreSQL
C->>UC: POST /login {username, password}
UC->>US: verify(user)
US->>AM: authenticate(UsernamePasswordAuthenticationToken)
AM->>MUDS: loadUserByUsername(username)
MUDS->>DB: findByUsername(username)
DB-->>MUDS: Users entity
MUDS-->>AM: UserPrincipal (UserDetails)
AM->>AM: BCrypt.matches(rawPassword, hashedPassword)
alt Credentials valid
AM-->>US: Authentication (isAuthenticated = true)
US->>JWTs: generateToken(username)
JWTs-->>US: Signed JWT string
US-->>UC: JWT string
UC-->>C: 200 OK — JWT token
else Credentials invalid
AM-->>US: AuthenticationException
US-->>UC: "fail"
UC-->>C: "fail"
end
sequenceDiagram
participant C as Client
participant UC as UserController
participant US as UserService
participant DB as PostgreSQL
C->>UC: POST /register {id, username, password}
UC->>US: register(user)
US->>US: BCryptPasswordEncoder(strength=12).encode(password)
US->>DB: save(user with hashed password)
DB-->>US: Persisted Users entity
US-->>UC: Users entity
UC-->>C: 200 OK — Users entity (with hashed password)
| Component | Technology | Version |
|---|---|---|
| Language | Java | 17 |
| Framework | Spring Boot | 3.5.3 |
| Security | Spring Security | 6.x (managed by Boot) |
| Persistence | Spring Data JPA | 3.x (managed by Boot) |
| ORM | Hibernate | 6.x (managed by Boot) |
| Database | PostgreSQL | Compatible with 42.7.7 driver |
| JDBC Driver | PostgreSQL JDBC Driver | 42.7.7 |
| JWT Library | JJWT (jjwt-api / jjwt-impl / jjwt-jackson) | 0.12.6 |
| Build Tool | Apache Maven (via Maven Wrapper) | 3.x |
| Testing | JUnit 5 + Spring Boot Test + Spring Security Test | Managed by Boot |
| Servlet API | Jakarta Servlet API | Provided by container |
| Java Crypto | javax.crypto.KeyGenerator (HmacSHA256) |
JDK built-in |
- User Registration — Accepts
{id, username, password}, hashes the password with BCrypt (cost factor 12), and persists the user to PostgreSQL. - JWT-Based Login — Authenticates credentials via Spring Security's
AuthenticationManagerand returns a signed JWT on success. - Stateless Session Management — Sessions are never created (
SessionCreationPolicy.STATELESS); every request is independently authenticated from its token. - Custom JWT Filter —
JwtFilter(extendsOncePerRequestFilter) inspects theAuthorizationheader on every request, validates the token, and populates theSecurityContext. - BCrypt Password Hashing — Registration encodes passwords with BCrypt at strength 12 before storage; no plaintext passwords are ever persisted.
- DAO Authentication Provider —
DaoAuthenticationProviderdelegates credential verification toMyUserDetailsService+ BCrypt comparator. - Protected Student Resource — A
/studentsendpoint (GET and POST) that is accessible only to authenticated callers, demonstrating resource protection. - CSRF Inspection Endpoint — A
/csrf-tokenutility endpoint (CSRF is disabled globally for stateless API use). - Per-Request HMAC-SHA256 Token Signing — JWTs are signed with a dynamically generated
HmacSHA256secret key created at application startup. - HTTP Basic fallback — HTTP Basic authentication is enabled as a secondary mechanism alongside JWT Bearer token support.
d:\WorkSpace\Java_WorkSpace\SpringSecurity
├── pom.xml # Maven build descriptor
├── mvnw / mvnw.cmd # Maven Wrapper scripts
├── src
│ ├── main
│ │ ├── java
│ │ │ └── com.ak.SpringSecutity
│ │ │ ├── SpringSecutityApplication.java # Bootstrap entry point
│ │ │ ├── config/
│ │ │ │ ├── SecurityConfig.java # Spring Security configuration
│ │ │ │ └── JwtFilter.java # JWT request interception filter
│ │ │ ├── controller/
│ │ │ │ ├── UserController.java # /register and /login endpoints
│ │ │ │ └── StudentController.java # Protected /students resource
│ │ │ ├── model/
│ │ │ │ ├── Users.java # JPA entity for persisted users
│ │ │ │ ├── UserPrincipal.java # Spring Security UserDetails adapter
│ │ │ │ └── Student.java # In-memory DTO for student data
│ │ │ ├── repo/
│ │ │ │ └── UserRepo.java # Spring Data JPA repository
│ │ │ └── service/
│ │ │ ├── UserService.java # Registration and login orchestration
│ │ │ ├── JWTService.java # Token generation and validation
│ │ │ └── MyUserDetailsService.java # UserDetailsService implementation
│ │ └── resources
│ │ └── application.properties # Application and datasource config
│ └── test
│ └── java
│ └── com.ak.SpringSecutity
│ └── SpringSecutityApplicationTests.java # Context load smoke test
└── target/ # Compiled output (gitignored)
| Package | Responsibility |
|---|---|
config |
Defines the SecurityFilterChain, authentication provider wiring, AuthenticationManager bean, and the JwtFilter that intercepts every HTTP request. |
controller |
Thin HTTP layer. UserController delegates registration and login to UserService. StudentController exposes the protected student resource. |
model |
Domain objects. Users is the JPA-managed entity. UserPrincipal is the Spring Security bridge (UserDetails). Student is an in-memory data class. |
repo |
Spring Data JPA repository for Users. Provides CRUD plus a derived findByUsername query. |
service |
Business logic. UserService orchestrates registration (hashing) and login (authentication + JWT issuance). JWTService encapsulates all JJWT operations. MyUserDetailsService is the UserDetailsService contract implementation. |
Authentication is performed via Spring Security's DaoAuthenticationProvider, which delegates to:
MyUserDetailsService.loadUserByUsername()— retrieves theUsersentity from PostgreSQL and wraps it in aUserPrincipal.BCryptPasswordEncoder(12)— compares the submitted raw password with the stored BCrypt hash.
Authorization is route-based. The SecurityFilterChain applies the following rules:
/register → permitAll() (public)
/login → permitAll() (public)
/** → authenticated() (requires valid JWT or HTTP Basic credentials)
┌────────────────────────────────────────────────────────────────────┐
│ JWTService │
│ │
│ Constructor: │
│ KeyGenerator("HmacSHA256") → SecretKey → Base64-encode → store │
│ │
│ generateToken(username): │
│ Jwts.builder() │
│ .subject(username) │
│ .issuedAt(now) │
│ .expiration(now + 300 minutes) │
│ .signWith(HmacSHA256 key) │
│ .compact() → JWT string │
│ │
│ validateToken(token, userDetails): │
│ extractUserName(token) == userDetails.getUsername() │
│ && !isTokenExpired(token) │
└────────────────────────────────────────────────────────────────────┘
Token lifetime: 300 minutes (18,000 seconds) from issuance.
Signing algorithm: HMAC-SHA-256.
Key lifecycle: Generated fresh each time the application starts (in-memory; not persisted). All previously issued tokens become invalid on restart.
| Property | Value |
|---|---|
| Algorithm | BCrypt |
| Cost factor (work factor) | 12 |
| Implementation | org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder |
| When applied | UserService.register() before repo.save() |
| Verification | DaoAuthenticationProvider via PasswordEncoder.matches() |
The filter chain executes in this order on every request:
Incoming Request
│
▼
JwtFilter (OncePerRequestFilter)
│ ── extracts Bearer token from Authorization header
│ ── validates token and sets SecurityContext
│
▼
UsernamePasswordAuthenticationFilter (Spring built-in)
│
▼
... (remaining Spring Security default filters)
│
▼
DispatcherServlet → Controller
JwtFilter is registered before UsernamePasswordAuthenticationFilter via http.addFilterBefore(...).
// CSRF disabled — stateless API, no cookie-based sessions
http.csrf(customizer -> customizer.disable());
// Route-level authorization
http.authorizeHttpRequests(request ->
request.requestMatchers("register", "login").permitAll()
.anyRequest().authenticated());
// HTTP Basic enabled as secondary authentication mechanism
http.httpBasic(Customizer.withDefaults());
// No sessions — each request carries its own credentials
http.sessionManagement(session ->
session.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
// JWT filter wired in before the username/password filter
http.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class);Every authenticated user is assigned a single authority: USER.
// UserPrincipal.java
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return Collections.singleton(new SimpleGrantedAuthority("USER"));
}No role-based differentiation is implemented at this stage; all authenticated principals are treated equally.
| Method | Endpoint | Description | Authentication Required | Request Body | Response |
|---|---|---|---|---|---|
POST |
/register |
Register a new user account. Password is BCrypt-hashed before persistence. | ❌ Public | {"id": 1, "username": "alice", "password": "secret"} |
Users entity (with hashed password) |
POST |
/login |
Authenticate with credentials. Returns a signed JWT on success. | ❌ Public | {"username": "alice", "password": "secret"} |
JWT string |
GET |
/students |
Retrieve the list of all students. | ✅ Bearer JWT | — | [{"id":1,"name":"Atharva","marks":100}, ...] |
POST |
/students |
Add a new student to the in-memory list. | ✅ Bearer JWT | {"id": 3, "name": "Ravi", "marks": 85} |
Student object |
GET |
/csrf-token |
Retrieve the current CSRF token (utility endpoint; CSRF is disabled globally). | ✅ Bearer JWT | — | CsrfToken object |
Registration
POST /register
Content-Type: application/json
{
"id": 1,
"username": "alice",
"password": "mysecretpassword"
}Login
POST /login
Content-Type: application/json
{
"username": "alice",
"password": "mysecretpassword"
}
# Response: eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJhbGljZSIsImlhdCI6...Authenticated Request
GET /students
Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJhbGljZSIs...| Column | Java Type | JPA Annotation | Constraint | Description |
|---|---|---|---|---|
id |
int |
@Id |
Primary Key, manually assigned | Unique user identifier |
username |
String |
— | None declared (effectively unique by lookup) | Login username |
password |
String |
— | Not null (enforced by BCrypt step) | BCrypt-hashed password |
Note:
@GeneratedValueis not used; the caller is responsible for supplying a uniqueidon registration.
| Field | Java Type | Description |
|---|---|---|
id |
int |
Student identifier |
name |
String |
Student name |
marks |
int |
Student score |
Student is managed entirely in a List inside StudentController and is not persisted to the database.
No inter-entity relationships exist at this stage. The Users entity is standalone.
Spring Data JPA with Hibernate will auto-derive the users table DDL at startup based on @Entity and @Id annotations. The effective schema is:
CREATE TABLE users (
id INTEGER NOT NULL,
username VARCHAR(255),
password VARCHAR(255),
PRIMARY KEY (id)
);# Application name
spring.application.name=SpringSecutity
# Default in-memory Spring Security user (used for HTTP Basic before database auth)
spring.security.user.name=atharva
spring.security.user.password=admin
# PostgreSQL datasource
spring.datasource.url=jdbc:postgresql://localhost:5432/SpringSecurityDB
spring.datasource.username=Atharva
spring.datasource.password=0000| Property | Description | Sensitivity |
|---|---|---|
spring.datasource.url |
JDBC URL for your PostgreSQL instance | Medium |
spring.datasource.username |
Database login username | Medium |
spring.datasource.password |
Database login password | High — externalize in production |
spring.security.user.name |
Default HTTP Basic username (development only) | Low |
spring.security.user.password |
Default HTTP Basic password (development only) | High — remove in production |
No spring.jpa.* or spring.jpa.hibernate.ddl-auto property is set explicitly. Hibernate's default behavior applies (none or validate, depending on the dialect's auto-detection). To auto-create the schema on first run, add:
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=trueThe HMAC-SHA256 secret key is generated at runtime inside the JWTService constructor using javax.crypto.KeyGenerator. It is not configurable through application.properties. This means:
- All tokens issued before an application restart become invalid after the restart.
- In a multi-instance deployment, different instances will generate different keys, making tokens non-portable across nodes.
For production, the secret key must be externalised (see Future Improvements).
| Requirement | Version |
|---|---|
| JDK | 17 or later |
| Apache Maven | 3.6+ (or use the included mvnw wrapper) |
| PostgreSQL | 13 or later |
# Clone the repository
git clone https://github.com/<your-org>/SpringSecurity.git
cd SpringSecurity- Create the PostgreSQL database:
CREATE DATABASE "SpringSecurityDB";
CREATE USER "Atharva" WITH PASSWORD '0000';
GRANT ALL PRIVILEGES ON DATABASE "SpringSecurityDB" TO "Atharva";- Update
src/main/resources/application.propertieswith your PostgreSQL credentials:
spring.datasource.url=jdbc:postgresql://localhost:5432/SpringSecurityDB
spring.datasource.username=<your-db-username>
spring.datasource.password=<your-db-password>
# Enable auto schema creation on first run
spring.jpa.hibernate.ddl-auto=update# Using the Maven Wrapper (recommended)
./mvnw spring-boot:run
# On Windows
mvnw.cmd spring-boot:run
# Or with a locally installed Maven
mvn spring-boot:runThe application starts on http://localhost:8080 by default.
# Run all tests
./mvnw test
# On Windows
mvnw.cmd test# Compile the source
./mvnw compile
# Run tests
./mvnw test
# Package into an executable JAR (skipping tests)
./mvnw package -DskipTests
# Clean previous builds and repackage
./mvnw clean packageThe executable JAR is produced at:
target/SpringSecutity-0.0.1-SNAPSHOT.jar
java -jar target/SpringSecutity-0.0.1-SNAPSHOT.jar| Concern | Recommendation |
|---|---|
| Secret key management | Inject the JWT signing key via environment variable or a secrets manager (Vault, AWS Secrets Manager). |
| Database credentials | Use environment variables or a secrets store; never commit credentials to source control. |
| DDL management | Disable ddl-auto in production; use Flyway or Liquibase for controlled schema migrations. |
| HTTPS | Place the application behind a TLS-terminating reverse proxy (nginx, AWS ALB). |
| Logging | Configure structured JSON logging for log aggregation pipelines. |
| Health checks | Add Spring Boot Actuator (/actuator/health) for load-balancer and Kubernetes probes. |
| Token invalidation | Implement a token revocation mechanism (e.g., deny-list in Redis) before deployment. |
| Multiple instances | Share the JWT signing key across all instances via external configuration. |
There is no custom @ControllerAdvice or @ExceptionHandler implemented in this version. Error handling is delegated to Spring Security's default mechanisms:
| Scenario | Behavior |
|---|---|
Missing or malformed Authorization header |
Request passes through JwtFilter without setting a SecurityContext. Spring Security returns 401 Unauthorized. |
| Expired JWT | JwtFilter.validateToken() returns false; SecurityContext is not populated; Spring Security returns 401 Unauthorized. |
| Invalid token signature | Jwts.parser().parseSignedClaims() throws a JwtException; the exception propagates and results in a 500 Internal Server Error (unhandled in current version). |
User not found during loadUserByUsername |
MyUserDetailsService throws UsernameNotFoundException, which Spring Security translates to 401 Unauthorized. |
Registration with duplicate id |
Hibernate/PostgreSQL throws a DataIntegrityViolationException, resulting in a 500 Internal Server Error (unhandled in current version). |
| Test Class | Type | What it Verifies |
|---|---|---|
SpringSecutityApplicationTests |
Integration (Spring Boot context load) | The complete Spring application context assembles without errors — all beans are wired correctly, the database connection is available, and auto-configuration succeeds. |
The contextLoads test is a smoke test that validates the entire dependency graph (security configuration, JWT filter, authentication provider, repository, and service beans) at startup time.
./mvnw test| Dependency | Purpose |
|---|---|
spring-boot-starter-test |
JUnit 5, Mockito, AssertJ, Spring Test utilities |
spring-security-test |
Security-aware mock MVC support, @WithMockUser, @WithSecurityContext |
- Separation of concerns — Security configuration, business logic, and data access are in clearly delineated packages. No cross-layer coupling.
- Adapter pattern —
UserPrincipalacts as an adapter between the domainUsersentity and Spring Security'sUserDetailscontract, keeping the domain model free of security framework dependencies. - Single Responsibility —
JWTServiceowns only token operations;UserServiceorchestrates only registration and login;MyUserDetailsServicehandles only user lookup.
- Stateless design —
SessionCreationPolicy.STATELESSensures no server-side session is ever created, which is appropriate for API-first architectures. - BCrypt with high cost factor — Using strength 12 (vs. the common default of 10) meaningfully increases resistance to offline brute-force attacks.
OncePerRequestFilter— Guarantees JWT validation runs exactly once per request, regardless of how many filter chains are involved.- Bearer token isolation — The
JwtFilteronly sets theSecurityContextwhen the token is valid; malformed or expired tokens result in a clean unauthenticated state. - CSRF disabled appropriately — CSRF protection is correctly disabled for a stateless REST API that does not rely on browser cookie-based sessions.
ApplicationContextlookup inJwtFilter—JwtFilterfetchesMyUserDetailsServicefrom theApplicationContextdirectly rather than injecting it as a field. This is an intentional design choice to avoid a circular dependency that would arise from Spring Security's bean initialization order whenUserDetailsServiceand theSecurityConfigare wired together.- Dynamic key generation — Generating the HMAC key at startup rather than hardcoding it prevents accidental secret exposure in source control, though it introduces the restart-invalidation trade-off documented above.
The following enterprise-grade enhancements are recommended for production readiness:
| Area | Improvement |
|---|---|
| JWT Key Management | Externalise the HMAC secret to application.properties (injected via environment variable or Vault) so the key is stable across restarts and consistent across cluster nodes. |
| Token Expiry | Make token TTL configurable via application.properties (app.jwt.expiration-ms). |
| Refresh Token | Implement a refresh token endpoint (/refresh) to issue new access tokens without re-authentication, reducing credential exposure. |
| Token Revocation | Implement a deny-list (backed by Redis) to invalidate tokens before expiry (e.g., on logout or password change). |
| Role-Based Access Control | Persist user roles/authorities in the database and enforce method-level security with @PreAuthorize annotations. |
| Database ID Generation | Replace the manually assigned int id in Users with @GeneratedValue(strategy = GenerationType.IDENTITY) to prevent duplicate-key errors. |
| Schema Migration | Integrate Flyway or Liquibase for version-controlled, repeatable database schema evolution. |
| Global Exception Handling | Add a @ControllerAdvice with @ExceptionHandler methods for JwtException, DataIntegrityViolationException, UsernameNotFoundException, and BadCredentialsException to return consistent, structured error responses. |
| DTO Layer | Introduce request/response DTOs (e.g., RegisterRequest, LoginResponse) to decouple the API contract from the JPA entity and avoid serialising the hashed password in the registration response. |
| Input Validation | Add Jakarta Bean Validation (@NotBlank, @Size) to request bodies and enable @Valid in controller method signatures. |
| Actuator & Observability | Add spring-boot-starter-actuator for health, metrics, and info endpoints; integrate with Prometheus/Grafana for operational visibility. |
| Containerisation | Add a Dockerfile and docker-compose.yml (with a PostgreSQL service) to enable one-command local setup and reproducible deployments. |
| CI/CD Pipeline | Add a GitHub Actions workflow for automated build, test, and Docker image publication on every push. |
| API Documentation | Integrate SpringDoc OpenAPI (springdoc-openapi-starter-webmvc-ui) to auto-generate and serve Swagger UI at /swagger-ui.html. |
| Test Coverage | Write unit tests for JWTService, UserService, and MyUserDetailsService using Mockito, and integration tests for the auth endpoints using MockMvc with @WithMockUser. |