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
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
β
βββββββββββββββββ ββββββββββββββββββββββββββββββββββββββββββββββββ
β 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:
API chain (/auth/**, /api/**, /actuator/**) β stateless JWT, no sessions
Default chain (everything else) β OAuth2 Login + Form Login with sessions
Authorization Server β built-in Spring Authorization Server endpoints
Requirement
Minimum
JDK
21+
Docker & Docker Compose
latest
Gradle
9+ or use bundled ./gradlew
PostgreSQL
15+ (or via Docker)
Redis
7+ (or via Docker)
git clone https://github.com/yourusername/identity-service.git
cd identity-service
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
Or launch everything in Docker
docker compose up -d --build
The service will be available at http://localhost:8080
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
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 }
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
Method
Endpoint
Description
GET
/oauth2/authorization/google
Redirect to Google
GET
/oauth2/authorization/github
Redirect to GitHub
Method
Endpoint
Description
GET
/swagger-ui/index.html
Swagger UI
GET
/v3/api-docs
OpenAPI 3.0 spec (JSON)
GET
/actuator/health
Health check
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..."
{
"type" : " access" ,
"sub" : " 550e8400-e29b-41d4-a716-446655440000" ,
"roles" : [" ROLE_USER" ],
"iss" : " Identity Service" ,
"iat" : 1740240000 ,
"exp" : 1740240900 ,
"jti" : " unique-token-identifier"
}
{
"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)
ββββββββββββ ββββββββββββββββ βββββββββ
β 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 ββββ β
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
ββββββββββββ ββββββββββββββββ βββββββββ
β 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 βββββββββ β
ββββββββββββ ββββββββββββββββ βββββββββ
β 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 βββββββββββ β
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)
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);
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' );
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);
ββββββββββββββββ ββββββββββββββββ ββββββββββββββββ
β 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
BCrypt hashing with per-user salt
Passwords are never logged or returned in responses
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
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.
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
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
Profile
Use
default
Local development (localhost DB & Redis)
docker
Docker Compose network (postgres, redis hostnames)
logging :
level :
dev.mathalama.identityservice : DEBUG
org.springframework.security : INFO
# 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 .
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
# 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>
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
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
Fork the repository
Create a feature branch β git checkout -b feature/amazing-feature
Follow Clean Architecture layer boundaries
Add tests for new functionality
Commit β git commit -m 'Add amazing feature'
Push β git push origin feature/amazing-feature
Open a Pull Request
Distributed under the MIT License . See LICENSE for details.
Built with Spring Boot 4 & Java 21