Demo Hardening
Architecture Reference File: README.md
Mermaid diagrams: 5 sequence diagrams covering PKCE, refresh rotation, SCIM JML, device flow, TOTP
iam-protocol-engine/
├── backend/
│ ├── auth-core/ # JPA entities, AuditService, repositories, Flyway migrations
│ ├── oauth-oidc/ # /authorize, /token, OIDC discovery, JWKS, ID token, /userinfo
│ ├── saml-federation/ # SAML SP: metadata, AuthnRequest, ACS, SAML→OIDC bridge
│ ├── scim/ # SCIM 2.0 /Users, /Groups, JML lifecycle hooks
│ ├── mfa/ # TOTP (RFC 6238) + WebAuthn/FIDO2 (W3C)
│ ├── device-flow/ # RFC 8628 Device Authorization Grant
│ ├── demo-resource/ # Protected sample API (validates Bearer tokens)
│ └── api-gateway/ # Spring Boot @SpringBootApplication entry point
├── frontend/
│ └── app/ # Docusaurus learning site + Admin UI scaffold
├── scripts/
│ └── demo-e2e.sh # End-to-end demo script
└── infra/
└── docker-compose.yml # PostgreSQL 16 + Redis 7
Key constraint: auth-core is the only module with JPA entities. All other modules depend on it for data access.
Entity Primary Key Notable Design OAuthClientclient_idclient_secret_hash (SHA-256), redirect_uris as comma-separated TEXTAuthCodecodecode_challenge stored verbatim, 5-min TTL, consumed after single useTokenjtitype value among access_token, refresh_token, id_token, family_id for rotation groupsScimUserid (UUID)user_name UNIQUE, groups as comma-separated TEXT, attributes JSONBScimGroupid (UUID)members as comma-separated scim_user.id TEXT, attributes JSONBWebAuthnCredentialcredential_id (base64url)public_key_cose BYTEA, sign_count BIGINTTotpCredentialid (UUID)user_id UNIQUE, secret_encrypted BYTEA (AES-256-GCM)DeviceCodedevice_codeuser_code UNIQUE, 16-char formatted code, status value among pending/approved/denied/expiredAuditEventidJSONB details column for structured event data
PostgreSQL has native JSONB but no native array type. Other projects might use TEXT[] or JSONB. This project uses comma-separated TEXT for redirect_uris, scopes, groups, and grant_types because:
Simpler schema, no custom type casting needed
LIKE queries work directly (e.g., WHERE ',' || scopes || ',' LIKE '%,openid,%')
No JSON parsing overhead for simple membership checks
attributes on SCIM entities uses JSONB because SCIM schemas are complex and extensibility is required per RFC 7643
Decision Implementation Why It Matters PKCE required for public clients code_challenge_method=S256 enforced at /authorizeAuth code interception on public networks becomes useless without the code_verifier RS256 (asymmetric) for all tokens JWS signed with RSA-SHA256, public key in JWKS Compromised server doesn't expose signing key; only the public key need be shared kid in JWKS from day oneEach key has a stable kid derived from its thumbprint Key rotation works without breaking old tokens; rolling keys is a config change, not a code change redirect_uri exact matchString equality check against registered URI Pattern matching is an injection vector; exact match is unambiguous Refresh token rotation Old token atomically revoked on reuse, same family_id Prevents replay if old token is stolen; "family sweep" revokes entire token lineage Device code single-use Deleted from PostgreSQL after successful token issuance Device code cannot be replayed after consumption TOTP secret AES-256-GCM encrypted 12-byte random IV per encryption Plaintext secret never hits the DB; IV reuse impossible across encryptions WebAuthn sign_count anti-cloning Server stores and increments counter on each auth Cloned credential fails sign_count check after first legitimate use Audit log on every token operation AuditService.audit(event) called from token issuance, revocation, introspectionIncident response requires a paper trail; every token has a jti for correlation PostgreSQL for short-lived state Auth codes (5min TTL), device codes (10min TTL), refresh tokens (7d TTL) Redis is ephemeral; if Redis state is lost, tokens become invalid. PostgreSQL is durable.
Client Authorization Server PostgreSQL
| | |
|-- GET /authorize ---->| |
| code_challenge=... |-- Store auth_code ---->|
| | (5min TTL, unconsumed) |
|<-- 302 redirect -----| |
| ?code=AUTHCODE | |
| | |
|-- POST /token -------->| |
| code + verifier |-- Verify code_challenge|
| | = SHA256(verifier) |
| |-- Mark auth_code used ->|
| |-- Issue tokens -------->|
|<-- access_token -----| |
| refresh_token | |
| id_token | |
Client Auth Server PostgreSQL
|-- POST /token (refresh_token=R1) -->|
| |-- Lookup R1, active=true
| |-- Issue R2 + AT2
| |-- Revoke R1 + AT1 atomically
|<-- access_token_2 + R2 ----------|
|
| (Attacker reuses stolen R1)
|-- POST /token (refresh=R1) -------->|
|<-- 400 invalid_grant --------------|
| (Family sweep: AT1 also revoked) |
SCIM Client ScimUserService TokenService PostgreSQL
| | | |
| JOINER | | |
|-- POST /Users ->| | |
| |-- audit(joiner) -->| |
| |-- INSERT user ---->|---------------->|
|<-- 201 ---------| | |
| | | |
| MOVER | | |
|-- PUT /Users -->| | |
| |-- UPDATE user ---->|---------------->|
|<-- 200 ---------| | |
| | | |
| LEAVER | | |
|-- DELETE /User->| | |
| |-- revokeAllTokens ->| |
| | |-- UPDATE token -->|
| | | revoked=true |
| |-- DELETE user ----->|---------------->|
|<-- 204 ---------| | |
Device (TV) Auth Server User (phone) PostgreSQL
|-- POST /device_authorization -->|
|<-- { device_code, user_code }-|
| |------- GET /device?user_code=XXXX ->|
|<------------------------------ HTML page with approve button --|
| |-- POST /device/approve -------->|
| |-- UPDATE status=approved ----->|
|<------------------------------ Device approved ----------------|
| | |
| (poll every 5s) | |
|-- POST /token (device_code) -->| |
|<-- 400 authorization_pending-| (not yet approved)|
| (wait 5s) | |
|-- POST /token (device_code) -->| |
|<-- { access_token, ... } ------| (after approval)|
| | |-- DELETE device_code
User TotpService Auth App PostgreSQL
|-- POST /mfa/totp/setup -->|
| (Bearer token) |-- Generate 160-bit secret
| |-- AES-256-GCM encrypt
| |-- INSERT totp_credential
|<-- { secret, qrCodeImage }-| (verified=false)
| | |
|-- Scan QR in app | |
| | |
| (loop every 30s) | |
|-- POST /mfa/totp/verify -->| |
| code=123456 |-- Decrypt secret |
| |-- Verify TOTP == code
| |-- UPDATE verified=true (first time)
|<-- { verified: true } ----| |
Method Path Grant GET/oauth2/authorize— POST/oauth2/tokenauthorization_code, client_credentials, refresh_token, urn:ietf:params:oauth:grant-type:device_codePOST/oauth2/introspectRFC 7662 POST/oauth2/revokeRFC 7009 GET/.well-known/openid-configurationRFC 8414 GET/.well-known/jwks.jsonRFC 7517 POST/.well-known/jwks.jsonKey rotation GET/userinfoOIDC Core
Method Path RFC POST/scim/v2/Users§5.2 GET/scim/v2/Users§5.2 GET/scim/v2/Users/{uuid}§5.2 PUT/scim/v2/Users/{uuid}§5.2 DELETE/scim/v2/Users/{uuid}§5.2 POST/scim/v2/Groups§5.3 GET/scim/v2/Groups§5.3 GET/scim/v2/Groups/{uuid}§5.3 PATCH/scim/v2/Groups/{uuid}§5.3
Method Path Notes GET/saml/metadataSigned SP metadata XML GET/saml/initiateBuilds and redirects with signed AuthnRequest POST/saml/acsReceives SAMLResponse from IdP
Method Path RFC POST/mfa/totp/setupRFC 6238 POST/mfa/totp/verifyRFC 6238 GET/mfa/totp/status— POST/webauthn/register/beginW3C WebAuthn POST/webauthn/register/completeW3C WebAuthn POST/webauthn/authenticate/beginW3C WebAuthn POST/webauthn/authenticate/completeW3C WebAuthn
Method Path RFC POST/device_authorizationRFC 8628 §3.1 GET/deviceRFC 8628 §3.2 (user approval page) POST/device/approveRFC 8628 §3.3