Skip to content

mathalama/identity-service

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

32 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

Identity Service

Production-ready authentication & authorization microservice

Java Spring Boot PostgreSQL Redis Docker Gradle

Spring Security JWT OAuth2 Flyway Swagger License


Issues JWT access & refresh tokens, serves as an OAuth2 Authorization Server, and supports OAuth2 Login with third-party providers (Google, GitHub) - all backed by PostgreSQL and Redis.

Getting Started Β· API Reference Β· Architecture Β· Deployment


Table of Contents


Features

Feature Description
JWT Access + Refresh Tokens Short-lived access token + long-lived refresh token with automatic rotation
Token Blacklisting Instant access token revocation via Redis blacklist on logout
OAuth2 Authorization Server Full OIDC support with RSA-256 signing
OAuth2 Login Third-party authentication (Google, GitHub, and more)
OAuth Provider Linking Link multiple OAuth providers to a single account for unified authentication
User Management Registration, authentication, email / username login
User Profile Get current user details and manage linked authentication methods
Role-Based Access Control Multi-tier permissions β€” USER, ADMIN, SUPER_ADMIN
Email Verification Token-based verification with Redis storage (30 min TTL)
Resend Cooldown Rate-limited email resend (60 sec throttle)
Forgot Password Email-based password reset with time-limited Redis token
Logout & Revocation POST /auth/logout blacklists access token and revokes refresh token
Input Validation Jakarta Bean Validation on all DTOs (@NotBlank, @Email, @Size, @Pattern)
Clean Architecture Domain β†’ Application β†’ Infrastructure β†’ Presentation
Async Email Non-blocking sending via Spring TaskExecutor
Automatic Migrations Flyway-managed schema versioning
API Documentation Swagger UI with OpenAPI 3.0
CORS Support Configurable cross-origin access
Service-to-Service Auth POST /auth/validate for inter-microservice token validation

Technology Stack

Layer Technology Version
Language Java (OpenJDK) 21
Framework Spring Boot 4.0.3
Security Spring Security + OAuth2 Authorization Server + OAuth2 Client 6.x
ORM Spring Data JPA + Hibernate β€”
Database PostgreSQL 15+
Migrations Flyway β€”
Cache / Tokens Redis (Lettuce driver) 7+
JWT jjwt (io.jsonwebtoken) 0.12.3
API Docs SpringDoc OpenAPI 3.0.0
Build Gradle 9+
Container Docker & Docker Compose β€”

