AuthForge — An open-source Identity & Access Management (IAM) platform. Think Keycloak, but modern: Spring Boot 3, Redis, Kafka, PostgreSQL, with first-class SDKs for React, Next.js, Python, and Laravel.
- OAuth2 / OpenID Connect (authorization code, password, refresh, client credentials)
- Multi-realm / multi-tenant support
- JWT access tokens + refresh token rotation
- SSO (Single Sign-On) with hosted login UI
- Role-Based Access Control (RBAC)
- Redis token cache (sub-ms validation)
- Kafka event bus (real-time auth events)
- Admin REST API + GraphQL
- Built-in analytics dashboard
- WebSocket real-time session monitoring
- SDK integrations: React, Next.js, Python, Laravel
You MUST generate the project with this exact top-level structure:
authforge/
├── build/ ← Gradle build output (generated, do not create)
├── build.gradle ← Root Gradle build file
├── gradle/
│ └── wrapper/
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew ← Unix Gradle wrapper script
├── gradlew.bat ← Windows Gradle wrapper script
├── settings.gradle ← Project name + submodule declarations
└── src/
├── main/
│ ├── java/
│ │ └── io/authforge/
│ │ ├── AuthForgeApplication.java
│ │ ├── config/
│ │ ├── controller/
│ │ ├── domain/
│ │ ├── dto/
│ │ ├── event/
│ │ ├── exception/
│ │ ├── repository/
│ │ ├── security/
│ │ └── service/
│ └── resources/
│ ├── application.yml
│ ├── application-dev.yml
│ ├── application-prod.yml
│ ├── db/migration/ ← Flyway SQL migrations
│ │ ├── V1__init_schema.sql
│ │ ├── V2__seed_master_realm.sql
│ │ └── V3__seed_default_roles.sql
│ ├── static/ ← Frontend assets (login UI, admin UI)
│ │ ├── css/
│ │ ├── js/
│ │ └── img/
│ └── templates/ ← Thymeleaf templates
│ ├── login.html
│ ├── register.html
│ └── error.html
└── test/
└── java/
└── io/authforge/
├── AuthForgeApplicationTests.java
├── controller/
└── service/
rootProject.name = 'authforge'plugins {
id 'java'
id 'org.springframework.boot' version '3.3.0'
id 'io.spring.dependency-management' version '1.1.4'
}
group = 'io.authforge'
version = '1.0.0'
sourceCompatibility = '21'
configurations {
compileOnly {
extendsFrom annotationProcessor
}
}
repositories {
mavenCentral()
}
dependencies {
// Spring Boot Core
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
implementation 'org.springframework.boot:spring-boot-starter-websocket'
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'org.springframework.boot:spring-boot-starter-actuator'
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
implementation 'org.springframework.boot:spring-boot-starter-mail'
implementation 'org.springframework.boot:spring-boot-starter-cache'
// OAuth2 / JWT
implementation 'org.springframework.boot:spring-boot-starter-oauth2-resource-server'
implementation 'org.springframework.security:spring-security-oauth2-authorization-server:1.2.3'
implementation 'io.jsonwebtoken:jjwt-api:0.12.5'
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.5'
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.12.5'
// Kafka
implementation 'org.springframework.kafka:spring-kafka'
// Database
runtimeOnly 'org.postgresql:postgresql'
implementation 'org.flywaydb:flyway-core'
implementation 'org.flywaydb:flyway-database-postgresql'
// GraphQL
implementation 'org.springframework.boot:spring-boot-starter-graphql'
// Redis
implementation 'org.springframework.session:spring-session-data-redis'
// Utilities
implementation 'org.mapstruct:mapstruct:1.5.5.Final'
annotationProcessor 'org.mapstruct:mapstruct-processor:1.5.5.Final'
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.4.0'
implementation 'com.github.ua-parser:uap-java:1.6.1'
// Testing
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.security:spring-security-test'
testImplementation 'org.springframework.kafka:spring-kafka-test'
testImplementation 'com.h2database:h2'
testImplementation 'org.testcontainers:junit-jupiter'
testImplementation 'org.testcontainers:postgresql'
testImplementation 'org.testcontainers:kafka'
}
tasks.named('test') {
useJUnitPlatform()
}distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists-- REALMS
CREATE TABLE realms (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name VARCHAR(100) UNIQUE NOT NULL,
display_name VARCHAR(200),
enabled BOOLEAN DEFAULT TRUE,
registration_allowed BOOLEAN DEFAULT TRUE,
access_token_lifespan INT DEFAULT 300,
refresh_token_lifespan INT DEFAULT 1800,
sso_session_idle INT DEFAULT 1800,
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
);
-- USERS
CREATE TABLE users (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
realm_id UUID NOT NULL REFERENCES realms(id) ON DELETE CASCADE,
username VARCHAR(150) NOT NULL,
email VARCHAR(255) NOT NULL,
password_hash VARCHAR(255) NOT NULL,
first_name VARCHAR(100),
last_name VARCHAR(100),
enabled BOOLEAN DEFAULT TRUE,
email_verified BOOLEAN DEFAULT FALSE,
last_login TIMESTAMP,
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW(),
UNIQUE(realm_id, username),
UNIQUE(realm_id, email)
);
-- ROLES
CREATE TABLE roles (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
realm_id UUID NOT NULL REFERENCES realms(id) ON DELETE CASCADE,
name VARCHAR(100) NOT NULL,
description TEXT,
is_composite BOOLEAN DEFAULT FALSE,
client_role BOOLEAN DEFAULT FALSE,
created_at TIMESTAMP DEFAULT NOW(),
UNIQUE(realm_id, name)
);
-- USER ROLES
CREATE TABLE user_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)
);
-- OAUTH2 CLIENTS
CREATE TABLE clients (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
realm_id UUID NOT NULL REFERENCES realms(id) ON DELETE CASCADE,
client_id VARCHAR(100) NOT NULL,
client_secret VARCHAR(255),
name VARCHAR(200),
description TEXT,
enabled BOOLEAN DEFAULT TRUE,
public_client BOOLEAN DEFAULT FALSE,
redirect_uris TEXT DEFAULT '[]',
web_origins TEXT DEFAULT '[]',
grant_types TEXT DEFAULT '["authorization_code","refresh_token"]',
created_at TIMESTAMP DEFAULT NOW(),
UNIQUE(realm_id, client_id)
);
-- SESSIONS (SSO)
CREATE TABLE sessions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
realm_id UUID NOT NULL REFERENCES realms(id) ON DELETE CASCADE,
client_id VARCHAR(100),
ip_address VARCHAR(45),
user_agent TEXT,
refresh_token TEXT UNIQUE,
expires_at TIMESTAMP NOT NULL,
created_at TIMESTAMP DEFAULT NOW(),
last_accessed TIMESTAMP DEFAULT NOW()
);
-- AUTHORIZATION CODES
CREATE TABLE auth_codes (
code VARCHAR(255) PRIMARY KEY,
client_id VARCHAR(100) NOT NULL,
user_id UUID NOT NULL,
realm_id UUID NOT NULL,
redirect_uri TEXT,
scope TEXT,
expires_at TIMESTAMP NOT NULL,
used BOOLEAN DEFAULT FALSE,
created_at TIMESTAMP DEFAULT NOW()
);
-- AUDIT LOG
CREATE TABLE audit_log (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
realm_id UUID,
user_id UUID,
event_type VARCHAR(100) NOT NULL,
client_id VARCHAR(100),
ip_address VARCHAR(45),
details JSONB,
created_at TIMESTAMP DEFAULT NOW()
);
-- Indexes
CREATE INDEX idx_users_realm ON users(realm_id);
CREATE INDEX idx_sessions_user ON sessions(user_id);
CREATE INDEX idx_sessions_refresh ON sessions(refresh_token);
CREATE INDEX idx_audit_realm ON audit_log(realm_id);
CREATE INDEX idx_audit_user ON audit_log(user_id);
CREATE INDEX idx_audit_event ON audit_log(event_type);spring:
application:
name: authforge
datasource:
url: jdbc:postgresql://localhost:5432/authforge
username: authforge
password: authforge_secret
driver-class-name: org.postgresql.Driver
hikari:
maximum-pool-size: 20
minimum-idle: 5
jpa:
hibernate:
ddl-auto: validate
show-sql: false
properties:
hibernate:
dialect: org.hibernate.dialect.PostgreSQLDialect
format_sql: true
flyway:
enabled: true
locations: classpath:db/migration
baseline-on-migrate: true
data:
redis:
host: localhost
port: 6379
password: ""
timeout: 2000ms
kafka:
bootstrap-servers: localhost:9092
producer:
key-serializer: org.apache.kafka.common.serialization.StringSerializer
value-serializer: org.springframework.kafka.support.serializer.JsonSerializer
consumer:
group-id: authforge-group
auto-offset-reset: earliest
key-deserializer: org.apache.kafka.common.serialization.StringDeserializer
value-deserializer: org.springframework.kafka.support.serializer.JsonDeserializer
session:
store-type: redis
timeout: 30m
graphql:
graphiql:
enabled: true
path: /graphiql
cache:
type: redis
authforge:
jwt:
secret: "CHANGE_ME_IN_PRODUCTION_USE_256_BIT_KEY"
issuer: "http://localhost:8080"
admin:
default-username: admin
default-password: admin
default-email: admin@localhost
token:
cache-ttl: 300
cors:
allowed-origins: "http://localhost:3000,http://localhost:5173"
server:
port: 8080
servlet:
context-path: /
management:
endpoints:
web:
exposure:
include: health,info,metrics,prometheus
endpoint:
health:
show-details: always
springdoc:
api-docs:
path: /api-docs
swagger-ui:
path: /swagger-ui.html@SpringBootApplication
@EnableCaching
@EnableScheduling
public class AuthForgeApplication {
public static void main(String[] args) {
SpringApplication.run(AuthForgeApplication.class, args);
}
}Create these entities with full Lombok + JPA annotations:
| Entity | Table | Key Fields |
|---|---|---|
Realm |
realms |
id, name, displayName, enabled, accessTokenLifespan |
User |
users |
id, realmId, username, email, passwordHash, enabled, emailVerified |
Role |
roles |
id, realmId, name, description, isComposite |
Client |
clients |
id, realmId, clientId, clientSecret, publicClient, redirectUris |
Session |
sessions |
id, userId, realmId, refreshToken, expiresAt |
AuthCode |
auth_codes |
code, clientId, userId, realmId, expiresAt, used |
AuditLog |
audit_log |
id, realmId, userId, eventType, ipAddress, details |
AuthController — handles all OAuth2/OIDC endpoints
GET /realms/{realm}/.well-known/openid-configuration
GET /realms/{realm}/protocol/openid-connect/certs
POST /realms/{realm}/protocol/openid-connect/token
GET /realms/{realm}/protocol/openid-connect/auth
POST /realms/{realm}/protocol/openid-connect/logout
GET /realms/{realm}/protocol/openid-connect/userinfo
POST /realms/{realm}/protocol/openid-connect/token/introspect
POST /realms/{realm}/register
AdminRealmController — realm CRUD
GET /admin/realms
POST /admin/realms
GET /admin/realms/{realm}
PUT /admin/realms/{realm}
DELETE /admin/realms/{realm}
AdminUserController — user management
GET /admin/realms/{realm}/users
POST /admin/realms/{realm}/users
GET /admin/realms/{realm}/users/{id}
PUT /admin/realms/{realm}/users/{id}
DELETE /admin/realms/{realm}/users/{id}
GET /admin/realms/{realm}/users/{id}/roles
POST /admin/realms/{realm}/users/{id}/roles
DELETE /admin/realms/{realm}/users/{id}/roles/{roleId}
GET /admin/realms/{realm}/users/{id}/sessions
DELETE /admin/realms/{realm}/users/{id}/sessions
AdminRoleController — role management
GET /admin/realms/{realm}/roles
POST /admin/realms/{realm}/roles
GET /admin/realms/{realm}/roles/{id}
PUT /admin/realms/{realm}/roles/{id}
DELETE /admin/realms/{realm}/roles/{id}
AdminClientController — OAuth2 client management
GET /admin/realms/{realm}/clients
POST /admin/realms/{realm}/clients
GET /admin/realms/{realm}/clients/{id}
PUT /admin/realms/{realm}/clients/{id}
DELETE /admin/realms/{realm}/clients/{id}
AdminSessionController — session monitoring
GET /admin/realms/{realm}/sessions
DELETE /admin/realms/{realm}/sessions/{id}
AdminAuditController — audit log
GET /admin/realms/{realm}/audit-log
AnalyticsController — metrics
GET /admin/realms/{realm}/analytics/overview
GET /admin/realms/{realm}/analytics/logins
GET /admin/realms/{realm}/analytics/active-sessions
| Service | Responsibility |
|---|---|
TokenService |
Generate + validate JWT. Redis cache for token validation. |
AuthService |
Login, register, logout. OAuth2 flows (password, auth_code, refresh, client_credentials). |
UserService |
CRUD, role assignment, password hashing (BCrypt strength 12). |
RealmService |
Realm CRUD, settings management. |
RoleService |
Role CRUD, assignment. |
ClientService |
OAuth2 client registration, secret generation. |
SessionService |
Create/revoke/expire sessions. Redis + DB. |
AuditService |
Write audit events. Publishes to Kafka. |
AnalyticsService |
Query audit log for metrics. |
| Class | Purpose |
|---|---|
SecurityConfig |
Spring Security filter chain. Permit /realms//token, /realms//auth, protect /admin/** |
JwtTokenFilter |
OncePerRequestFilter — extract + validate JWT from Authorization header |
JwtTokenProvider |
Build / parse / validate JWT using jjwt 0.12+ |
RedisTokenCache |
Cache token validation results in Redis with TTL |
CorsConfig |
CORS configuration from application.yml |
authforge.auth.login ← on successful login
authforge.auth.login_failed ← on failed login
authforge.auth.logout ← on logout
authforge.auth.register ← on registration
authforge.token.refresh ← on token refresh
authforge.session.expired ← on session expiry
authforge.admin.user.created ← on user creation
authforge.admin.user.updated ← on user update
authforge.admin.user.deleted ← on user delete
@Component
public class AuthEventProducer {
// Use KafkaTemplate<String, AuthEvent>
// Publish events to all topics listed above
// Key = realmName:userId
}public record AuthEvent(
String eventType,
String realmId,
String userId,
String clientId,
String ipAddress,
Instant timestamp,
Map<String, Object> details
) {}| Config Class | Purpose |
|---|---|
RedisConfig |
RedisTemplate, ObjectMapper |
KafkaConfig |
KafkaAdmin, topic creation |
CacheConfig |
CacheManager with Redis |
WebSocketConfig |
STOMP over WebSocket for real-time sessions |
GraphQLConfig |
Schema + resolvers |
OpenApiConfig |
Swagger/OpenAPI 3.1 docs |
SchedulerConfig |
Cleanup expired sessions every 5 min |
Serve these HTML pages via Thymeleaf at:
- Full login form with realm name displayed
- OAuth2 authorization code flow
- Links to register
- Styled with embedded CSS (dark theme, professional)
- Form POSTs to
/realms/{realm}/protocol/openid-connect/token - JS redirects with auth code after login
- Registration form: username, email, password, first/last name
- POSTs to
/realms/{realm}/register - Redirects to login on success
- Generic error page for 4xx/5xx
// Install
npm install @authforge/react
// AuthForgeProvider wraps your app
import { AuthForgeProvider, useAuth } from '@authforge/react'
function App() {
return (
<AuthForgeProvider
url="http://localhost:8080"
realm="myrealm"
clientId="my-react-app"
>
<Router />
</AuthForgeProvider>
)
}
// useAuth hook
const { user, token, login, logout, hasRole, isAuthenticated } = useAuth()// middleware.ts
import { withAuth } from '@authforge/nextjs'
export default withAuth({ realm: 'myrealm', clientId: 'my-next-app' })
export const config = { matcher: ['/dashboard/:path*'] }
// Server component
import { getServerSession } from '@authforge/nextjs/server'
const session = await getServerSession()
// Client component
'use client'
import { useAuth } from '@authforge/nextjs/client'
const { user, login, logout } = useAuth()# pip install authforge-python
from authforge import AuthForgeClient
from authforge.fastapi import require_auth, require_role, get_current_user
client = AuthForgeClient(
url="http://localhost:8080",
realm="myrealm",
client_id="my-python-api",
client_secret="secret"
)
@app.get("/me")
@require_auth(client)
async def me(user = Depends(get_current_user(client))):
return user
@app.delete("/users/{id}")
@require_role(client, "admin")
async def delete(id: str):
...// composer require authforge/laravel
// config/authforge.php
Route::middleware('authforge')->group(function () {
Route::get('/profile', [ProfileController::class, 'show']);
});
Route::middleware('authforge:admin')->group(function () {
Route::apiResource('users', AdminUserController::class);
});
// In controller
$user = auth()->user(); // AuthForge user object
$user->hasRole('admin');
$user->getRoles();version: '3.9'
services:
postgres:
image: postgres:16-alpine
environment:
POSTGRES_DB: authforge
POSTGRES_USER: authforge
POSTGRES_PASSWORD: authforge_secret
ports:
- "5432:5432"
volumes:
- postgres_data:/var/lib/postgresql/data
redis:
image: redis:7-alpine
ports:
- "6379:6379"
command: redis-server --appendonly yes
volumes:
- redis_data:/data
zookeeper:
image: confluentinc/cp-zookeeper:7.6.0
environment:
ZOOKEEPER_CLIENT_PORT: 2181
ports:
- "2181:2181"
kafka:
image: confluentinc/cp-kafka:7.6.0
depends_on: [zookeeper]
ports:
- "9092:9092"
environment:
KAFKA_BROKER_ID: 1
KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181
KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://localhost:9092
KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1
KAFKA_AUTO_CREATE_TOPICS_ENABLE: "true"
authforge:
build: .
ports:
- "8080:8080"
depends_on: [postgres, redis, kafka]
environment:
SPRING_DATASOURCE_URL: jdbc:postgresql://postgres:5432/authforge
SPRING_DATA_REDIS_HOST: redis
SPRING_KAFKA_BOOTSTRAP_SERVERS: kafka:9092
volumes:
postgres_data:
redis_data:# 1. Start infrastructure
docker-compose up -d postgres redis zookeeper kafka
# 2. Build
./gradlew clean build
# 3. Run
./gradlew bootRun
# OR run the JAR
java -jar build/libs/authforge-1.0.0.jar
# 4. Access
# API: http://localhost:8080
# Swagger: http://localhost:8080/swagger-ui.html
# GraphQL: http://localhost:8080/graphiql
# 5. Default credentials
# Username: admin
# Password: admin
# Realm: masterFollow this order when building:
- Create Gradle wrapper files (gradlew, gradlew.bat, gradle/wrapper/)
- Write
build.gradleandsettings.gradle - Write
application.yml(dev + prod profiles) - Create all JPA entities in
io.authforge.domain - Write all Flyway migration SQL files
- Seed master realm + admin user on startup (
ApplicationRunner)
-
TokenService— JWT generation + Redis cache -
AuthService— all grant types -
AuthController— all OIDC endpoints -
SecurityConfig— protect routes, JWT filter -
SessionService— create/revoke sessions
- All Admin controllers (realm, user, role, client, session, audit)
- All corresponding Services
- Request validation (Jakarta Bean Validation)
- Global exception handler (
@RestControllerAdvice)
-
KafkaConfig+ topic creation -
AuthEventProducer— publish all auth events -
AuditService— write to DB + Kafka -
RedisTokenCache— cache token validation results - Session expiry scheduler (every 5 min)
-
login.html— Thymeleaf SSO login page -
register.html— self-service registration -
error.html— error page
- GraphQL schema + resolvers (admin queries)
- WebSocket session monitor endpoint
- Analytics endpoints
- Swagger/OpenAPI docs
- Docker + docker-compose.yml
-
/docs/sdks/integration guides
- Use Spring Boot 3.3.x — Do NOT use Spring Boot 2.x
- Java 21 — use records, pattern matching, sealed classes where appropriate
- Use Lombok —
@Data,@Builder,@Slf4j,@RequiredArgsConstructor - BCrypt cost 12 for all password hashing
- All admin endpoints require
adminrole — enforce with@PreAuthorize("hasRole('admin')") - All tokens validated via Redis before hitting the DB
- Every auth event published to Kafka before returning response
- Flyway only for DB migrations — never use
ddl-auto: create - No hardcoded secrets — use
application.ymlor env vars - Write tests for all service classes using JUnit 5 + Mockito
On first startup, auto-create:
| Item | Value |
|---|---|
| Realm | master |
| Admin user | admin / admin |
| Admin email | admin@localhost |
| Roles | admin, user |
| Default client | admin-cli (confidential) |
| Default client | account (public, for React/Next.js) |
# Get OIDC discovery
GET /realms/master/.well-known/openid-configuration
# Login (password grant)
POST /realms/master/protocol/openid-connect/token
Body: grant_type=password&username=admin&password=admin&client_id=admin-cli&client_secret=xxx
# Register user
POST /realms/master/register
Body: { "username": "john", "email": "john@example.com", "password": "secret123" }
# Get all users (admin)
GET /admin/realms/master/users
Header: Authorization: Bearer <admin_access_token>
# Create realm
POST /admin/realms
Body: { "name": "myapp", "displayName": "My Application" }