---
title: The three-key model
description: How Sudomimus proves a redemption is legitimate by splitting
  per-session proof across three keys — exposureKey, hiddenKey, and
  confirmationKey — each held by a different party.
editUrl: true
head: []
template: doc
sidebar:
  order: 2
  hidden: false
  attrs: {}
pagefind: true
draft: false
---

The three-key model is the heart of Sudomimus's session security for the browser-mediated flow. Every authentication round-trip through Connect mints **three distinct keys** at different moments, held by different parties, defending against different classes of attack. A successful `/redeem` must present all three.

:::note[Applies to the Connect browser flow]
The three-key model is specific to the **Connect** lifecycle — the four-phase flow described in [the Connect flow](/en-us/connect/flow/). Native one-shot flows (`/direct-issue/steam-ticket`, `/direct-issue/access-key`) bypass it: the credential they present *is* the proof. OIDC uses its own authorization-code + PKCE shape. This page describes the shape that backs the Connect flow.
:::

This is not about the long-lived material Sudomimus uses to sign tokens (covered in [Tokens and verification](/en-us/concepts/tokens-and-verification/)). It is about how a single browser-mediated login proves itself.

## The three keys

| Key | Created by | Held by | Visible to |
|---|---|---|---|
| **exposureKey** | `/establish` | Application backend, then the browser | Browser, URL, `via.sudomimus.com` |
| **hiddenKey** | `/establish` | Application backend only | Backend |
| **confirmationKey** | `via.sudomimus.com`, after the user authenticates | Application backend (via callback) | Browser, URL |

`exposureKey` and `hiddenKey` are issued together as a **pair** by the same `/establish` call. They belong to one session, and only that session.

Each key carries a role-specific prefix so a misplaced key can be rejected at the request boundary:

- `exp_` + 32 lowercase hex characters
- `hid_` + 32 lowercase hex characters
- `cnf_` + 32 lowercase hex characters

## Why three keys, and not one

If a single opaque session reference were enough to redeem a token, anyone who saw that reference could redeem on the user's behalf. Splitting the proof three ways means an attacker has to compromise **three different vantage points at the same time**:

- **Without `hiddenKey`** — an attacker who steals the URL the user is visiting still cannot redeem. The hidden key never leaves your server.
- **Without `exposureKey`** — an attacker who breaches your server still cannot redeem someone else's session, because each pending session's exposure key is bound to the specific browser it was sent to.
- **Without `confirmationKey`** — neither party can redeem a session the user never actually completed. The confirmation key is only minted by `via.sudomimus.com` after a real passkey or OTP challenge succeeds.

## Lifecycle

```mermaid
flowchart TD
    Establish["POST /establish"] --> Exposure["exposureKey<br/>public session reference"]
    Establish --> Hidden["hiddenKey<br/>server-side secret"]

    Exposure -->|redirect URL| Browser["User browser"]
    Hidden -->|kept by| Backend["Application backend"]

    Browser -->|exposureKey| Via["via.sudomimus.com<br/>user authenticates"]
    Via -->|authentication succeeds| Confirmation["confirmationKey<br/>authentication proof"]

    Exposure -. carried through browser .-> Callback["Application callback"]
    Confirmation -->|callback URL| Callback

    Callback -->|exposureKey + confirmationKey| Redeem["POST /redeem"]
    Backend -->|hiddenKey| Redeem

    Redeem --> Tokens["accessToken + refreshToken"]
```

After `/redeem` succeeds, all three keys are consumed and cannot be reused. A second redemption with the same triple fails — even if the application repeats the request.

## Comparison with OAuth 2.0

If you are familiar with the OAuth authorization-code flow, the rough analogues are:

| Sudomimus | OAuth 2.0 |
|---|---|
| `exposureKey` | `state` parameter (loosely) |
| `confirmationKey` | authorization `code` |
| `hiddenKey` | *no direct equivalent* |

OAuth relies on the long-lived `client_secret` to authenticate the token exchange. Sudomimus instead uses a per-session `hiddenKey` that is fresh on every `/establish` call. Leaking one session's hidden key compromises that single redemption — not every redemption your application ever performs.

This is the same idea as proof-of-possession tokens: prefer short-lived, narrowly-scoped secrets over long-lived shared ones.

If you actually want OAuth/OIDC semantics, Sudomimus also runs a [standard OIDC provider](/en-us/oidc/flow/) at `oidc.sudomimus.com`. The three-key model is what powers the Connect protocol underneath; relying parties using OIDC do not see it directly.

## The long-lived keypairs

Aside from the three per-session keys, each application has two independent RSA-2048 key authorities:

- **Token-signing keys** — Sudomimus signs access and refresh JWTs with the active private key; the application selects the matching `kid` from the Session JWK Set at `GET /applications/{applicationAnchor}/jwks.json`.
- **Client-auth keypair** — the application signs every `/establish` request with its private half (delivered once at application creation and at each rotation); Sudomimus verifies with the stored public half.

Neither of these is part of the "three-key" model. They authenticate the *channel* between Sudomimus and the application; the three per-session keys authenticate a *single login*. The details are in [Tokens and verification](/en-us/concepts/tokens-and-verification/).