Architecture

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”      β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚   Frontend    │─────▢│         Identity Service (Spring Boot)       β”‚
β”‚ (React, etc.) β”‚      β”‚                                              β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜      β”‚  β‘  OAuth2 Authorization Server              β”‚
                       β”‚     /oauth2/**  /.well-known/**              β”‚
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”      β”‚     Issues OAuth2/OIDC tokens (RSA-256)     β”‚
β”‚ Microservices │─────▢│                                              β”‚
β”‚   (Backend)   β”‚      β”‚  β‘‘ JWT API β€” Stateless                      β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜      β”‚     /auth/**  /api/**                        β”‚
                       β”‚     Access + Refresh tokens (HMAC-SHA256)    β”‚
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”      β”‚     Redis token blacklist & refresh store    β”‚
β”‚ Google OAuth2 │◀────▢│                                              β”‚
β”‚ GitHub OAuth2 │◀────▢│  β‘’ OAuth2 Login + Form Login                β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜      β”‚     Session-based fallback auth              β”‚
                       β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                                      β”‚
                          β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                          β”‚                       β”‚
                    β”Œβ”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”          β”Œβ”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”
                    β”‚PostgreSQL β”‚          β”‚    Redis     β”‚
                    β”‚   15      β”‚          β”‚      7       β”‚
                    β”‚Users,Rolesβ”‚          β”‚Refresh tokensβ”‚
                    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜          β”‚Access blackl.β”‚
                                           β”‚Verify tokensβ”‚
                                           β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

The service exposes three security filter chains ordered by priority:

  1. API chain (/auth/**, /api/**, /actuator/**) β€” stateless JWT, no sessions
  2. Default chain (everything else) β€” OAuth2 Login + Form Login with sessions
  3. Authorization Server β€” built-in Spring Authorization Server endpoints

Prerequisites

Requirement Minimum
JDK 21+
Docker & Docker Compose latest
Gradle 9+ or use bundled ./gradlew
PostgreSQL 15+ (or via Docker)
Redis 7+ (or via Docker)

Quick Start

1. Clone the repository

git clone https://github.com/yourusername/identity-service.git
cd identity-service

2. Create a .env file

cat > .env << 'EOF'
POSTGRES_DB=identity_db
POSTGRES_USER=postgres
POSTGRES_PASSWORD=secure_password

JWT_SECRET=generate-with-openssl-rand-base64-32
JWT_EXPIRATION=900000
JWT_REFRESH_EXPIRATION=604800000

FRONTEND_URL=http://localhost:3000
BASE_URL=http://localhost:8080

REDIS_HOST=redis
REDIS_PORT=6379

SECURITY_USER=admin
SECURITY_PASSWORD=admin

GOOGLE_CLIENT_ID=your-client-id.apps.googleusercontent.com
GOOGLE_CLIENT_SECRET=your-client-secret
EOF

3. Start infrastructure only

docker compose up -d postgres redis

4. Run the application

./gradlew bootRun

Or launch everything in Docker

docker compose up -d --build

The service will be available at http://localhost:8080


Environment Variables

Variable Required Default Description
POSTGRES_DB βœ… β€” PostgreSQL database name
POSTGRES_USER βœ… β€” PostgreSQL username
POSTGRES_PASSWORD βœ… β€” PostgreSQL password
JWT_SECRET βœ… β€” HMAC key for JWT signing (min 32 chars)
JWT_EXPIRATION βœ… β€” Access token lifetime in ms (e.g. 900000 = 15 min)
JWT_REFRESH_EXPIRATION β€” 604800000 Refresh token lifetime in ms (default 7 days)
FRONTEND_URL βœ… β€” CORS allowed origin
BASE_URL βœ… β€” OAuth2 AS issuer URL
REDIS_HOST β€” localhost Redis hostname
REDIS_PORT β€” 6379 Redis port
REDIS_PASSWORD β€” (empty) Redis password
SECURITY_USER β€” β€” Spring Security default user
SECURITY_PASSWORD β€” β€” Spring Security default password
GOOGLE_CLIENT_ID β€” β€” Google OAuth2 Client ID
GOOGLE_CLIENT_SECRET β€” β€” Google OAuth2 Client Secret
GITHUB_CLIENT_ID β€” β€” GitHub OAuth2 Client ID
GITHUB_CLIENT_SECRET β€” β€” GitHub OAuth2 Client Secret

API Endpoints

Authentication (Public)

Method Endpoint Description Body
POST /auth/register Register new user { username, email, password }
POST /auth/authenticate Login β†’ access + refresh tokens { login, password }
POST /auth/refresh Refresh token pair (rotation) { refreshToken }
POST /auth/verify-email Verify email token { token }
POST /auth/resend-verification Resend verification { email }
POST /auth/forgot-password Request password reset email { email }
POST /auth/reset-forgotten-password Set new password via reset token { token, newPassword }

User Profile (Protected)

Method Endpoint Description
GET /auth/me Get current user profile
GET /auth/me/providers List linked OAuth providers
DELETE /auth/me/providers/{provider} Unlink OAuth provider

Account Management (Protected)

Method Endpoint Description
POST /auth/logout Revoke refresh token + blacklist access token
POST /auth/reset-password Change password

Service-to-Service (Internal)

Method Endpoint Description
POST /auth/validate Validate JWT token and get user info

OAuth2 Authorization Server

Method Endpoint Description
GET /.well-known/openid-configuration OIDC discovery
POST /oauth2/token Token endpoint
GET /oauth2/authorize Authorization endpoint
GET /oauth2/jwks JSON Web Key Set

OAuth2 Social Login

Method Endpoint Description
GET /oauth2/authorization/google Redirect to Google
GET /oauth2/authorization/github Redirect to GitHub

Docs & Health

Method Endpoint Description
GET /swagger-ui/index.html Swagger UI
GET /v3/api-docs OpenAPI 3.0 spec (JSON)
GET /actuator/health Health check

Usage Examples

Register a new user
curl -X POST http://localhost:8080/auth/register \
  -H "Content-Type: application/json" \
  -d '{
    "username": "john_doe",
    "email": "john@example.com",
    "password": "SecurePassword123!"
  }'
// 201 Created
{
  "message": "User registered successfully. Please check your email to verify your account."
}
Authenticate & get tokens
curl -X POST http://localhost:8080/auth/authenticate \
  -H "Content-Type: application/json" \
  -d '{
    "login": "john_doe",
    "password": "SecurePassword123!"
  }'
// 200 OK
{
  "accessToken": "eyJhbGciOiJIUzI1NiJ9.eyJ0eXBlIjoiYWNjZXNzIiwic3ViIjoiNTUw...",
  "refreshToken": "eyJhbGciOiJIUzI1NiJ9.eyJ0eXBlIjoicmVmcmVzaCIsInN1YiI6IjU1MC..."
}
Refresh tokens
curl -X POST http://localhost:8080/auth/refresh \
  -H "Content-Type: application/json" \
  -d '{
    "refreshToken": "eyJhbGciOiJIUzI1NiJ9.eyJ0eXBlIjoicmVmcmVzaCIs..."
  }'
// 200 OK β€” old refresh token is revoked, new pair issued
{
  "accessToken": "eyJhbGciOiJIUzI1NiJ9.eyJ0eXBlIjoiYWNjZXNzIiw...",
  "refreshToken": "eyJhbGciOiJIUzI1NiJ9.eyJ0eXBlIjoicmVmcmVzaCIs..."
}
Logout (revoke all tokens)
curl -X POST http://localhost:8080/auth/logout \
  -H "Authorization: Bearer eyJhbGciOiJIUzI1NiJ9..."
// 204 No Content
// Access token is blacklisted in Redis (TTL = remaining lifetime)
// Refresh token is deleted from Redis
Get current user profile
curl http://localhost:8080/auth/me \
  -H "Authorization: Bearer eyJhbGciOiJIUzI1NiJ9..."
// 200 OK
{
  "id": "550e8400-e29b-41d4-a716-446655440000",
  "username": "john_doe",
  "email": "john@example.com",
  "emailVerified": true,
  "accountState": "ACTIVE",
  "roles": ["ROLE_USER"],
  "createdAt": 1740240000000
}
List linked OAuth providers
curl http://localhost:8080/auth/me/providers \
  -H "Authorization: Bearer eyJhbGciOiJIUzI1NiJ9..."
// 200 OK
[
  {
    "id": "550e8400-e29b-41d4-a716-446655440001",
    "providerName": "GOOGLE",
    "providerEmail": "john@gmail.com",
    "linkedAt": 1740239000000,
    "lastLoginAt": 1740240000000
  },
  {
    "id": "550e8400-e29b-41d4-a716-446655440002",
    "providerName": "GITHUB",
    "providerEmail": "john_dev",
    "linkedAt": 1740238000000,
    "lastLoginAt": 1740235000000
  }
]
Unlink OAuth provider
curl -X DELETE http://localhost:8080/auth/me/providers/GOOGLE \
  -H "Authorization: Bearer eyJhbGciOiJIUzI1NiJ9..."
// 204 No Content
// Provider unlinked successfully
Validate token (service-to-service)
curl -X POST http://localhost:8080/auth/validate \
  -H "Content-Type: application/json" \
  -d '{
    "token": "eyJhbGciOiJIUzI1NiJ9.eyJ0eXBlIjoiYWNjZXNzIiw..."
  }'
// 200 OK (on success - check valid flag)
{
  "valid": true,
  "userId": "550e8400-e29b-41d4-a716-446655440000",
  "username": "john_doe",
  "email": "john@example.com",
  "roles": ["ROLE_USER"],
  "message": "Token is valid"
}
// 200 OK (on failure - check valid flag)
{
  "valid": false,
  "userId": null,
  "username": null,
  "email": null,
  "roles": [],
  "message": "Invalid or expired token"
}
Verify email
curl -X POST http://localhost:8080/auth/verify-email \
  -H "Content-Type: application/json" \
  -d '{
    "token": "550e8400-e29b-41d4-a716-446655440000"
  }'
// 200 OK
{
  "message": "Email verified successfully",
  "verified": true
}
Resend verification email
curl -X POST http://localhost:8080/auth/resend-verification \
  -H "Content-Type: application/json" \
  -d '{
    "email": "john@example.com"
  }'
// 200 OK
{
  "message": "Verification email sent successfully",
  "verified": false
}
Forgot password
curl -X POST http://localhost:8080/auth/forgot-password \
  -H "Content-Type: application/json" \
  -d '{
    "email": "john@example.com"
  }'
// 200 OK
{
  "message": "If the email exists, a password reset link has been sent."
}
Reset forgotten password
curl -X POST http://localhost:8080/auth/reset-forgotten-password \
  -H "Content-Type: application/json" \
  -d '{
    "token": "550e8400-e29b-41d4-a716-446655440000",
    "newPassword": "NewSecurePassword456!"
  }'
// 200 OK
{
  "message": "Password has been reset successfully. Please log in with your new password."
}
Call a protected endpoint
curl http://localhost:8080/api/some-endpoint \
  -H "Authorization: Bearer eyJhbGciOiJIUzI1NiJ9..."

JWT Token Structure

Access Token

{
  "type": "access",
  "sub": "550e8400-e29b-41d4-a716-446655440000",
  "roles": ["ROLE_USER"],
  "iss": "Identity Service",
  "iat": 1740240000,
  "exp": 1740240900,
  "jti": "unique-token-identifier"
}

Refresh Token

{
  "type": "refresh",
  "sub": "550e8400-e29b-41d4-a716-446655440000",
  "iss": "Identity Service",
  "iat": 1740240000,
  "exp": 1740844800,
  "jti": "unique-refresh-token-id"
}
Claim Description
type Token type: access or refresh
sub User UUID
roles Granted authorities (access token only)
iss Issuer identifier
iat Issued-at (Unix epoch)
exp Expiration (Unix epoch)
jti Unique token ID (used for blacklisting & refresh validation)

Token Lifecycle

 β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”          β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”          β”Œβ”€β”€β”€β”€β”€β”€β”€β”
 β”‚  Client  β”‚          β”‚Identity Svc  β”‚          β”‚ Redis β”‚
 β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”˜          β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜          β””β”€β”€β”€β”¬β”€β”€β”€β”˜
      β”‚                       β”‚                      β”‚
      β”‚  POST /authenticate   β”‚                      β”‚
      │──────────────────────▢│                      β”‚
      β”‚                       β”‚  Generate access     β”‚
      β”‚                       β”‚  Generate refresh    β”‚
      β”‚                       │── store jti ────────▢│ SET refresh:token:{userId} (7d)
      │◁── { accessToken,     β”‚                      β”‚
      β”‚      refreshToken } ──│                      β”‚
      β”‚                       β”‚                      β”‚
      β”‚  GET /api/** (Bearer) β”‚                      β”‚
      │──────────────────────▢│  validate signature  β”‚
      β”‚                       β”‚  check type=access   β”‚
      β”‚                       │── check blacklist ──▢│ EXISTS blacklist:access:{jti}
      β”‚                       │◁── false ───────────│
      │◁── 200 OK ───────────│                      β”‚
      β”‚                       β”‚                      β”‚
      ┆  (access token expired)                      β”‚
      β”‚                       β”‚                      β”‚
      β”‚  POST /auth/refresh   β”‚                      β”‚
      │──────────────────────▢│  parse refresh token β”‚
      β”‚                       │── validate jti ────▢│ GET refresh:token:{userId}
      β”‚                       │◁── matches ────────│
      β”‚                       │── DEL old refresh ─▢│
      β”‚                       β”‚  Generate new pair   β”‚
      β”‚                       │── store new jti ───▢│ SET refresh:token:{userId} (7d)
      │◁── { accessToken,     β”‚                      β”‚
      β”‚      refreshToken } ──│                      β”‚
      β”‚                       β”‚                      β”‚
      β”‚  POST /auth/logout    β”‚                      β”‚
      β”‚  (Bearer: access)     β”‚                      β”‚
      │──────────────────────▢│── blacklist access ─▢│ SET blacklist:access:{jti} (remaining TTL)
      β”‚                       │── DEL refresh ─────▢│ DEL refresh:token:{userId}
      │◁── 204 No Content ───│                      β”‚

Redis Key Schema

Key Pattern Value TTL Purpose
refresh:token:{userId} Refresh token jti 7 days Refresh token validation & rotation
blacklist:access:{jti} "revoked" Remaining access token lifetime Instant access token revocation
verify:token:{hash} User UUID 30 min Email verification
verify:cooldown:{userId} Timestamp 60 sec Resend rate limiting
reset:token:{hash} User UUID 30 min Password reset token
reset:cooldown:{userId} Timestamp 60 sec Password reset rate limiting

Email Verification Flow

 β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”          β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”          β”Œβ”€β”€β”€β”€β”€β”€β”€β”
 β”‚  Client  β”‚          β”‚Identity Svc  β”‚          β”‚ Redis β”‚
 β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”˜          β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜          β””β”€β”€β”€β”¬β”€β”€β”€β”˜
      β”‚  POST /auth/register  β”‚                      β”‚
      │──────────────────────▢│  Create user          β”‚
      β”‚                       β”‚  (PENDING_VERIFICATION)β”‚
      β”‚                       β”‚  Generate UUID token   β”‚
      β”‚                       │──── SHA-256 hash ────▢│ SETEX (30 min)
      β”‚                       β”‚  Send email (async)    β”‚
      │◁─── 201 Created ─────│                        β”‚
      β”‚                       β”‚                        β”‚
      β”‚  POST /auth/verify    β”‚                        β”‚
      │──────────────────────▢│  Hash incoming token   β”‚
      β”‚                       │──── GET ─────────────▢│
      β”‚                       │◁─── user_id ──────────│
      β”‚                       β”‚  Set ACTIVE            β”‚
      β”‚                       │──── DEL ─────────────▢│
      │◁─── 200 Verified ────│                        β”‚
      β”‚                       β”‚                        β”‚
      β”‚  POST /auth/resend    β”‚                        β”‚
      │──────────────────────▢│  Check cooldown (60s)  β”‚
      β”‚                       │──── GET cooldown ────▢│
      β”‚                       β”‚  Generate new token    β”‚
      β”‚                       │──── SETEX ───────────▢│
      β”‚                       β”‚  Send email (async)    β”‚
      │◁─── 200 Sent ────────│                        β”‚

Forgot Password Flow

 β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”          β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”          β”Œβ”€β”€β”€β”€β”€β”€β”€β”
 β”‚  Client  β”‚          β”‚Identity Svc  β”‚          β”‚ Redis β”‚
 β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”˜          β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜          β””β”€β”€β”€β”¬β”€β”€β”€β”˜
      β”‚  POST /auth/          β”‚                      β”‚
      β”‚  forgot-password      β”‚                      β”‚
      │──────────────────────▢│  Lookup user by email β”‚
      β”‚                       β”‚  Check cooldown (60s) β”‚
      β”‚                       │──── GET cooldown ───▢│
      β”‚                       β”‚  Generate UUID token  β”‚
      β”‚                       │──── SHA-256 hash ───▢│ SETEX (30 min)
      β”‚                       β”‚  Send email (async)   β”‚
      │◁─── 200 OK ──────────│                       β”‚
      β”‚                       β”‚                       β”‚
      β”‚  POST /auth/          β”‚                       β”‚
      β”‚  reset-forgotten-pwd  β”‚                       β”‚
      │──────────────────────▢│  Hash incoming token  β”‚
      β”‚                       │──── GET ────────────▢│
      β”‚                       │◁─── user_id ─────────│
      β”‚                       β”‚  Set new password     β”‚
      β”‚                       β”‚  (BCrypt)             β”‚
      β”‚                       │──── DEL token ──────▢│
      β”‚                       β”‚  Revoke refresh token β”‚
      β”‚                       │──── DEL refresh ────▢│
      │◁─── 200 OK ──────────│                       β”‚

Input Validation

All request DTOs are validated with Jakarta Bean Validation (spring-boot-starter-validation). Requests failing validation receive a 400 Bad Request response.

DTO Field Constraints
SignUpRegister username @NotBlank, @Size(3..50), @Pattern(^[a-zA-Z0-9_]+$)
email @NotBlank, @Email
password @NotBlank, @Size(8..128)
SignInRequest login @NotBlank
password @NotBlank
RefreshTokenRequest refreshToken @NotBlank
ResetPasswordRequest username @NotBlank
oldPassword @NotBlank
newPassword @NotBlank, @Size(8..128)
ForgotPasswordRequest email @NotBlank, @Email
NewPasswordRequest token @NotBlank
newPassword @NotBlank, @Size(8..128)
VerifyEmailRequest token @NotBlank
ResendVerificationRequest email @NotBlank, @Email
UpdateRequest username @NotBlank, @Size(3..50)
email @NotBlank, @Email
password @NotBlank, @Size(8..128)

Database Schema

users

CREATE TABLE users (
    id                        UUID PRIMARY KEY,
    username                  VARCHAR(255) NOT NULL UNIQUE,
    email                     VARCHAR(255) NOT NULL UNIQUE,
    password                  VARCHAR(255) NOT NULL,
    account_state             VARCHAR(50),
    security_status           VARCHAR(50),
    email_verified            BOOLEAN DEFAULT false,
    verified_at               TIMESTAMP,
    last_verification_sent_at TIMESTAMP,
    created_at                TIMESTAMP DEFAULT NOW()
);

CREATE INDEX idx_users_account_state  ON users(account_state);
CREATE INDEX idx_users_email          ON users(email);
CREATE INDEX idx_users_username       ON users(username);
CREATE INDEX idx_users_email_verified ON users(email_verified);

roles

CREATE TABLE roles (
    id   UUID PRIMARY KEY,
    name VARCHAR(255) NOT NULL UNIQUE
);

-- Seed data
INSERT INTO roles (name) VALUES ('ROLE_USER'), ('ROLE_ADMIN'), ('ROLE_SUPER_ADMIN');

users_roles (junction)

CREATE TABLE users_roles (
    user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
    role_id UUID NOT NULL REFERENCES roles(id) ON DELETE CASCADE,
    PRIMARY KEY (user_id, role_id)
);

oauth_providers (Multi-provider authentication)

CREATE TABLE oauth_providers (
    id            UUID PRIMARY KEY,
    user_id       UUID NOT NULL,
    provider_name VARCHAR(50) NOT NULL,
    provider_id   VARCHAR(500) NOT NULL,
    provider_email VARCHAR(255),
    created_at    TIMESTAMP DEFAULT NOW(),
    updated_at    TIMESTAMP DEFAULT NOW(),
    last_login_at TIMESTAMP,
    CONSTRAINT fk_oauth_user FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
    CONSTRAINT unique_provider_per_user UNIQUE (user_id, provider_name)
);

CREATE INDEX idx_oauth_provider_name ON oauth_providers(provider_name);
CREATE INDEX idx_oauth_provider_id ON oauth_providers(provider_id);
CREATE INDEX idx_oauth_user_id ON oauth_providers(user_id);

ER Diagram

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”       β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”       β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚    users     β”‚       β”‚ users_roles  β”‚       β”‚    roles     β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€       β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€       β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ id       (PK)│──┐    β”‚ user_id (FK) β”‚    β”Œβ”€β”€β”‚ id       (PK)β”‚
β”‚ username     β”‚  └───▢│ role_id (FK) β”‚β—€β”€β”€β”€β”˜  β”‚ name         β”‚
β”‚ email        β”‚       β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜       β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
β”‚ password     β”‚
β”‚ account_stateβ”‚  Enums: PENDING_VERIFICATION Β· ACTIVE Β· DISABLED Β· DELETED
β”‚ security_statβ”‚  Enums: MFA_REQUIRED β”‚ MFA_VERIFIED β”‚ PENDING β”‚ VERIFIED
β”‚ email_verifiedβ”‚
β”‚ created_at   β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
       β”‚
       β”‚ 1:N
       β”‚
β”Œβ”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ oauth_providers     β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ id            (PK)  β”‚
β”‚ provider_name       β”‚ (GOOGLE, GITHUB, etc)
β”‚ provider_id         β”‚ (Provider's unique ID)
β”‚ provider_email      β”‚ (Email from provider)
β”‚ last_login_at       β”‚ (Login tracking)
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Project Structure (Clean Architecture)

src/main/java/dev/mathalama/identityservice/
β”‚
β”œβ”€β”€ domain/                              ← Business logic & rules
β”‚   β”œβ”€β”€ entity/
β”‚   β”‚   β”œβ”€β”€ Users.java                     UserDetails aggregate root
β”‚   β”‚   β”œβ”€β”€ Role.java                      GrantedAuthority entity
β”‚   β”‚   └── OAuthProvider.java             OAuth provider link tracking
β”‚   β”œβ”€β”€ enums/
β”‚   β”‚   β”œβ”€β”€ AccountState.java              PENDING_VERIFICATION Β· ACTIVE Β· DISABLED Β· DELETED
β”‚   β”‚   └── SecurityStatus.java            MFA_REQUIRED Β· MFA_VERIFIED Β· PENDING Β· VERIFIED
β”‚   β”œβ”€β”€ repository/
β”‚   β”‚   └── OAuthProviderRepository.java    OAuth provider queries
β”‚   └── exception/
β”‚       β”œβ”€β”€ UnauthorizedException.java
β”‚       β”œβ”€β”€ UserAlreadyExistException.java
β”‚       └── UserNotFoundException.java
β”‚
β”œβ”€β”€ application/                         ← Use cases & DTOs
β”‚   β”œβ”€β”€ dto/
β”‚   β”‚   β”œβ”€β”€ auth/
β”‚   β”‚   β”‚   β”œβ”€β”€ AuthResponse.java           Access + refresh token pair
β”‚   β”‚   β”‚   β”œβ”€β”€ CurrentUserDto.java         Current user profile
β”‚   β”‚   β”‚   β”œβ”€β”€ OAuthProviderDto.java       OAuth provider link info
β”‚   β”‚   β”‚   β”œβ”€β”€ RefreshTokenRequest.java    Refresh token request
β”‚   β”‚   β”‚   β”œβ”€β”€ ForgotPasswordRequest.java  Forgot password request
β”‚   β”‚   β”‚   β”œβ”€β”€ NewPasswordRequest.java     Password reset via token
β”‚   β”‚   β”‚   β”œβ”€β”€ SignUpRegister.java         Registration request
β”‚   β”‚   β”‚   β”œβ”€β”€ SignInRequest.java          Login request
β”‚   β”‚   β”‚   β”œβ”€β”€ TokenValidationRequest.java Service-to-service token validation
β”‚   β”‚   β”‚   β”œβ”€β”€ TokenValidationResponse.java Token validation result
β”‚   β”‚   β”‚   β”œβ”€β”€ ResetPasswordRequest.java   Password change (authenticated)
β”‚   β”‚   β”‚   β”œβ”€β”€ VerifyEmailRequest.java     Email verification request
β”‚   β”‚   β”‚   β”œβ”€β”€ ResendVerificationRequest.java Resend request
β”‚   β”‚   β”‚   └── VerificationResponse.java   Verification status
β”‚   β”‚   └── user/
β”‚   β”‚       └── UpdateRequest.java          User profile update
β”‚   └── service/
β”‚       β”œβ”€β”€ AuthService.java                Interface β€” auth use cases
β”‚       β”œβ”€β”€ EmailService.java               Interface β€” email sending
β”‚       β”œβ”€β”€ JwtService.java                 Interface β€” JWT & token ops
β”‚       β”œβ”€β”€ VerificationTokenService.java   Interface β€” verification token mgmt
β”‚       β”œβ”€β”€ OAuthProviderService.java       Interface β€” OAuth provider management
β”‚       └── impl/
β”‚           └── AuthServiceImpl.java        Core auth logic
β”‚
β”œβ”€β”€ infrastructure/                      ← Frameworks & drivers
β”‚   β”œβ”€β”€ repository/
β”‚   β”‚   β”œβ”€β”€ UserRepository.java             Spring Data JPA
β”‚   β”‚   β”œβ”€β”€ RoleRepository.java             Spring Data JPA
β”‚   β”‚   └── OAuthProviderRepository.java    Spring Data JPA (OAuth queries)
β”‚   β”œβ”€β”€ service/
β”‚   β”‚   β”œβ”€β”€ EmailServiceImpl.java           Async email via TaskExecutor
β”‚   β”‚   β”œβ”€β”€ JwtServiceImpl.java             JWT gen/validation, refresh store, blacklist
β”‚   β”‚   └── VerificationTokenRedisService  Redis verification token storage
β”‚   └── config/
β”‚       β”œβ”€β”€ SecurityConfig.java             3 ordered filter chains
β”‚       β”œβ”€β”€ JwtAuthenticationFilter.java    Bearer β†’ SecurityContext (+ blacklist check)
β”‚       β”œβ”€β”€ AsyncConfig.java                Thread pool config
β”‚       β”œβ”€β”€ RedisConfig.java                Redis connection & serialization
β”‚       β”œβ”€β”€ PasswordConfig.java             BCryptPasswordEncoder
β”‚       β”œβ”€β”€ WebConfig.java                  CORS configuration
β”‚       └── FrontendProperties.java         @ConfigurationProperties
β”‚
β”œβ”€β”€ presentation/                        ← HTTP entry points
β”‚   └── controller/
β”‚       └── AuthController.java             /auth/** REST controller (15 endpoints)
β”‚
└── IdentityServiceApplication.java      ← Spring Boot main class

Security

Password Handling

  • BCrypt hashing with per-user salt
  • Passwords are never logged or returned in responses

JWT Token Security

  • Signed with HMAC-SHA256 (JWT_SECRET)
  • Access token: short-lived (default 15 min), carries user roles
  • Refresh token: long-lived (default 7 days), stored in Redis by jti
  • Refresh token rotation: each use invalidates the old token and issues a new pair
  • Access token blacklisting: on logout, access token jti is added to Redis with TTL = remaining lifetime
  • Payload: user UUID + roles β€” no sensitive data
  • Validated on every request by JwtAuthenticationFilter

Token Revocation Strategy

Scenario What Happens
Logout Access token blacklisted + refresh token deleted from Redis
Refresh Old refresh token deleted, new pair issued (rotation)
Reuse attack If an already-used refresh token is presented, all tokens for that user are revoked
Access token expires Naturally removed β€” no Redis cleanup needed
Blacklist entry expires Redis auto-deletes when access token would have expired anyway

Email Verification Tokens

  • Stored in Redis (not the database)
  • SHA-256 hashed before storage β€” raw token only exists in the email link
  • 30-minute TTL, one-time use, deleted after verification
  • Resend rate-limited to 60 seconds

Password Reset Tokens

  • Same Redis-based flow as email verification
  • SHA-256 hashed, 30-minute TTL, one-time use
  • On successful reset all refresh tokens are revoked (forces re-login)
  • Rate-limited to 60 seconds between requests

Account State Enforcement

AccountState isEnabled() isAccountNonLocked() isAccountNonExpired()
PENDING_VERIFICATION βœ… βœ… βœ…
ACTIVE βœ… βœ… βœ…
DISABLED βœ… ❌ βœ…
DELETED ❌ βœ… ❌

Spring Security checks these methods during authentication β€” disabled and deleted accounts cannot log in.

Authentication Pipeline

HTTP Request
  └─▢ JwtAuthenticationFilter
        β”œβ”€β”€ Extract Bearer token from Authorization header
        β”œβ”€β”€ Validate HMAC-SHA256 signature
        β”œβ”€β”€ Assert token not expired
        β”œβ”€β”€ Assert type = "access"
        β”œβ”€β”€ Check Redis blacklist (blacklist:access:{jti})
        β”œβ”€β”€ Parse user UUID + roles
        β”œβ”€β”€ Load User entity from DB
        └── Populate SecurityContext
              └─▢ @PreAuthorize / @Secured annotations enforce role checks

Configuration

Application Properties

Key configuration in application.yml:

spring:
  datasource:
    url: jdbc:postgresql://localhost:5432/${POSTGRES_DB}
  redis:
    host: ${REDIS_HOST:localhost}
    port: ${REDIS_PORT:6379}
  flyway:
    enabled: true
    locations: classpath:db/migration

jwt:
  secret: ${JWT_SECRET}
  expiration: ${JWT_EXPIRATION}
  refresh-expiration: ${JWT_REFRESH_EXPIRATION:604800000}

app:
  frontend:
    url: ${FRONTEND_URL:http://localhost:3000}
  verification:
    token-expiry-minutes: 30
    resend-cooldown-seconds: 60

Profiles

Profile Use
default Local development (localhost DB & Redis)
docker Docker Compose network (postgres, redis hostnames)

Logging

logging:
  level:
    dev.mathalama.identityservice: DEBUG
    org.springframework.security: INFO

Development

# Build
./gradlew clean build

# Run tests
./gradlew test

# Start locally (requires running Postgres + Redis)
./gradlew bootRun

# Build Docker image
docker build -t mathalama/identity-service:latest .

Deployment

Docker Compose (recommended for dev / staging)

docker compose up -d --build

This spins up three containers:

Container Image Port
identity-service Custom build 8080
identity-postgres postgres:15-alpine 5432
identity-redis redis:7-alpine 6379

Production Checklist

# Generate a strong JWT secret
openssl rand -base64 32

# Required env vars
JWT_SECRET=<generated_above>
JWT_EXPIRATION=900000
JWT_REFRESH_EXPIRATION=604800000
FRONTEND_URL=https://yourdomain.com
BASE_URL=https://api.yourdomain.com
POSTGRES_PASSWORD=<strong_password>
REDIS_PASSWORD=<strong_password>
  • Use external managed PostgreSQL & Redis
  • Enable TLS termination (reverse proxy / load balancer)
  • Rotate JWT_SECRET periodically
  • Set spring.jpa.show-sql=false
  • Enable Prometheus metrics scraping (/actuator/prometheus)
  • Set short access token TTL (JWT_EXPIRATION=900000 = 15 min)
  • Monitor Redis memory usage (blacklist entries auto-expire)

Monitoring & Health Checks

curl http://localhost:8080/actuator/health
{
  "status": "UP",
  "components": {
    "db":    { "status": "UP" },
    "redis": { "status": "UP" }
  }
}
Endpoint Purpose
/actuator/health Liveness & readiness
/actuator/prometheus Prometheus metrics

Troubleshooting

Problem Solution
JWT claims string is empty Ensure header is Authorization: Bearer <token> (note the space)
Token is not an access token You're sending a refresh token to an API endpoint β€” use the access token
Access token has been revoked User has logged out β€” re-authenticate or use refresh token first
Refresh token has been revoked or is invalid Refresh token was already used (rotation) or user logged out β€” re-authenticate
User not found Verify user exists and email is verified (account_state = ACTIVE)
Verification email was recently sent Wait 60 seconds before resending
Invalid token Token expired (30 min window) or already used
Account is disabled/deleted User account is DISABLED or DELETED β€” contact support
Forgot password email not received Verify email exists in system; check spam folder; wait 60s before re-requesting
Validation error (400) Check request body matches DTO constraints (see Input Validation)
Redis connection failed Check REDIS_HOST / REDIS_PORT and that Redis is running

Contributing

  1. Fork the repository
  2. Create a feature branch β€” git checkout -b feature/amazing-feature
  3. Follow Clean Architecture layer boundaries
  4. Add tests for new functionality
  5. Commit β€” git commit -m 'Add amazing feature'
  6. Push β€” git push origin feature/amazing-feature
  7. Open a Pull Request

License

Distributed under the MIT License. See LICENSE for details.


Built with Spring Boot 4 & Java 21

About

Identity & Authentication Microservice (Spring Boot, JWT, RabbitMQ)

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors