Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Authentication Flows

This document describes every authentication flow supported by the Authentication Verifier:

#FlowCredentialOutput
1Username / PasswordHTTP Basic / JSON body_ea_ session cookie
2Username / Password + TOTPCredentials + TOTP code_ea_ session cookie
3JWT BearerSigned JWT_ea_ session cookie
4Client Certificate (mTLS)TLS client certificate_ea_ session cookie
5AppRolerole_id + secret_idX-Vault-Token app token
6Kubernetes service-accountKubernetes SA JWTX-Vault-Token app token
7Token self-serviceExisting app tokenMetadata / renewed token

Flows 1–4 produce a session cookie (_ea_) used by human and administrator clients. Flows 5–6 produce an opaque app token (X-Vault-Token) for machine-to-machine workloads. Flow 7 is the self-service lifecycle for app tokens produced by flows 5 and 6.


Overview

The Authentication Verifier sits in front of your application. Every client must prove its identity to the Authentication Verifier before it can access any protected resource. Once authenticated, the server issues a session cookie (_ea_) that the client presents on subsequent API requests.

Client ──► Authentication Verifier ──► Session Cookie ──► Your API ──► Services

Terminology note: Throughout this documentation, client means any entity that authenticates against the /login endpoint — a human via browser, the Auth CLI, or a machine/service account. An Admin (capitalised) is a database record representing an administrator account (super admin or realm admin). See index.md for the full glossary.

All communication uses HTTPS (TLS). Session cookies carry a plain-text cookie_string that is the session identifier stored in the server-side session store.


Realms

Every authentication operation is scoped to a realm. A realm is an isolated authentication domain with its own:

  • Session lifetime settings
  • Authentication parameters (which methods are allowed, TOTP requirements, etc.)

The special _ realm (called ADMIN_REALM) is the administrative realm. All administrator clients authenticate through this realm.


This is the most common flow for clients (human or automated) such as web browsers or the Auth CLI.

Happy-path sequence

sequenceDiagram
    autonumber
    participant U as Client / Browser
    participant EA as Authentication Verifier
    participant SS as Session Store
    participant API as Your API

    U->>EA: POST /login?realm={realm}<br/>{"username":"alice","password":"secret"}
    note over EA: EnsureAuth middleware validates credentials<br/>against the userpass table (Argon2id)
    EA->>SS: upsert_session(session_id, realm, claims, cookie_string)
    SS-->>EA: OK
    EA-->>U: 200 OK<br/>Set-Cookie: _ea_=<cookie_string><br/>{"session_id":"…", "next_step":"Authenticated"}

    U->>API: GET /api/resource<br/>Cookie: _ea_=<cookie_string>
    note over API: CookieAuthSameServer middleware<br/>1. Extracts cookie_string from _ea_ cookie<br/>2. Looks up session in store by cookie_string<br/>3. Returns SessionData claims to handler
    API->>EA: (internal) find_admins_by_auth_scheme
    EA-->>API: Admin{id, realms, …}
    API-->>U: 200 OK  {"data": …}

The _ea_ cookie value is a plain-text cookie_string. All session state is kept server-side in the session store. The cookie_string is used as a lookup key to retrieve the full SessionData record:

FieldTypeDescription
session_idStringUnique session identifier (UUID)
realm_idStringRealm the session belongs to
usernameStringAuthenticated client username
auth_schemeStringAuthentication method used (userpass, jwt, cert)
cookie_stringStringThe opaque value stored in the _ea_ cookie
max_age_secondsi64Maximum absolute session lifetime
max_stale_age_secondsi64Maximum idle time before the session is considered stale
created_ati64Unix timestamp when the session was created

Credential validation

Passwords are hashed with Argon2id using a per-user salt:

salt  = SHA-256(lowercase(username))  → base64url
hash  = Argon2id(password, salt, memory=19456, iterations=2, parallelism=1)

The hash is stored in the userpass table. On login, the server recomputes the hash and compares it in constant time.


Flow 2 — Username / Password with 2FA (TOTP)

When a client's account has TOTP enabled, the login sequence requires a second step.

Sequence with TOTP

sequenceDiagram
    autonumber
    participant U as Client / Browser
    participant App as Authenticator App
    participant EA as Authentication Verifier
    participant SS as Session Store

    U->>EA: POST /login?realm={realm}<br/>{"username":"alice","password":"secret"}
    note over EA: Primary credentials valid
    EA->>EA: is_totp_enabled(realm, alice) → true
    EA-->>U: 200 OK<br/>{"next_step":"TotpRequired", "session_id":null}

    U->>App: Read current TOTP code
    App-->>U: "482913"

    U->>EA: POST /login?realm={realm}<br/>{"username":"alice","password":"secret","totp_code":"482913"}
    note over EA: TOTP token validated against stored secret
    EA->>SS: upsert_session(…)
    SS-->>EA: OK
    EA-->>U: 200 OK<br/>Set-Cookie: _ea_=<cookie_string><br/>{"next_step":"Authenticated"}

Note: The next_step field in the response tells the client what to do next:

  • "Authenticated" — login complete, cookie issued
  • "TotpRequired" — provide a TOTP code to proceed

See two_factor_authentication.md for the full TOTP enrollment and management flows.


Flow 3 — JWT Bearer Token

Machine-to-machine scenarios (service accounts, CI pipelines, SDKs) use RS256 or ES256 JWT tokens instead of cookies.

JWT Bearer Token sequence

sequenceDiagram
    autonumber
    participant C as Client (Service)
    participant IDP as External Identity Provider
    participant EA as Authentication Verifier
    participant API as Your API
    participant SS as Session Store

    C->>IDP: Request JWT (RS256/ES256)
    note over IDP: Signs JWT with private key<br/>sub=client_id, aud=api_audience
    IDP-->>C: Bearer eyJhbG…

    C->>API: GET /api/resource<br/>Authorization: Bearer eyJhbG…
    note over API: JwtAuth middleware
    API->>EA: GET /.well-known/jwks.json  (key discovery)
    EA-->>API: JWKS (public keys)
    note over API: Validates JWT signature + exp + aud
    API->>SS: upsert_session(jwt_claims)
    SS-->>API: session_id
    API-->>C: 200 OK  {"data": …}

Key discovery

The server exposes a JWKS endpoint at GET /.well-known/jwks.json which returns the RSA/EC public keys used to verify tokens. The JWT middleware caches this document and refreshes it on a configurable interval.

JWT requirements

ClaimRequiredDescription
subYesSubject (user or service identifier)
audYesMust match the configured audience
expYesExpiration time (Unix seconds)
issYesIssuer URL matching JWKS discovery URL

Flow 4 — Client Certificate (mTLS)

For high-assurance scenarios, clients authenticate using an EC P-256 client certificate over mutual TLS.

Client Certificate sequence

sequenceDiagram
    autonumber
    participant C as Client
    participant EA as Authentication Verifier (TLS)
    participant SS as Session Store
    participant API as Your API

    note over C,EA: TLS handshake — client presents certificate
    C->>EA: POST /login?realm={realm}  (+ client cert in TLS layer)
    note over EA: extract_peer_certificate middleware<br/>extracts DN from certificate
    EA->>EA: Validate certificate chain against CA
    EA->>SS: upsert_session(cert_subject_claims)
    SS-->>EA: OK
    EA-->>C: 200 OK<br/>Set-Cookie: _ea_=<cookie_string>

    C->>API: GET /api/resource<br/>Cookie: _ea_=<cookie_string>
    API-->>C: 200 OK  {"data": …}

Flow 5 — AppRole (Machine Credentials)

AppRole is the recommended flow for server-to-server and SPIRE workloads. An operator provisions a role out-of-band; the service uses its role_id + secret_id pair to obtain an opaque app token (X-Vault-Token).

AppRole sequence

