01 $ cat ~/projects/authforge/README.md

AuthForge

A self-hosted, multi-tenant identity platform — Flutter + NestJS + Keycloak — demonstrating OIDC with PKCE, per-realm JWT validation, SAML federation and role-based access, end to end. Below is a 2:15 walkthrough of the architecture.

FlutterNestJSKeycloak 26PostgreSQL 15Docker Compose
authforge — architecture walkthrough · 2:15

Runs entirely in your browser — the animation is self-hosted, no third-party player. Use the controls to scrub through all seven scenes.

3
Keycloak realms
PKCE
S256 auth-code flow
SAML 2.0
enterprise federation
0
Keycloak SDKs on the API
02 $ ls ~/projects/authforge/flows

How it works.

Four moving parts do the interesting work — password-safe login, multi-tenant token validation, enterprise federation, and near-zero-config tenant onboarding.

PKCE — the app never sees the password

Sign-in generates a random verifier and its SHA-256 challenge, redirects the browser straight to Keycloak, then swaps a one-time code plus the original verifier for tokens. Web uses a hand-rolled flow (pkce_service_web.dart); mobile uses flutter_appauth. Tokens land in flutter_secure_storage — Keystore / Keychain.

code_challenge=S256public clientflutter_secure_storage

Per-realm JWT validation

The NestJS guard decodes the token's issuer, parses the realm, checks it against a KEYCLOAK_REALMS allowlist, then verifies RS256 + expiry with a JwksClient cached per realm. RolesGuard authorizes on the union of realm-level and client-level roles — raw passport-jwt + jwks-rsa, no Keycloak adapter.

passport-jwtjwks-rsaRolesGuard

SAML federation — tenant-b ← saml-idp

tenant-b brokers to a simulated enterprise SAML IdP (ADFS/Okta-style). On first login, broker mappers create or link the user in tenant-b and assign the viewer role. Flutter and the API never know SAML was involved — they only ever see a standard Keycloak JWT.

identity brokerattribute mappershardcoded-role

A new tenant in ~5 minutes

A tenant is a fully isolated realm. Copy a realm JSON, add its name to KEYCLOAK_REALMS and the Flutter tenant switcher, restart the stack — no code changes. Tokens are realm-scoped: a tenant-a token is rejected by tenant-b.

realm-per-tenantenv-var configisolated users
03 $ cat SECURITY.md

Security decisions.

The choices that make the platform production-shaped rather than a toy demo.

  • No client secret in Flutterpublic client + PKCE — secrets are extractable from any web/mobile binary
  • Raw JWKS validationno Keycloak-specific adapter, so the API validates any OIDC-compliant issuer
  • Dynamic per-realm JwksClientone API binary serves every tenant; a new realm is an env-var change
  • KEYCLOAK_PUBLIC_URL patterndecouples the Docker-internal JWKS host from the public iss claim URL
  • Secure token storageflutter_secure_storage → Android Keystore / iOS Keychain, never plaintext
  • 401 → silent refresh → retrya Dio interceptor renews tokens transparently, else logs out