A production-ready JWT Authentication Microservice built with Spring Boot 3.5, featuring role-based access control, Redis caching, rate limiting, and full Docker support.
┌─────────────────────────────────────────────────┐
│ Docker Network │
│ │
Client │ ┌─────────────────────────────────────────┐ │
│ │ │ authservice-app │ │
│ HTTP :8080 │ │ │ │
└───────────────────┼──► ┌──────────────┐ ┌────────────────┐ │ │
│ │ │ RateLimiter │ │ JwtAuthFilter │ │ │
│ │ │ Filter │ │ (OncePerReq.) │ │ │
│ │ └──────┬───────┘ └───────┬────────┘ │ │
│ │ │ │ │ │
│ │ ┌──────▼───────────────────▼────────┐ │ │
│ │ │ Controllers │ │ │
│ │ │ /auth/** /api/user /api/admin │ │ │
│ │ └──────────────────┬────────────────┘ │ │
│ │ │ │ │
│ │ ┌──────────────────▼────────────────┐ │ │
│ │ │ Service Layer │ │ │
│ │ │ UserRegister / UserLogin / │ │ │
│ │ │ AdminService (@Cacheable) │ │ │
│ │ └──────┬───────────────────┬────────┘ │ │
│ │ │ │ │ │
│ │ ┌──────▼──────┐ ┌───────▼────────┐ │ │
│ │ │ PostgreSQL │ │ Redis │ │ │
│ │ │ :5432 │ │ :6379 │ │ │
│ │ └─────────────┘ └────────────────┘ │ │
│ └─────────────────────────────────────────┘ │
└─────────────────────────────────────────────────┘
Request flow:
- Every request hits
RateLimiterFilterfirst —/auth/loginand/auth/registerare capped at 5 req/min per IP JwtAuthFilterextracts the Bearer token, validates it, and sets theSecurityContext- Spring Security enforces RBAC —
/api/user/**requiresROLE_USER,/api/admin/**requiresROLE_ADMIN - The service layer reads from Redis cache before hitting PostgreSQL (admin endpoints only)
┌─────────────────────────────────────────────────────────┐
│ users │
├──────────────────┬──────────────────────────────────────┤
│ id │ UUID (PK, gen_random_uuid()) │
│ email │ VARCHAR(255) NOT NULL UNIQUE │
│ password │ VARCHAR(255) NOT NULL (BCrypt) │
│ role │ VARCHAR(20) NOT NULL DEFAULT 'USER' │
│ salutation │ VARCHAR(10) NOT NULL (MR/MRS/OTHERS) │
│ first_name │ VARCHAR(100) NOT NULL │
│ last_name │ VARCHAR(100) NOT NULL │
│ phone_number │ VARCHAR(20) NOT NULL │
│ date_of_birth │ DATE NOT NULL │
│ created_at │ TIMESTAMP NOT NULL DEFAULT now() │
│ is_active │ BOOLEAN NOT NULL DEFAULT TRUE │
└──────────────────┴──────────────────────────────────────┘
Indexes
idx_user_email ON users(email)
Roles
USER — default on registration, access to /api/user/**
ADMIN — manually assigned, access to /api/admin/**
Notes
- Passwords are never stored in plain text (BCrypt, strength 10)
- Deletion is soft — is_active = false, record is never removed
- Migrations managed by Flyway (V1 → V3)
- On login, two tokens are issued:
- Access token — signed JWT (HS256), UUID as subject, expires in 15 minutes
- Refresh token — random UUID, stored in Redis, expires in 7 days
- When the access token expires, the client uses the refresh token to get a new pair silently
- Refresh tokens are rotated on every use — old token is deleted, new one is issued
- Logout deletes the refresh token from Redis, immediately invalidating the session
- The JWT secret is a Base64-encoded 256-bit key set via environment variable
JWT_SECRET - Every protected request must include
Authorization: Bearer <access_token>
| Path | Required role | Notes |
|---|---|---|
/auth/register |
None | Register |
/auth/login |
None | Login |
/auth/refresh |
None | Refresh access token |
/auth/logout |
ROLE_USER |
Invalidate refresh token |
/api/user/** |
ROLE_USER |
Authenticated users |
/api/admin/** |
ROLE_ADMIN |
Admin-only CRUD |
/actuator/** |
None | Health check (internal) |
/swagger-ui/** |
None | API docs |
- Applied to
/auth/login,/auth/register, and/auth/refresh - 5 requests per minute per IP (token bucket, greedy refill)
- Returns
429 Too Many Requestswhen the bucket is empty
- BCrypt with default strength (10 rounds)
- Minimum 8 characters enforced at the API layer
- No server-side sessions;
SessionCreationPolicy.STATELESS - Each request is independently authenticated via the JWT
curl -X POST http://localhost:8080/auth/register \
-H "Content-Type: application/json" \
-d '{
"salutation": "MR",
"firstName": "John",
"lastName": "Doe",
"email": "john.doe@example.com",
"password": "securepass123",
"phoneNumber": "+919876543210",
"dateOfBirth": "1995-06-15"
}'Response 201 Created:
{
"id": "a1b2c3d4-...",
"salutation": "MR",
"firstName": "John",
"lastName": "Doe",
"email": "john.doe@example.com",
"phoneNumber": "+919876543210",
"dateOfBirth": "1995-06-15",
"role": "USER"
}curl -X POST http://localhost:8080/auth/login \
-H "Content-Type: application/json" \
-d '{
"email": "john.doe@example.com",
"password": "securepass123"
}'Response 200 OK:
{
"accessToken": "eyJhbGciOiJIUzI1NiJ9...",
"refreshToken": "550e8400-e29b-41d4-a716-446655440000"
}curl -X POST http://localhost:8080/auth/refresh \
-H "Content-Type: application/json" \
-d '{"refreshToken": "<refreshToken>"}'Response 200 OK:
{
"accessToken": "eyJhbGciOiJIUzI1NiJ9...",
"refreshToken": "661f9511-f30c-52e5-b827-557766551111"
}curl -X POST http://localhost:8080/auth/logout \
-H "Authorization: Bearer <accessToken>" \
-H "Content-Type: application/json" \
-d '{"refreshToken": "<refreshToken>"}'Response 204 No Content
curl http://localhost:8080/api/user/userDetail \
-H "Authorization: Bearer <token>"curl http://localhost:8080/api/admin/user/{userId} \
-H "Authorization: Bearer <admin_token>"# All users, page 0, 10 per page
curl "http://localhost:8080/api/admin/user/query?page=0&pageSize=10" \
-H "Authorization: Bearer <admin_token>"
# Filter by role
curl "http://localhost:8080/api/admin/user/query?role=USER&page=0&pageSize=5" \
-H "Authorization: Bearer <admin_token>"Response 200 OK:
{
"users": [ { "id": "...", "firstName": "John", "role": "USER", ... } ],
"page": 0,
"pageSize": 10,
"totalUsers": 42
}curl -X PATCH http://localhost:8080/api/admin/user/{userId} \
-H "Authorization: Bearer <admin_token>" \
-H "Content-Type: application/json" \
-d '{
"firstName": "Jane",
"role": "ADMIN"
}'curl -X DELETE http://localhost:8080/api/admin/user/{userId} \
-H "Authorization: Bearer <admin_token>"Response 204 No Content
curl http://localhost:8080/actuator/healthPrerequisites: Docker and Docker Compose installed.
-
Clone the repo
-
Create a
.envfile in the project root:JWT_SECRET=<base64-encoded-256-bit-key>Generate a key:
openssl rand -base64 32 -
Start all services:
docker compose up -d
-
The app is available at
http://localhost:8080 -
Stop everything:
docker compose down
Prerequisites: Java 17, Maven, PostgreSQL 16, Redis 7.
- Create a PostgreSQL database named
authservice - Set environment variables or edit
application.yaml:SPRING_DATASOURCE_URL=jdbc:postgresql://localhost:5432/authservice SPRING_DATASOURCE_USERNAME=postgres SPRING_DATASOURCE_PASSWORD=yourpassword REDIS_HOST=localhost JWT_SECRET=<base64-encoded-256-bit-key> - Run:
./mvnw spring-boot:run
Available at http://localhost:8080/swagger-ui/index.html — use the lock icon to paste your Bearer token.
The project includes 62 unit tests covering services, security, controllers, and exception handling. Tests use JUnit 5, Mockito, AssertJ, and Spring MockMvc.
./mvnw testsrc/test/java/com/subhayan/authservice/
├── service/
│ ├── UserRegisterTest.java (4 tests)
│ ├── UserLoginTest.java (3 tests)
│ └── AdminServiceTest.java (9 tests)
├── security/
│ ├── JwtUtilTest.java (5 tests)
│ ├── CustomUserDetailsServiceTest.java (4 tests)
│ ├── RefreshTokenServiceTest.java (3 tests)
│ ├── RateLimiterFilterTest.java (3 tests)
│ └── JwtAuthFilterTest.java (4 tests)
├── controller/
│ ├── AuthRegisterControllerTest.java (4 tests)
│ ├── AuthLoginControllerTest.java (3 tests)
│ ├── AuthTokenControllerTest.java (4 tests)
│ ├── AdminControllerTest.java (7 tests)
│ ├── UserControllerTest.java (3 tests)
│ └── TestSecurityConfig.java
├── exception/
│ └── GlobalExceptionHandlerTest.java (5 tests)
└── AuthServiceApplicationTests.java (1 test)
| Layer | What's tested |
|---|---|
| Services | Registration (happy path, duplicate email, age validation), login (valid credentials, unknown email, wrong password), admin CRUD (get/query/update/delete, null-safe partial updates, soft delete) |
| Security | JWT generation/parsing/validation/expiry/tampering, user details loading by email and ID, refresh token Redis lifecycle, rate limiting (pass-through, 429 on excess), JWT filter (auth context setup, skip when missing/invalid/already authenticated) |
| Controllers | HTTP status codes, request/response mapping, bean validation (blank/invalid fields), role-based access (ADMIN-only endpoints return 403 for USER, 401 for unauthenticated) |
| Exceptions | All 5 @ExceptionHandler methods (409, 401, 400, 400, 500) with correct status codes and messages |
- Service/security tests use
@ExtendWith(MockitoExtension.class)with@Mock/@InjectMocksfor pure unit testing without Spring context - Controller tests use
@WebMvcTestwithMockMvcfor HTTP-layer testing, with aTestSecurityConfigthat mirrors production RBAC rules without the custom servlet filters - Custom filters (
RateLimiterFilter,JwtAuthFilter) are excluded from controller test component scanning to avoid interference, and tested independently - Redis operations are mocked via
StringRedisTemplate— no running Redis instance needed - No database required — all repository calls are mocked
| Layer | Technology |
|---|---|
| Framework | Spring Boot 3.5.10 |
| Language | Java 17 |
| Database | PostgreSQL 16 |
| Migrations | Flyway |
| ORM | Spring Data JPA / Hibernate |
| Security | Spring Security (stateless) |
| JWT | jjwt 0.12.6 (HS256) |
| Cache | Redis 7 + Spring Cache |
| Rate Limiting | Bucket4j 8.10.1 |
| API Docs | SpringDoc OpenAPI 2.8.14 (Swagger) |
| Health | Spring Boot Actuator |
| Boilerplate | Lombok |
| Containerization | Docker + Docker Compose |
| Testing | JUnit 5, Mockito, AssertJ, MockMvc |
#AI generated documentation only.