sequenceDiagram
    autonumber
    participant O as Operator (Admin)
    participant S as Service / SPIRE
    participant EA as Authentication Verifier
    participant DB as Database

    O->>EA: POST /auth/approle/role/{name}<br/>Cookie: _ea_=<admin session><br/>{ token_ttl: 3600, bind_secret_id: true }
    EA-->>O: 204 No Content

    O->>EA: GET /auth/approle/role/{name}/role-id<br/>Cookie: _ea_=<admin session>
    EA-->>O: { data: { role_id: "a5d7…" } }

    O->>EA: POST /auth/approle/role/{name}/secret-id<br/>Cookie: _ea_=<admin session>
    EA-->>O: { data: { secret_id: "b7c4…", secret_id_accessor: "9f8e…" } }

    O->>S: Provision role_id + secret_id (out-of-band)

    S->>EA: POST /auth/approle/login<br/>{ role_id: "a5d7…", secret_id: "b7c4…" }
    note over EA: 1. Resolve role by role_id<br/>2. Verify SHA-256(secret_id) hash<br/>3. Decrement num_uses_remaining<br/>4. Issue token (SHA-256 hash stored)
    EA->>DB: Insert app_tokens row (hash only)
    DB-->>EA: OK
    EA-->>S: { auth: { client_token: "hvs.…", renewable: true, lease_duration: 3600 } }

    S->>EA: GET /auth/token/lookup-self<br/>X-Vault-Token: hvs.…
    EA-->>S: { data: { id: "hvs.…", ttl: 3541, renewable: true, … } }

Secret ID consumption

Each successful login consumes one use of the secret_id. With num_uses = 1 (the SPIRE default), the secret ID is permanently invalidated after the first login. Generate a new one before the service restarts.

For the full endpoint reference, field descriptions, and management operations, see app_auth_api.md.


Flow 6 — Kubernetes Service-Account

Kubernetes workloads authenticate by presenting their pod-mounted service-account JWT. No operator-provisioned secret is required — the JWT is the credential.

Kubernetes sequence

sequenceDiagram
    autonumber
    participant O as Operator (Admin)
    participant P as Pod (workload)
    participant K8s as Kubernetes API Server
    participant EA as Authentication Verifier

    O->>EA: POST /auth/kubernetes/role/{name}<br/>Cookie: _ea_=<admin session><br/>{ jwks_url, bound_sa_names, bound_sa_namespaces, token_ttl }
    EA-->>O: 204 No Content

    P->>K8s: Read projected token<br/>/var/run/secrets/kubernetes.io/serviceaccount/token
    K8s-->>P: Service-account JWT (signed by Kubernetes)

    P->>EA: POST /auth/kubernetes/login<br/>{ role: "my-k8s-role", jwt: "<SA JWT>" }
    note over EA: 1. Fetch JWKS from role's jwks_url<br/>2. Select key by kid from JWT header<br/>3. Validate signature, exp, nbf<br/>4. Parse sub: system:serviceaccount:<ns>:<name><br/>5. Check sa_name against bound_sa_names<br/>6. Check namespace against bound_sa_namespaces
    EA-->>P: { auth: { client_token: "hvs.…", lease_duration: 3600 } }

JWKS availability

A JWKS endpoint outage blocks new Kubernetes logins for the duration of the outage. Existing tokens remain valid until their TTL expires.

For the full endpoint reference and field descriptions, see app_auth_api.md.


Flow 7 — Token Self-Service

All app tokens — regardless of which method produced them (AppRole or Kubernetes) — expose three self-service endpoints. These require a valid X-Vault-Token header.

Token self-service sequence

sequenceDiagram
    autonumber
    participant S as Service
    participant EA as Authentication Verifier

    note over S: Service holds a token from AppRole or Kubernetes login

    S->>EA: GET /auth/token/lookup-self<br/>X-Vault-Token: hvs.…
    EA-->>S: { data: { id: "hvs.…", entity_id: "spire-server",<br/>           renewable: true, ttl: 3541, … } }

    note over S: Token nearing expiry — renew it
    S->>EA: POST /auth/token/renew-self<br/>X-Vault-Token: hvs.…
    EA-->>S: { auth: { client_token: "hvs.…",<br/>           lease_duration: 3600, renewable: true } }

    note over S: Service is shutting down — revoke token
    S->>EA: POST /auth/token/revoke-self<br/>X-Vault-Token: hvs.…
    EA-->>S: 204 No Content

App token lifecycle

stateDiagram-v2
    [*] --> Active : POST /auth/approle/login or<br/>POST /auth/kubernetes/login
    Active --> Active : POST /auth/token/renew-self<br/>(resets TTL to lease_duration)
    Active --> Expired : token_ttl elapsed
    Active --> Revoked : POST /auth/token/revoke-self
    Expired --> [*] : Rejected with 403 on next lookup-self
    Revoked --> [*] : Rejected with 403 immediately

For the full endpoint reference, see app_auth_api.md.


Session Lifecycle

stateDiagram-v2
    [*] --> Active : POST /login (credentials valid)
    Active --> Active : Any authenticated request<br/>(resets stale timer)
    Active --> Expired : session_max_age_seconds elapsed
    Active --> Revoked : DELETE /sessions (explicit logout)
    Expired --> [*] : Stale-session collector removes it
    Revoked --> [*] : Immediately rejected by CookieAuthSameServer

Session parameters (per realm)

ParameterDefaultDescription
session_max_age_seconds3600Maximum absolute lifetime of a session
session_max_stale_age_seconds3600Maximum idle time before a session is considered stale

Sessions are stored in the configured session backend (SQLite / PostgreSQL / MySQL / Redis).

Explicit logout

To invalidate a session:

DELETE /sessions
Content-Type: application/json

{"session_ids": ["<session_id>"]}

Administratively, a super admin can revoke all sessions for a realm:

DELETE /sessions/realms/{realm_id}

API Endpoint Reference

Human / session authentication endpoints

MethodPathDescriptionAuth required
POST/login?realm={realm}Authenticate and issue a session cookieNo (credentials in body)
GET/whoami?realm={realm}Return the current session's claims as a signed JWTSession cookie
GET/public/versionServer version stringNo
GET/.well-known/jwks.jsonJSON Web Key Set for JWT verificationNo

Machine authentication endpoints

These endpoints produce or manage app tokens (X-Vault-Token). See app_auth_api.md for full request/response detail.

AppRole — login

MethodPathDescriptionAuth required
POST/PUT/auth/approle/loginExchange role_id + secret_id for an app tokenNo (credential is body)
MethodPathDescription
POST/auth/approle/role/{name}Create or update a role
GET/auth/approle/role/{name}/role-idRead the stable role_id
POST/auth/approle/role/{name}/secret-idGenerate a new secret_id
POST/auth/approle/role/{name}/secret-id/destroyInvalidate a secret_id by accessor
DELETE/auth/approle/role/{name}Delete a role
GET/auth/approle/role?list=trueList all role names

Kubernetes — login

MethodPathDescriptionAuth required
POST/auth/kubernetes/loginExchange a K8s service-account JWT for an app tokenNo (credential is body)
MethodPathDescription
POST/auth/kubernetes/role/{name}Create or update a Kubernetes role
DELETE/auth/kubernetes/role/{name}Delete a Kubernetes role

Token self-service (requires X-Vault-Token header)

MethodPathDescription
GET/auth/token/lookup-selfValidate token and return its metadata
POST/auth/token/renew-selfExtend a renewable token's TTL
POST/auth/token/revoke-selfImmediately invalidate the current token

Session JWT — /whoami response

GET /whoami?realm={realm} returns a signed JWT (ClientClaims) whose payload is a flat JSON object containing three groups of claims:

ClaimGroupDescription
subRegistered (RFC 7519 §4.1.2)Username of the authenticated client
expRegisteredExpiry time (Unix seconds)
iatRegisteredIssued-at time (Unix seconds)
rolesAuthorization (RFC 9068 §2.2.3.1)RBAC roles assigned to the user — omitted when empty
as_asPrivateAuth scheme: "up" / "jwt" / "cc" / "f2" / "dc"
as_ridPrivateRealm ID (also used as the OPA domain scope)
as_pkPrivateClient public key PEM — only present for mTLS sessions

The roles claim follows RFC 9068 §2.2.3.1 and RFC 7643 §4.1.2. It is a flat Vec<String> of role names (e.g. ["CryptoOfficer"]). Absence means no roles — downstream OPA policies should treat absence as fail-closed.

Roles are set per credential via the admin API. See the Credential Management section of the client library guide for the UserPass.roles field, and the ClientClaims type reference for the full struct layout and a wire-format example.

Session management endpoints

