Skip to content

Repository files navigation

Spring Boot 3 JWT Starter

Production-ready template with JWT auth, rate limiting (Bucket4j), Redis/Lettuce, Actuator, CORS, and Testcontainers.

Requirements

  • Java 21
  • Docker (for Testcontainers and local dev)

Configuration

Use environment variables (recommended via a .env file when running locally). See the list below and .env.example.

JWT

  • JWT_SECRET: Base64 encoded secret (HS512). Must decode to at least 64 bytes.
  • APP_JWT_VALIDITY_SECONDS: Access token TTL (seconds).
  • APP_JWT_REMEMBER_VALIDITY_SECONDS: Access token TTL when remember-me (seconds).
  • APP_JWT_ISSUER: JWT iss claim.
  • APP_JWT_AUDIENCE: JWT aud claim.
  • APP_JWT_CLOCK_SKEW_SECONDS: Allowed clock skew for parsing (seconds).
  • APP_JWT_REFRESH_TTL_DAYS: Refresh token TTL (days).

Password policy

  • APP_PASSWORD_REQUIRE_SPECIAL: true|false. When true, passwords must include a non-alphanumeric (special) character in addition to length, lower/upper and digit checks. When false (default), only length + lower/upper + digit are required.

CORS

  • APP_CORS_ALLOWED_ORIGINS: Allowed origins (e.g. * or https://example.com).

Rate limiting

  • APP_RATE_LIMIT_ENABLED: true|false.
  • APP_RATE_LIMIT_BACKEND: none|redis.
  • APP_RATE_LIMIT_AUTH_MAX_REQUESTS, APP_RATE_LIMIT_AUTH_WINDOW_SECONDS: Limits for /api/authenticate.
  • APP_RATE_LIMIT_REFRESH_MAX_REQUESTS, APP_RATE_LIMIT_REFRESH_WINDOW_SECONDS: Limits for /api/refresh.
  • APP_RATE_LIMIT_IN_MEMORY_RETENTION_SECONDS: TTL for in-memory counters cleanup when APP_RATE_LIMIT_BACKEND=none. Prevents unbounded growth of IP keys when Redis is not used. Default 900. Ignored when using Redis.

Redis (required when APP_RATE_LIMIT_BACKEND=redis)

  • REDIS_HOST, REDIS_PORT, REDIS_SSL.

Database

  • SPRING_DATASOURCE_URL, SPRING_DATASOURCE_USERNAME, SPRING_DATASOURCE_PASSWORD.

Run locally

./gradlew bootRun

The build task auto-loads .env for bootRun only.

Tests

./gradlew test

Tests default to APP_RATE_LIMIT_BACKEND=none. Tests that need Redis or DB start Testcontainers and set properties dynamically.

Docker (single image)

Build jar and image:

./gradlew bootJar
docker build -t mohirdev/jwt-app:latest .

Run container (supply env vars):

docker run --rm -p 8085:8085 --env-file .env mohirdev/jwt-app:latest

Docker Compose (app + Postgres + Redis)

Compose reads variables from .env by default.

# Build jar and image
./gradlew bootJar
# Start stack with .env
docker compose up --build -d
# Tail logs
docker compose logs -f app

Notes:

  • Postgres: postgres:17-alpine (latest major)
  • Redis: redis:7-alpine
  • App: built from local Dockerfile, exposes 8085

Example .env for Compose

SERVER_PORT=8085
SPRING_DATASOURCE_USERNAME=mohirdev
SPRING_DATASOURCE_PASSWORD=mohirdev
JWT_SECRET=PASTE_BASE64_64B_PLUS
APP_JWT_VALIDITY_SECONDS=3600
APP_JWT_REMEMBER_VALIDITY_SECONDS=86400
APP_JWT_ISSUER=mohirdev-auth
APP_JWT_AUDIENCE=mohirdev-api
APP_JWT_CLOCK_SKEW_SECONDS=30
APP_JWT_REFRESH_TTL_DAYS=14
APP_CORS_ALLOWED_ORIGINS=*
APP_RATE_LIMIT_ENABLED=true
APP_RATE_LIMIT_BACKEND=redis
APP_RATE_LIMIT_AUTH_MAX_REQUESTS=20
APP_RATE_LIMIT_AUTH_WINDOW_SECONDS=10
APP_RATE_LIMIT_REFRESH_MAX_REQUESTS=30
APP_RATE_LIMIT_REFRESH_WINDOW_SECONDS=10
APP_RATE_LIMIT_IN_MEMORY_RETENTION_SECONDS=900
# Password policy
APP_PASSWORD_REQUIRE_SPECIAL=false
REDIS_HOST=redis
REDIS_PORT=6379
REDIS_SSL=false

Endpoints

Public/auth endpoints:

  • POST /api/authenticate — authenticate with username/password. Returns access token and refresh token.
  • POST /api/refresh — rotate refresh token and return new access token (+new refresh token).
  • POST /api/logout — revoke a refresh token.

User endpoints:

  • POST /api/register — register a new user (public). Assigns ROLE_USER.
  • GET /api/users — list users (requires ROLE_ADMIN).

Actuator health (no auth required):

  • GET /actuator/health
  • GET /actuator/health/readiness — includes redis and db contributors; returns 503 if any is DOWN.

Swagger UI:

  • /swagger-ui.html and /swagger-ui/index.html
  • OpenAPI JSON: /v3/api-docs

Docker with local Postgres on host

When running the app container but using your locally installed Postgres, containers cannot use localhost to reach the host. Use host.docker.internal (Docker Desktop Windows/Mac).

Example .env for this mode:

SERVER_PORT=8085
SPRING_DATASOURCE_URL=jdbc:postgresql://host.docker.internal:5432/mohirdev_database
SPRING_DATASOURCE_USERNAME=YOUR_DB_USER
SPRING_DATASOURCE_PASSWORD=YOUR_DB_PASSWORD

# JWT (required; HS512 base64, decode >= 64 bytes)
JWT_SECRET=PASTE_BASE64_64B_PLUS
APP_JWT_VALIDITY_SECONDS=3600
APP_JWT_REMEMBER_VALIDITY_SECONDS=86400
APP_JWT_ISSUER=mohirdev-auth
APP_JWT_AUDIENCE=mohirdev-api
APP_JWT_CLOCK_SKEW_SECONDS=30
APP_JWT_REFRESH_TTL_DAYS=14

# CORS
APP_CORS_ALLOWED_ORIGINS=*

# Rate limit
APP_RATE_LIMIT_ENABLED=true
# If you do not run Redis locally, keep 'none'
APP_RATE_LIMIT_BACKEND=none
# If you run Redis locally, switch to 'redis' and set host:
# APP_RATE_LIMIT_BACKEND=redis
# REDIS_HOST=host.docker.internal
# REDIS_PORT=6379
# REDIS_SSL=false

APP_RATE_LIMIT_AUTH_MAX_REQUESTS=20
APP_RATE_LIMIT_AUTH_WINDOW_SECONDS=10
APP_RATE_LIMIT_REFRESH_MAX_REQUESTS=30
APP_RATE_LIMIT_REFRESH_WINDOW_SECONDS=10
# In-memory counter cleanup TTL (seconds), only used when backend=none
APP_RATE_LIMIT_IN_MEMORY_RETENTION_SECONDS=900

# Password policy: require a special (non-alphanumeric) character
APP_PASSWORD_REQUIRE_SPECIAL=false

Run:

docker run --rm -p 8085:8085 --env-file .env mohirdev/jwt-app:latest

.env.example

Copy as a starting point for local development. Adjust values as needed.

# Server
SERVER_PORT=8085

# Database
SPRING_DATASOURCE_URL=jdbc:postgresql://localhost:5432/mohirdev_database
SPRING_DATASOURCE_USERNAME=postgres
SPRING_DATASOURCE_PASSWORD=postgres

# JWT (HS512, base64; must decode >= 64 bytes)
JWT_SECRET=REPLACE_WITH_BASE64_64B_PLUS
APP_JWT_VALIDITY_SECONDS=3600
APP_JWT_REMEMBER_VALIDITY_SECONDS=86400
APP_JWT_ISSUER=mohirdev-auth
APP_JWT_AUDIENCE=mohirdev-api
APP_JWT_CLOCK_SKEW_SECONDS=30
APP_JWT_REFRESH_TTL_DAYS=14

# CORS
APP_CORS_ALLOWED_ORIGINS=*

# Rate limiting
APP_RATE_LIMIT_ENABLED=true
# Use 'none' to keep everything in-memory (no Redis required)
APP_RATE_LIMIT_BACKEND=none
APP_RATE_LIMIT_AUTH_MAX_REQUESTS=20
APP_RATE_LIMIT_AUTH_WINDOW_SECONDS=10
APP_RATE_LIMIT_REFRESH_MAX_REQUESTS=30
APP_RATE_LIMIT_REFRESH_WINDOW_SECONDS=10

# Redis (only needed when APP_RATE_LIMIT_BACKEND=redis)
REDIS_HOST=localhost
REDIS_PORT=6379
REDIS_SSL=false

# In-memory rate limit cleanup (seconds), used only when backend=none
APP_RATE_LIMIT_IN_MEMORY_RETENTION_SECONDS=900

# Password policy: require a special character in addition to lower/upper/digit
APP_PASSWORD_REQUIRE_SPECIAL=false

About

https://github.com/gayratjonr/spring-boot/ clone

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages