Skip to content

Authentication Flow

IDToken implements the ITU-T X.1280 mutual out-of-band authentication protocol. The service presents an OTP to the user, who confirms it on their mobile device with facial recognition.

The X.1280 protocol inverts traditional authentication: instead of the user proving their identity to the service, both parties authenticate each other through an out-of-band channel.

BrowserAuth ServerMobile AppPOST /auth/initiate{ tokenId }FCM Push (OTP){ sessionId, OTP, wsToken, random }WS /ws/session/{ sid }{ type: “otp_ready” }User compares OTPsUser sees OTP matchConfirms + FacePOST /auth/verify{ sessionId, tokenId, otp, signature, timestamp }VerifyIssue JWTWS { type: “approved”, jwt, hash, random }AuthenticatedJWT received

The browser (or relying party) initiates an auth session by providing the user’s tokenId and identifying the service.

Request: POST /auth/initiate

The service can identify itself in two ways:

Option A — Service VDS (recommended): The service presents its cryptographically signed Service VDS. The server verifies the signature and extracts the service identity — no serviceId needed. See Mutual Service Identity for details.

{
"tokenId": "vds-uuid-from-enrollment",
"serviceVds": "HC1:6BFOXN%TS3DH...",
"scopes": ["identity:name", "identity:age_over_18"]
}

Option B — serviceId (legacy): The service passes a plain identifier registered by an administrator.

{
"tokenId": "vds-uuid-from-enrollment",
"serviceId": "my-web-app",
"scopes": ["identity:name", "identity:age_over_18"]
}

Server processing:

  1. Verify enrollment exists and is not revoked
  2. Identify the service: verify Service VDS signature (Option A) or look up serviceId in registry (Option B)
  3. Validate requested scopes against the service’s allowed scopes (from VDS scope ceiling or registry)
  4. Generate a 6-digit OTP using HKDF-SHA256, bound to the session context (tokenId + sessionId)
  5. Generate a WebSocket authentication token (HMAC-SHA256)
  6. Generate a random nonce for response verification
  7. Store session in the ephemeral cache (60-second TTL)
  8. Send FCM push notification to the mobile app (with verified service identity when using Service VDS)
  9. Notify the browser via WebSocket

Response:

{
"sessionId": "sess_abc123",
"autoPassword": "482917",
"wsToken": "hmac-token-for-websocket",
"random": "a1b2c3d4e5f6...",
"expiresAt": "2025-01-15T10:01:00Z"
}

The browser connects to the WebSocket endpoint to receive real-time session updates.

Connection: GET /ws/session/{sessionId}?token={wsToken}

The wsToken is verified using constant-time HMAC comparison. Only one connection per session is allowed (new connections replace old ones with close code 4009).

Events received:

{ "type": "otp_ready", "autoPassword": "4829**", "expiresAt": "..." }

The browser displays the OTP to the user.

The mobile app receives an FCM push notification containing the OTP and session details. The user:

  1. Compares the OTP on screen with the OTP on their phone
  2. If they match, confirms with facial recognition (id3 Face SDK)
  3. The app signs the verification payload with its ECDSA private key

Signed message format:

{sessionId}|{otp}|{timestamp}[|{grantedScopes}]

The grantedScopes field is optional and allows the user to selectively grant a subset of the requested scopes.

The mobile app sends the signed verification to the server.

Request: POST /auth/verify

{
"sessionId": "sess_abc123",
"tokenId": "vds-uuid",
"otp": "482917",
"signatureBase64": "MEUCIQD...",
"timestamp": 1705312860,
"grantedScopes": ["identity:name", "identity:age_over_18"]
}

Server processing:

  1. Retrieve session
  2. Validate timestamp (must be within ±30 seconds)
  3. Check attempt counter (max 3 attempts)
  4. Verify ECDSA signature against the enrollment’s public key
  5. Verify OTP (±1 time window tolerance)
  6. Build JWT claims based on granted scopes and VDS data
  7. Compute response verification hash (HMAC-SHA256 over jwt, sessionId, and nonce)
  8. Notify the browser via WebSocket
  9. Log AUTH_APPROVE audit event
  10. Delete session (single-use)

Response:

{
"jwt": "eyJhbGciOiJFUzI1NiIs...",
"hash": "response-verification-hmac",
"random": "a1b2c3d4e5f6...",
"expiresAt": "2025-01-15T11:00:00Z"
}

The browser receives the JWT via WebSocket:

{
"type": "approved",
"jwt": "eyJhbGciOiJFUzI1NiIs...",
"hash": "response-verification-hmac",
"random": "a1b2c3d4e5f6...",
"expiresAt": "2025-01-15T11:00:00Z"
}

The browser can optionally verify the response hash to confirm the JWT came from the legitimate server (X.1280 anti-forgery protection).

The issued JWT contains scope-filtered identity claims:

{
"iss": "https://idtoken.example.com",
"aud": "https://services.example.com",
"sub": "vds-token-id",
"iat": 1705312800,
"exp": 1705316400,
"scope": "identity:name identity:age_over_18",
"idtoken": {
"tokenId": "vds-token-id",
"givenName": "Jean",
"familyName": "Dupont",
"ageOver18": true,
"trustLevel": 3
}
}

See Claims & Scopes for the full scope system.

If verification fails, the server publishes a rejected event:

{
"type": "rejected",
"reason": "Invalid signature"
}

Possible rejection reasons:

  • Invalid signature — ECDSA signature verification failed
  • Invalid OTP — OTP does not match (within tolerance)
  • Too many attempts — Brute-force limit exceeded (3 attempts)
  • Session expired — 60-second TTL exceeded
PropertyMechanism
Mutual authenticationUser verifies service via OTP match (and verified Service VDS identity); service verifies user via ECDSA signature
Replay protectionSessions are single-use, deleted after verification
Brute-force protectionMax 3 OTP attempts per session
Time-bound60-second session TTL, ±30s timestamp validation
Anti-forgeryResponse hash allows browser to verify JWT authenticity
Out-of-bandOTP delivered via separate channel (FCM push)