Identity Management
IDM - Identity Management
1. Context
This project requires an Identity Management (IDM) and user authentication solution with support for modern authentication methods The current implementation uses a custom JWT authentication with a username/password flow stored in a PostgreSQL database. The goal is to migrate to Keycloak as a central Identity Provider (IdP) with support for Passkeys and Web3 wallet authentication.
2. Implementation Goals
• Deploy Keycloak as a central authentication service in the Kubernetes cluster • Implement Passkeys (WebAuthn) support • Implement Web3 wallet login (Sign-In with Ethereum / SIWE) • Ensure secure token storage in HttpOnly Secure cookies • Separate identity data from application data following best practices
3. Technical Stack
| Component | Technology |
|---|---|
| Frontend | React application |
| Backend | Kotlin microservices (Spring Boot) |
| Deployment | Kubernetes cluster |
| Database | PostgreSQL |
| New component | Keycloak (latest stable, passkeys feature enabled) |
4. Architecture
4.1 Passkey Authentication (WebAuthn via Keycloak)
The solution uses passwordless authentication with passkeys (FIDO2/WebAuthn) and Keycloak as the central Identity Provider. Authentication is based on secure cryptographic mechanisms (biometrics/PIN), eliminating passwords and reducing phishing and credential-theft risks.
Application access is handled via a token-based strategy (OIDC + OAuth 2.0). Issued tokens are stored in HttpOnly Secure cookies, ensuring XSS protection and secure transmission.
Keycloak provides centralised authentication, session management, and token issuance for all applications (DEUSS Core, Broker Platform, Admin).
4.2 Authentication Flow — Direct Cookie Pattern
React Frontend ↓(OIDC Authorization Code Flow + PKCE) Keycloak(authentication + token issuance) ↓(HttpOnly Secure cookie containing tokens) React Frontend ↓(cookie attached automatically to every request) Kotlin Microservices ↓(JWT validation from cookie) Keycloak(token introspection — only if stateless JWT verification is insufficient)
| ℹ️ Why cookies? Tokens stored in HttpOnly Secure cookies are inaccessible to JavaScript (XSS protection). The browser attaches the cookie automatically, keeping the architecture simpler than a BFF pattern. PKCE protects the authorization code flow. |
|---|
4.3 Token Strategy (OIDC + OAuth 2.0)
ID Token (user identity — consumed by the frontend)
{
"iss": "https://auth.deussblockchain.eu/realms/whitelabel",
"sub": "550e8400-e29b-41d4-a716-446655440000",
"aud": "whitelabel-fe",
"email": "john@example.com",
"email_verified": true,
"preferred_username": "john@example.com"
}
| ⚠️ Review note: wallet_address and kyc_status in the ID token is unusual — the ID token is meant purely for identifying the user, not for authorization decisions. Consider moving kyc_status exclusively to the Access Token and exposing wallet_address only via the /userinfo endpoint to avoid token bloat and information leakage. |
|---|
Access Token (authorization — consumed by backend services)
{
"typ": "Bearer",
"exp": 1779960389,
"iat": 1779960089,
"iss": "https://auth.deussblockchain.eu/realms/whitelabel",
"azp": "whitelabel-fe",
"sid": "6aa950eb-c52f-d6f4-7bd7-a950bc89d789",
// --- Important for backend ---
"sub": "2bb7079d-63de-4d26-ae2d-eab272e8fe4f",
"realm_access": {
"roles": [
"ROLE_USER",
"ROLE_ADMIN"
]
},
// --- Standard metadata (Ignored by backend) ---
"jti": "12dcc431-2e5f-7037-8213-61a4941147a6",
"auth_time": 1779950857,
"acr": "0",
"resource_access": { "account": { "roles": ["manage-account", "view-profile"] } },
"scope": "openid email profile",
"email": "john@example.com"
}
| ℹ️ Note: Access tokens should be short-lived (5–15 min) and contain only claims needed for authorization. Avoid placing PII or display data here — use the ID token or /userinfo for that. |
|---|
5. Hybrid Data Model
Use user-service is used to store additional user profile data. In Keycloak, we only keep the bare essentials, which is not everything we keep about the user.
5.1 Data in Keycloak (Identity Layer)
Keycloak stores the core identity and authorization attributes:
- ID (User identifier in
keycloak, same as ID inuser-service) - Username
- Registered Passkey
5.2 Data in PostgreSQL (user-service — Application Layer)
The application database stores additional business data and necessary on-chain identifiers:
- ID (User identifier in
user-service, same as ID inkeycloak) - First name
- Last name
- Roles (used for Broker Platform authorization)
- Onchain account address
- Identity verification state
The user-service exposes CRUD REST APIs for managing application-specific user profile data stored in PostgreSQL. Keycloak remains the central identity provider and source of authentication/authorization data, while the user-service manages extended business-related user information.
The integration is based on the user_id which serves as the unique primary identifier for both user-service and keycloak. User lifecycle operations must remain synchronized between Keycloak and the application layer.
CRUD REST API Operations (user-service)
User API
-
POST /auth/registerRegister a new user
GET /users/meGet info of currently logged in userPATCH /users/meUpdate info of currently logged in userDELETE /users/meDelete currently logged in user's accountPOST /users/me/recoveryStart passkey recovery for currently logged in userGET /users/{id}/accountsGet users onchain accountsPOST /findGet UUID by emailPOST /invites/verifyVerify validity of an invitation tokenPOST /users/{id}/roles/originatorAdd ORIGINATOR role to a user. Invoking user must have ADMIN or ORIGINATOR role.DELETE /users/{id}/roles/originatorRemove ORIGINATOR role from a user. Invoking user must have ADMIN or ORIGINATOR role.POST /users/identity_verification_recordsRequest a new KYC identity verification record for the current user
Admin API
-
GET /admin/usersGet all users by specified params
GET /admin/users/{id}Get user by IDPATCH /admin/users/{id}Update user by IDDELETE /admin/users/{id}Delete user by its IDGET /admin/invitesGet all invite tokens (Admin only)POST /admin/invites/generateGenerate new invitation tokensDELETE /admin/invites/{token}Cancel validity of an invitation tokenPOST /admin/users/{id}/roles/adminAdd admin role to a user. Invoking user must have ADMIN roleDELETE /admin/users/{id}/roles/adminRemove admin role from a user. Invoking user must have ADMIN role.
Keycloak Integration Operations
- Synchronize user lifecycle with Keycloak
- Validate JWT and resolve
keycloak_id - Synchronize selected identity attributes:
- Optional admin operations:
- enable/disable user
- role assignment
- reset authentication methods / passkeys
6. Infrastructure
6.1 Keycloak Deployment in Kubernetes
• Deploy Keycloak as a StatefulSet with at least 2 replicas (HA) • Provision a dedicated PostgreSQL database/schema for Keycloak • Enable the passkeys feature flag: --features=passkeys • Configure Ingress with a TLS certificate • Set resource limits (recommended: 512 Mi – 1 Gi RAM)
# Core environment configuration KC_DB: postgres KC_DB_URL: jdbc:postgresql://postgres-service:5432/keycloak KC_HOSTNAME: auth.yourdomain.com KC_PROXY: edge KC_FEATURES: passkeys
6.2 Realm Configuration
| ℹ️ Note: The realm name in the document is a placeholder ('app'). Align the actual name with your naming convention before deploying, as it forms part of all OIDC endpoint URLs and is difficult to rename later. |
|---|
Clients: • react-frontend — public, Authorization Code + PKCE • kotlin-services — bearer-only, for token validation Scopes: • app_access — common scope representing general access within a login session • blockchain:read — read wallets and transactions • blockchain:write — sign and submit transactions
Roles: • user — standard user • originator — can trade • admin — system administrator
6.3 Protocol Mappers
| Claim | Token | Client |
|---|---|---|
| email, name | ID token only | react-frontend |
| wallet_address (user attribute) | ID token only | react-frontend |
| kyc_status (user attribute) | Access token only | kotlin-services |
| country_code (user attribute) | Access token only | kotlin-services |
| realm_roles | Access token | kotlin-services |
7. Frontend Integration (React)
7.1 keycloak-js Setup
// keycloak.ts
import Keycloak from 'keycloak-js';
const keycloak = new Keycloak({
url: '',
realm: 'my-app',
clientId: 'react-frontend'
});
export default keycloak;
7.2 OIDC Authorization Code Flow with PKCE
// App.tsx
import { ReactKeycloakProvider } from '@react-keycloak/web';
import keycloak from './keycloak';
function App() {
return (
<ReactKeycloakProvider
authClient={keycloak}
initOptions={{
onLoad: 'check-sso',
pkceMethod: 'S256', // PKCE pro zabezpečení
checkLoginIframe: false,
scope: 'openid profile blockchain:read'
}}
>
<ApplicationRoutes />
</ReactKeycloakProvider>
);
}
7.3 Cookie Configuration
Configure in Keycloak Admin UI → Client react-frontend → Settings: • Access Type: public • Valid Redirect URIs: https://app.yourdomain.com/* • Web Origins: https://app.yourdomain.com • PKCE Code Challenge Method: S256
| ⚠️ Review note: keycloak-js does NOT automatically store tokens in HttpOnly cookies — it holds them in memory (or sessionStorage by default). To get true HttpOnly cookie storage you need either: (a) a thin BFF/reverse proxy that exchanges the code and sets the cookie server-side, or (b) Keycloak's experimental token-cookie feature (not production-ready). Clarify the exact mechanism before committing to this approach. |
|---|
7.4 API Calls with Token
// api.ts
import { useKeycloak } from '@react-keycloak/web';
export function useApi() {
const { keycloak } = useKeycloak();
const callApi = async (url: string, options: RequestInit = {}) => {
// Token se automaticky přikládá z cookie
// Pro explicitní Bearer header (pokud backend vyžaduje):
const response = await fetch(url, {
...options,
headers: {
...options.headers,
'Authorization': \`Bearer ${keycloak.token}\`,
},
});
return response;
};
return { callApi };
}
7.5 Backend configuration
| ⚠️ Review note: KeycloakRoleConverter is referenced but not shown. Keycloak places roles inside realm_access.roles (a nested JSON object), which Spring Security does not parse by default. Make sure the converter flattens that structure into GrantedAuthority objects with the ROLE_ prefix, otherwise hasRole("ADMIN") will never match. |
|---|
8.4 Removing the Old JWT Implementation
• Delete custom JWT signing/validation logic • Remove password hash columns from the users table (migration script required) • Refactor all endpoints that rely on the old authentication mechanism
9. Passkeys Implementation
9.1 Keycloak Configuration
In Admin UI: Authentication → Policies → WebAuthn Passwordless Policy • Relying Party Entity Name: Your App Name • Signature Algorithms: ES256, RS256 • Attestation Conveyance Preference: none • Authenticator Attachment: platform, cross-platform • Require Resident Key: Yes (required for passwordless) • User Verification Requirement: required
9.2 Authentication Flow
• Add WebAuthn Passwordless Authenticator as an ALTERNATIVE to the Username Password Form • Configure a conditionally required flow
9.3 Frontend Integration
Keycloak natively handles the WebAuthn flow after redirect to the login page. No additional frontend implementation is required — Keycloak detects browser support and presents the Passkey option automatically.
10. Web3 Wallet Login (Sign-In with Ethereum)
| ℹ️ Note: This section was initially drafted with AI assistance and requires careful review before implementation. The overall approach is architecturally sound but several details need attention (see inline notes below). |
|---|
10.1 Custom Authenticator (Keycloak SPI Extension)
| ⚠️ Review note — SIWE message construction: The buildSiweMessage() must produce a message that exactly matches what the frontend signs, including newlines, field order, and optional fields as defined in EIP-4361. Any mismatch will cause valid signatures to fail verification. Consider using a tested SIWE library (e.g. siwe-java) rather than hand-rolling the message format. |
|---|
| ⚠️ Review note — signature verification: The verifyEthereumSignature() stub uses web3j. Ensure the messageHash is computed as keccak256(prefix + message) where prefix is the Ethereum signed message prefix (\x19Ethereum Signed Message:\n + length). Test with known address/signature pairs before shipping. Also handle EIP-1271 (smart contract wallets) if multi-sig support is needed. |
|---|
| ⚠️ Review note — auto user creation: findOrCreateUserByWallet() silently creates a Keycloak user on first login. Consider whether a separate registration step is required to collect additional data (e-mail, KYC initiation) and whether an unverified wallet-only account should have restricted access until onboarding is complete. |
|---|
10.2 Frontend Web3 Integration
| ⚠️ Review note: The /auth/web3/nonce endpoint is referenced but not defined in the design. Options: expose it via a Keycloak REST extension, or serve it from a lightweight backend proxy. Also add WalletConnect as a fallback for mobile users who cannot use MetaMask. |
|---|
10.3 Keycloak Theme Customization
• Create a custom theme containing the Web3 login form (web3-login.ftl) • Add MetaMask detection and WalletConnect fallback • Style according to the application design system
10.4 Authentication Flow Registration
In Admin UI: Authentication → Flows → Browser Flow • Add Web3 Authenticator as ALTERNATIVE to Username Password Form • User can choose: classic login, Passkey, or Web3 wallet
11. Database Migration
11.1 Schema Changes
• Add keycloak_id column (UUID, UNIQUE) to the existing users table as the FK to Keycloak • Write a backfill migration to populate keycloak_id for existing users after the Keycloak import • After validation, drop password hash columns
11.2 Keycloak ↔ PostgreSQL Synchronisation
When a new Keycloak identity is created (registration or first Web3/Passkey login), the application database must create a corresponding user_profile row.
Two options: • Kafka event via a Keycloak SPI EventListener extension — preferred for decoupled microservice architecture • Standard OIDC endpoints (/userinfo, /userprofile) polled or triggered on first backend request — simpler but tighter coupling
| ⚠️ Review note: Make sure the synchronisation is idempotent. A Keycloak user can be created multiple times if a retry occurs, so the upsert logic must be safe to call more than once. |
|---|
12. Security Considerations
12.1 Token Security
• Access token expiry: 5–15 minutes • Refresh token expiry: 30 days • Enable refresh token rotation (each refresh token is single-use) • Revoke tokens on logout
12.2 Cookie Security
• Secure: true — HTTPS only • HttpOnly: true — inaccessible to JavaScript (XSS protection) • SameSite: Strict — CSRF protection (verify this does not break cross-origin flows) • Set appropriate expiry aligned with refresh token lifetime
12.3 CORS Configuration
TBD — align with existing CORS policy. Ensure allowed origins are explicitly whitelisted and wildcard (*) is never used for authenticated endpoints.
12.4 Rate Limiting (defaults)
• Login endpoint: 5 attempts / 15 minutes • Token refresh: 10 requests / minute • Web3 nonce endpoint: 10 requests / minute
12.5 Web3-Specific Security
• SIWE message must contain a timestamp (max 5 minutes old) • Nonce must be single-use (invalidate after first verification attempt) • Validate Chain ID to prevent cross-chain replay attacks • Normalise wallet addresses to EIP-55 checksummed format before storage and comparison