Skip to main content

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

ComponentTechnology
FrontendReact application
BackendKotlin microservices (Spring Boot)
DeploymentKubernetes cluster
DatabasePostgreSQL
New componentKeycloak (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).

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 in user-service)
  • Email
  • 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 in keycloak)
  • First name
  • Last name
  • Email
  • 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/register Register a new user
  • GET /users/me Get info of currently logged in user
  • PATCH /users/me Update info of currently logged in user
  • DELETE /users/me Delete currently logged in user's account
  • POST /users/me/recovery Start passkey recovery for currently logged in user
  • GET /users/{id}/accounts Get users onchain accounts
  • POST /find Get UUID by email
  • POST /invites/verify Verify validity of an invitation token
  • POST /users/{id}/roles/originator Add ORIGINATOR role to a user. Invoking user must have ADMIN or ORIGINATOR role.
  • DELETE /users/{id}/roles/originator Remove ORIGINATOR role from a user. Invoking user must have ADMIN or ORIGINATOR role.
  • POST /users/identity_verification_records Request a new KYC identity verification record for the current user

Admin API

    • GET /admin/users Get all users by specified params
  • GET /admin/users/{id} Get user by ID
  • PATCH /admin/users/{id} Update user by ID
  • DELETE /admin/users/{id} Delete user by its ID
  • GET /admin/invites Get all invite tokens (Admin only)
  • POST /admin/invites/generate Generate new invitation tokens
  • DELETE /admin/invites/{token} Cancel validity of an invitation token
  • POST /admin/users/{id}/roles/admin Add admin role to a user. Invoking user must have ADMIN role
  • DELETE /admin/users/{id}/roles/admin Remove 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:
    • email
  • 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

ClaimTokenClient
email, nameID token onlyreact-frontend
wallet_address (user attribute)ID token onlyreact-frontend
kyc_status (user attribute)Access token onlykotlin-services
country_code (user attribute)Access token onlykotlin-services
realm_rolesAccess tokenkotlin-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>
);
}

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

• 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