MethodPathDescriptionAuth required
GET/sessions/{id}Retrieve SessionData by session ID (null when not found)
POST/sessions/{id}Retrieve SessionData and optionally apply a SessionsAction
POST/sessions/realms/{realm}/clientsGet session IDs for a set of clients
DELETE/sessionsDelete sessions by ID list (logout)
DELETE/sessions/expiredPurge all expired sessions
DELETE/sessions/realms/{realm}Revoke all sessions for a realm

Session Actions on POST /sessions/{id}

When fetching a session you can pass an optional sessions_action in the request body to perform a bulk logout as part of the same call. This is the recommended way to implement "log out everywhere else" and "log out everywhere" features.

Request body

{
  "authenticated_clients": [
    { "username": "alice", "auth_scheme": "UsernamePassword" }
  ],
  "sessions_action": "LogoutOtherSessions"
}
FieldTypeDescription
authenticated_clientsVec<AuthenticatedClientScheme>Users whose sessions should be affected by the action
sessions_actionSessionsAction (optional)Action to perform; omit for a plain session lookup

SessionsAction variants

VariantBehaviour
LogoutOtherSessionsDeletes all active sessions for the given clients except the session being queried. Use this to implement "keep this session, log out everywhere else".
LogoutAllSessionsDeletes all active sessions for the given clients, including the queried session. The session data is returned before deletion. Use this to implement "log out everywhere".

Sequence — LogoutOtherSessions

sequenceDiagram
    autonumber
    participant U as Client
    participant EA as Authentication Verifier
    participant SS as Session Store

    U->>EA: POST /sessions/{session_a}<br/>{sessions_action: "LogoutOtherSessions", authenticated_clients: [...]}
    EA->>SS: get_session(session_a)
    SS-->>EA: SessionData for session_a
    EA->>SS: get_sessions_for_clients(authenticated_clients)
    SS-->>EA: [session_a, session_b, session_c]
    EA->>SS: delete_sessions([session_b, session_c])
    SS-->>EA: OK
    EA-->>U: 200 OK  SessionData for session_a
    note over U: session_a still valid<br/>session_b, session_c invalidated

Sequence — LogoutAllSessions

sequenceDiagram
    autonumber
    participant U as Client
    participant EA as Authentication Verifier
    participant SS as Session Store

    U->>EA: POST /sessions/{session_a}<br/>{sessions_action: "LogoutAllSessions", authenticated_clients: [...]}
    EA->>SS: get_session(session_a)
    SS-->>EA: SessionData for session_a
    EA->>SS: get_sessions_for_clients(authenticated_clients)
    SS-->>EA: [session_a, session_b, session_c]
    EA->>SS: delete_sessions([session_a, session_b, session_c])
    SS-->>EA: OK
    EA-->>U: 200 OK  SessionData for session_a (returned before deletion)
    note over U: all sessions invalidated

Request Authentication Decision Diagram

The following shows how the middleware stack decides whether to admit or reject an incoming request:

flowchart TD
    A[Incoming request] --> B{Has _ea_ cookie?}
    B -- No --> C{Has Bearer token?}
    B -- Yes --> D[CookieAuthSameServer]
    D --> G{Session found
in store?}
    G -- No --> F[401 Unauthorized]
    G -- Yes --> H[SessionData → ClientClaims injected]
    H --> I{AdminAuth required?}
    I -- Yes --> J[DB: find_admins_by_auth_scheme]
    J --> K{Admin record exists?}
    K -- No --> F
    K -- Yes --> L[Admin injected → handler runs]
    I -- No --> M[Handler runs with claims only]

    C -- No --> N{Has Basic Auth header?}
    C -- Yes --> O[JwtAuth middleware]
    O --> P{JWT sig + exp + aud valid?}
    P -- No --> F
    P -- Yes --> H
    N -- No --> S{Has X-Vault-Token header?}
    N -- Yes --> Q[UsernamePasswordAuth]
    Q --> R{Argon2id hash matches?}
    R -- No --> F
    R -- Yes --> H

    S -- No --> F
    S -- Yes --> T[AppTokenExtract middleware]
    T --> U{Token hash found
in DB, not expired?}
    U -- No --> F403[403 Forbidden]
    U -- Yes --> V[AppTokenClaims injected
→ self-service handler]

Note: AppRole login (POST /auth/approle/login) and Kubernetes login (POST /auth/kubernetes/login) are unauthenticated — the credential is the request body. They are not represented above.