Complete reference for all HTTP endpoints exposed by the Authentication Verifier.
All endpoints are served over HTTPS only. The base URL is https://{host}:{port}.
- Public Endpoints
- Authentication — Login and Session Claims
- SAML 2.0 Single Sign-On
- Session Management
- Machine Authentication (AppRole / Kubernetes / Token)
- Realm Administration
- Admin Administration
- Credential Management
- TOTP Management
- Common Response Codes
- Authentication for Admin Endpoints
- Data Types
No authentication required.
Returns the server version string.
HTTP/1.1 200 OK
Content-Type: text/plain
"0.1.2"
Returns the JSON Web Key Set containing the server's public key(s) for JWT signature verification.
Note: Only available in test builds. Production deployments expose JWKS through the Identity Provider's own JWKS endpoint.
{
"keys": [
{
"kty": "EC",
"crv": "P-256",
"x": "...",
"y": "...",
"use": "sig"
}
]
}Returns the JSON Web Key Set for the certificate signing key used by POST /certify —
deliberately a separate document from /.well-known/jwks.json, since certificates are
signed with a key distinct from the session JWT key. Same response shape as above.
Response — 404 Not Found — certificate signing is not configured on this server.
Authenticate a client and issue a session cookie. The authentication method is determined by the Authorization header or the request body.
Authentication methods:
| Method | How to supply credentials |
|---|---|
| Username/Password | Authorization: Basic <base64(username:password)> |
| JWT Bearer | Authorization: Bearer <jwt_token> |
| Client Certificate | Client certificate in TLS handshake |
{
"totp_code": "482913"
}| Field | Type | Description |
|---|---|---|
totp_code |
String | null |
TOTP verification code for 2FA step |
Response — 200 OK
{
"next_step": "Authenticated",
"session_id": "550e8400-e29b-41d4-a716-446655440000"
}Set-Cookie header: _ea_=<cookie_string>; HttpOnly; Secure; SameSite=Strict
| Field | Type | Description |
|---|---|---|
next_step |
"Authenticated" | "TotpRequired" | "ChangePassword" |
What the client should do next |
session_id |
String | null |
UUID of the new session (only present when Authenticated) |
Response — 401 Unauthorized — credentials invalid or missing.
Response — 200 OK with "next_step": "TotpRequired" — primary credentials valid but TOTP code is required. Re-submit the request with totp_code.
Returns the JWT claims for the currently authenticated session.
Authentication: Session cookie (_ea_ cookie).
Response — 200 OK
{
"iss": "auth-auth",
"sub": "alice",
"aud": ["my-service"],
"exp": 1745000000,
"iat": 1744996400,
"as_as": "up",
"as_rid": "my-service"
}See Data Types — ClientClaims for the full field reference.
Response — 401 Unauthorized — no valid session cookie.
Certify a verification (public) key under the identity of the currently authenticated
session. Returns a long-lived, ES256-signed certificate binding the caller's realm_id,
sub and auth_scheme to the supplied key — signed with a dedicated certificate signing
key, entirely separate from the session JWT key, so the certificate can never be presented
back as a session cookie/token.
Authentication: Session cookie (_ea_ cookie).
Request body:
{
"verification_key": "-----BEGIN PUBLIC KEY-----\n...\n-----END PUBLIC KEY-----",
"claims": ["as_registrant"],
"exclude_sub": false
}| Field | Type | Description |
|---|---|---|
verification_key |
String |
PEM-encoded public key to certify |
claims |
String[] |
Optional, default []. Names of the session's extra claims to copy into the certificate. Only names both listed here and present in the session are included — nothing is copied by default. |
exclude_sub |
bool |
Optional, default false. Omit sub from the certificate. Only allowed when claims yields at least one claim actually present in the session — a certificate can't identify nobody. |
Response — 200 OK
{
"certificate": "eyJhbGciOiJFUzI1NiJ9..."
}certificate is a compact JWS (alg: ES256) whose payload is a
CertificateClaims object.
Response — 400 Bad Request — verification_key missing, empty, not PEM-shaped
(-----BEGIN .../-----END ...), or longer than 8192 bytes; or exclude_sub: true while
the requested/present intersection of claims is empty.
Response — 401 Unauthorized — no valid session cookie.
Response — 500 Internal Server Error — certificate signing is not configured on this
server (no certificate_jwt_params).
Browser endpoints of the SAML Service Provider. They exist only on servers built with the
saml feature and configured with [saml_sp_params]; otherwise they return 404. See
SAML 2.0 single sign-on for setup and the validation rules.
Start a login at the realm's IdP. Meant to be opened by the browser, not called by API clients.
| Query parameter | Required | Description |
|---|---|---|
return_to |
No | Where the browser goes after login: an https URL under one of the realm's allowed_return_origins. Defaults to default_return_url. |
Response — 302 Found — Location is the IdP sign-in URL carrying a signed AuthnRequest
(HTTP-Redirect binding) whose RelayState is the request ID. Also sets
_ea_saml=<request ID>; Secure; HttpOnly; SameSite=None; Path=/; Max-Age=600.
Response — 400 Bad Request — unknown realm, realm without SAML, or return_to not allowed.
Assertion Consumer Service: the IdP's response arrives here through the browser (HTTP-POST
binding, application/x-www-form-urlencoded).
| Form field | Description |
|---|---|
SAMLResponse |
Base64-encoded SAML <Response> |
RelayState |
The request ID from the login redirect; must equal the _ea_saml cookie |
Response — 200 OK — an HTML page sending the browser on to the login's return URL, with
Set-Cookie: _ea_=<cookie_string>; HttpOnly; Secure; SameSite=Strict (auth scheme sa) and
the _ea_saml cookie cleared.
Response — 400 Bad Request — unknown realm or realm without SAML.
Response — 401 Unauthorized — "SAML error: <reason>": the login wasn't started from
this browser, the request is unknown, expired or already used, or the response fails
validation.
This server's SAML 2.0 SP metadata for the realm (entity ID, ACS URL, NameID format, signing
certificate, AuthnRequestsSigned="true"), to import into the IdP.
Response — 200 OK — Content-Type: application/samlmetadata+xml.
Response — 400 Bad Request — unknown realm or realm without SAML.
Retrieve session data by session ID. Returns null (not an error) when the session does not exist or has expired.
Response — 200 OK (session found)
{
"session_id": "550e8400-e29b-41d4-a716-446655440000",
"realm_id": "my-service",
"username": "alice",
"auth_scheme": "up",
"cookie_string": "eyJ…",
"max_age_seconds": 3600,
"max_stale_age_seconds": 1800,
"created_at": 1744996400
}Response — 200 OK (session not found)
nullRetrieve session data and optionally apply a bulk logout action in the same request.
{
"authenticated_clients": [
{ "username": "alice", "auth_scheme": "UsernamePassword" }
],
"sessions_action": "LogoutOtherSessions"
}| Field | Type | Description |
|---|---|---|
authenticated_clients |
AuthenticatedClientScheme[] |
Clients whose sessions may be affected |
sessions_action |
"LogoutOtherSessions" | "LogoutAllSessions" | null |
Action to perform alongside the lookup |
sessions_action values:
| Value | Effect |
|---|---|
"LogoutOtherSessions" |
Delete all sessions for the given clients except the queried one |
"LogoutAllSessions" |
Delete all sessions for the given clients (including the queried one); session data is returned before deletion |
Response — 200 OK — same shape as GET /sessions/{id}.
Return all session IDs for a set of authenticated clients in a realm.
[
{ "username": "alice", "auth_scheme": "UsernamePassword" },
{ "username": "alice", "auth_scheme": "Jwt" }
]Response — 200 OK
{
"session_ids": [
"550e8400-e29b-41d4-a716-446655440000",
"660f9511-f3ac-52e5-c827-557766551111"
]
}Delete sessions by ID.
{
"session_ids": [
"550e8400-e29b-41d4-a716-446655440000"
]
}Response — 204 No Content — empty body.
Delete all expired sessions from the session store.
Response — 204 No Content — empty body.
Delete all sessions for a given realm (administrative bulk logout).
Response — 204 No Content — empty body.
Endpoints for machine-to-machine authentication. These sit under the /auth/ prefix. Login endpoints are unauthenticated (the credential is the request body). Role management endpoints require an active admin session cookie. Token self-service endpoints require a valid X-Vault-Token header.
For complete request/response examples and field descriptions see app_auth_api.md.
Exchange a role_id + secret_id pair for an opaque app token. Also accepts PUT (SPIRE compatibility).
{
"role_id": "a5d7e2f1-0c3b-4a8d-9e6f-1234567890ab",
"secret_id": "b7c4e9d2-1a2b-4c3d-8e5f-fedcba987654"
}secret_id is optional when the role has bind_secret_id: false.
{
"auth": {
"client_token": "hvs.AAAAAQIDBAUGBwgJCgsMDQ4PEBESExQVFhcYGRob",
"renewable": true,
"lease_duration": 3600,
"policies": ["default"],
"metadata": { "role_name": "spire-server" }
}
}Error responses: 400 Bad Request (invalid role_id or secret_id), 403 Forbidden (secret_id exhausted).
Create or update an AppRole role. Requires admin session cookie.
name — role name (unique identifier for this role)
{
"token_ttl": 3600,
"secret_id_ttl": 86400,
"token_policies": ["default"],
"bind_secret_id": true
}| Field | Type | Default | Description |
|---|---|---|---|
token_ttl |
integer | 3600 | Token lifetime in seconds |
secret_id_ttl |
integer | 0 | Secret ID lifetime in seconds; 0 = no expiry |
token_policies |
array | [] |
Policies attached to every issued token |
bind_secret_id |
boolean | true | If false, login succeeds with role_id alone |
Response — 204 No Content
Read the stable role_id for a role. Requires admin session cookie.
{ "data": { "role_id": "a5d7e2f1-0c3b-4a8d-9e6f-1234567890ab" } }Generate a new secret_id for a role. Requires admin session cookie.
{ "ttl": 0, "num_uses": 1 }| Field | Type | Default | Description |
|---|---|---|---|
ttl |
integer | 0 | Per-secret-ID TTL override in seconds; 0 = use role default |
num_uses |
integer | 0 | Max login uses; 0 = unlimited |
{
"data": {
"secret_id": "b7c4e9d2-1a2b-4c3d-8e5f-fedcba987654",
"secret_id_accessor": "9f8e7d6c-5b4a-3c2d-1e0f-abcdef012345"
}
}Save the secret_id — it cannot be retrieved again. Use the secret_id_accessor to destroy it without knowing the value.
Invalidate a secret_id by its accessor. Requires admin session cookie.
{ "secret_id_accessor": "9f8e7d6c-5b4a-3c2d-1e0f-abcdef012345" }Response — 204 No Content
Delete a role and all its secret IDs (cascade). Requires admin session cookie.
Response — 204 No Content
List all AppRole role names. Requires admin session cookie.
{ "data": { "keys": ["spire-server", "mistral-agents"] } }Exchange a Kubernetes service-account JWT for an app token.
{
"role": "my-k8s-role",
"jwt": "<service-account JWT from /var/run/secrets/kubernetes.io/serviceaccount/token>"
}{
"auth": {
"client_token": "hvs.AAAAAQIDBAUGBwgJCgsMDQ4PEBESExQVFhcYGRob",
"renewable": true,
"lease_duration": 3600,
"policies": [],
"metadata": {}
}
}Error responses: 400 Bad Request (invalid JWT or role), 403 Forbidden (SA name / namespace not in allow-list).
Create or update a Kubernetes auth role. Requires admin session cookie.
{
"jwks_url": "https://kubernetes.default.svc/.well-known/jwks.json",
"bound_service_account_names": ["my-app"],
"bound_service_account_namespaces": ["production"],
"token_ttl": 3600
}| Field | Type | Description |
|---|---|---|
jwks_url |
string | URL of the Kubernetes JWKS endpoint |
bound_service_account_names |
array | Allowed SA names; ["*"] = any |
bound_service_account_namespaces |
array | Allowed namespaces; ["*"] = any |
token_ttl |
integer | Token lifetime in seconds |
expected_issuer |
string (optional) | If set, validates JWT iss claim |
bound_audiences |
array (optional) | If set, validates JWT aud claim |
Response — 204 No Content
Delete a Kubernetes role. Requires admin session cookie.
Response — 204 No Content
Validate an app token and return its metadata.
X-Vault-Token: hvs.AAAAAQIDBAUGBwgJCgsMDQ4PEBESExQVFhcYGRob{
"data": {
"id": "hvs.AAAAAQIDBAUGBwgJCgsMDQ4PEBESExQVFhcYGRob",
"entity_id": "spire-server",
"policies": ["default"],
"renewable": true,
"ttl": 3541,
"creation_time": 1753670400
}
}Error response: 403 Forbidden — token missing, invalid, expired, or revoked.
Extend a renewable token's TTL back to its configured lease_duration.
X-Vault-Token: hvs.AAAAAQIDBAUGBwgJCgsMDQ4PEBESExQVFhcYGRob{
"auth": {
"client_token": "hvs.AAAAAQIDBAUGBwgJCgsMDQ4PEBESExQVFhcYGRob",
"renewable": true,
"lease_duration": 3600,
"policies": ["default"],
"metadata": {}
}
}Error response: 403 Forbidden — token not found, expired, or not renewable.
Immediately invalidate the current token.
X-Vault-Token: hvs.AAAAAQIDBAUGBwgJCgsMDQ4PEBESExQVFhcYGRobResponse — 204 No Content
Realm CRUD endpoints live under /admins/realms. Creating, updating, and deleting realms requires super admin privileges. Listing and getting a single realm is accessible to any authenticated admin (filtered to their administered realms).
Create a new realm. Super admin only.
{
"id": "my-service",
"auth_params": {
"username_password_params": {
"allow_expired_passwords": false
}
},
"session_max_age_seconds": 3600,
"session_max_stale_age_seconds": 1800
}Response — 201 Created — created Realm object.
Response — 409 Conflict — realm ID already exists.
Retrieve a realm by ID. Realm admins may only retrieve realms they administer.
Response — 200 OK
{
"id": "my-service",
"auth_params": { "username_password_params": { "allow_expired_passwords": false } },
"session_max_age_seconds": 3600,
"session_max_stale_age_seconds": 1800
}Response — 404 Not Found — realm does not exist.
Replace a realm's configuration. Super admin only. Returns the updated realm.
Response — 200 OK — updated Realm object.
Delete a realm and all associated data. Super admin only.
Response — 204 No Content — empty body.
List all realms. Super admins see all realms; realm admins see only their administered realms.
Response — 200 OK
[
{
"id": "_",
"auth_params": {},
"session_max_age_seconds": 3600,
"session_max_stale_age_seconds": 3600
},
{
"id": "my-service",
...
}
]List all username/password credentials across every realm. Super admin only.
Response — 200 OK — array of UserPass objects. The password_hash field is always returned as an empty string.
Create a new Admin record. Super admins may create any admin. Realm admins may create an admin only if every realm in the new admin's realms list is one they administer.
{
"id": "bob",
"realms": ["my-service"],
"userpass": "bob",
"jwt": null,
"fido2": null,
"digital_credentials": null,
"client_certificate": null,
"totp_enabled": false,
"totp_secret": null,
"totp_auth_url": null
}| Field | Type | Description |
|---|---|---|
id |
String |
Unique user identifier |
realms |
String[] |
Realms this user administers. Use ["_"] for super admin. |
userpass |
String | null |
Username key referencing the userpass credential table |
jwt |
String | null |
JWT subject identifier |
fido2 |
String | null |
FIDO2 identifier (future) |
digital_credentials |
Object | null |
Key-value map of digital credential identifiers (future) |
client_certificate |
String | null |
Client certificate identifier |
totp_enabled |
bool | null |
Whether TOTP is active for this user |
totp_secret |
String | null |
Base32-encoded TOTP secret (read-only via API) |
totp_auth_url |
String | null |
otpauth:// URL for QR code enrollment (read-only) |
Response — 201 Created — created Admin object.
Retrieve an admin by ID.
Response — 200 OK — Admin object.
Response — 404 Not Found — admin does not exist.
Replace an admin record. Returns the updated admin.
Response — 200 OK — updated Admin object.
Delete an admin record.
Response — 204 No Content — empty body.
List all admins. Super admin only.
Response — 200 OK — array of Admin objects.
Add a realm to an admin's realms list.
Response — 200 OK — updated Admin object.
Remove a realm from an admin's realms list.
Response — 200 OK — updated Admin object.
All /realms/{realm} endpoints require a valid session from a user who administers the given realm (see authorization_and_administration.md).
Create a username/password credential in a realm.
{
"realm": "my-service",
"username": "alice",
"password_hash": "",
"password_input": { "plaintext": "secret" },
"change_password": false,
"roles": ["CryptoOfficer"],
"extra_claims": { "as_registrant": "acme-corp" }
}| Field | Type | Description |
|---|---|---|
realm |
String |
Realm ID (must match path parameter) |
username |
String |
Credential username |
password_hash |
String |
Server-computed stored password hash (a PHC string). Ignored on input — use password_input instead. Always empty in responses. |
password_input |
object | absent |
How to set the password: { "plaintext": "<password>" }, hashed with Argon2id by the server before storage; or { "hashed": "<PHC string>" }, stored as-is — for provisioning a credential without ever sending its plaintext to this server, and must use exactly this server's own Argon2id cost parameters (m=65536,t=3,p=4). Rejected with 400 if it doesn't parse as PHC, isn't argon2id, or its cost parameters don't match exactly. Required on create. |
change_password |
bool |
When true, the next login returns "ChangePassword" next step |
roles |
String[] |
Optional, default []. RBAC roles assigned to this user, emitted in the session JWT's roles claim. |
extra_claims |
object | null |
Optional. Arbitrary extra claims merged into the session JWT on username/password login, and selectively exposed via POST /certify's claims field. Rejected with 400 if it also uses a reserved claim name, or if its serialized size exceeds 4 KiB. |
Response — 201 Created — created UserPass object.
Response — 409 Conflict — a credential for this (realm, username) already exists,
including on a byte-for-byte resubmission of the same data: telling the two cases apart
would require verifying the submitted password against the stored hash on this
unrate-limited, admin-authenticated endpoint, turning it into a password-guessing oracle.
Use PUT /realms/{realm_id}/userpass/{username} to update an existing credential.
List all credentials in a realm.
Response — 200 OK — array of UserPass objects. The password_hash field is always empty.
Retrieve a single credential.
Response — 200 OK — UserPass object. The password_hash field is always empty.
Response — 404 Not Found — credential does not exist.
Update a credential (typically to change the password or set change_password/roles).
Omit password_input to keep the existing password unchanged and only apply
roles/change_password.
Response — 200 OK — updated UserPass object.
Delete a credential.
Response — 204 No Content — empty body.
See two_factor_authentication.md for the full enrollment flow.
Generate a TOTP secret for a user. The secret is not stored at this point — it is only persisted after a successful verification call.
Query parameters: realm={realm_id}
{
"username": "alice",
"issuer": "My Application"
}Response — 200 OK
{
"secret_base32": "JBSWY3DPEHPK3PXP",
"otpauth_url": "otpauth://totp/My%20Application:alice?secret=JBSWY3DPEHPK3PXP&issuer=My%20Application&algorithm=SHA1&digits=6&period=30"
}Verify a TOTP code and enable TOTP for the user. Call this after generate to confirm the user has enrolled their authenticator app.
Query parameters: realm={realm_id}
{
"username": "alice",
"token": "482913",
"secret": "JBSWY3DPEHPK3PXP",
"issuer": "My Application"
}Response — 200 OK — empty body. TOTP is now active for the user.
Response — 400 Bad Request — TOTP token is invalid.
Disable TOTP for a user.
Query parameters: realm={realm_id}
Response — 200 OK — empty body.
| Code | Meaning |
|---|---|
200 OK |
Success |
400 Bad Request |
Malformed request body or invalid parameter |
401 Unauthorized |
Missing or invalid credentials |
403 Forbidden |
Authenticated but not authorised (e.g., realm admin trying to modify another realm) |
404 Not Found |
Resource does not exist |
409 Conflict |
Resource already exists (duplicate ID) |
500 Internal Server Error |
Unexpected server error |
Admin endpoints (/admins, /admins/realms, /realms) require a valid session cookie obtained by logging into the _ realm (super admin) or the target realm (realm admin).
# Log in as super admin
curl --cacert ca.cert.pem \
-X POST "https://localhost:8443/login?realm=_" \
-u "admin:password" \
-c cookies.txt
# Use the session cookie for admin calls
curl --cacert ca.cert.pem \
-b cookies.txt \
https://localhost:8443/admins/realmsSee authorization_and_administration.md for the full authorization matrix.
{
"id": "my-service",
"auth_params": {
"username_password_params": { "allow_expired_passwords": false },
"jwt_params": {
"idp_params": [
{
"jwt_issuer_uri": "https://accounts.google.com",
"jwks_uri": "https://www.googleapis.com/oauth2/v3/certs",
"jwt_audience": "my-client-id.apps.googleusercontent.com"
}
],
"smallest_refresh_interval_seconds": 300
},
"totp_params": { "algorithm": "SHA1", "step": 30 }
},
"session_max_age_seconds": 3600,
"session_max_stale_age_seconds": 1800
}auth_params.saml_params (omitted when unset) enables SAML single sign-on; its fields are
described in SAML 2.0 single sign-on.
{
"id": "alice",
"realms": ["my-service"],
"userpass": "alice",
"jwt": null,
"fido2": null,
"digital_credentials": null,
"client_certificate": null,
"totp_enabled": false,
"totp_secret": null,
"totp_auth_url": null
}{
"realm": "my-service",
"username": "alice",
"password_hash": "",
"change_password": false,
"roles": ["CryptoOfficer"],
"extra_claims": { "as_registrant": "acme-corp" }
}The
password_hashfield is always returned as an empty string from GET endpoints, andpassword_inputis never returned. Send{ "plaintext": "<password>" }or{ "hashed": "<PHC string>" }inpassword_inputon create/update — seePOST /realms/{realm_id}/userpass.
{
"session_id": "550e8400-e29b-41d4-a716-446655440000",
"realm_id": "my-service",
"username": "alice",
"auth_scheme": "up",
"cookie_string": "eyJ…",
"max_age_seconds": 3600,
"max_stale_age_seconds": 1800,
"created_at": 1744996400
}| Field | Description |
|---|---|
auth_scheme |
"up" username/password · "jwt" JWT · "cc" client cert · "f2" FIDO2 · "dc" digital credentials · "sa" SAML |
{ "username": "alice", "auth_scheme": "UsernamePassword" }auth_scheme values: "UsernamePassword", "Jwt", "ClientCertificate", "Fido2", "DigitalCredentials", "Saml"
The JWT payload returned by GET /whoami:
| Claim | Type | Description |
|---|---|---|
iss |
String |
Issuer |
sub |
String |
Subject (authenticated username) |
aud |
String[] |
Audience list |
exp |
i64 |
Expiration (Unix seconds) |
nbf |
i64 |
Not-before (Unix seconds) |
iat |
i64 |
Issued-at (Unix seconds) |
jti |
String |
JWT ID |
roles |
String[] |
RBAC roles (e.g. ["CryptoOfficer"]), sourced from UserPass.roles for username/password sessions and from the realm's role_attribute for SAML sessions. Absent/empty means no roles (fail-closed in OPA). Empty for all other auth schemes. |
as_as |
String |
Auth scheme used (up/jwt/cc/f2/dc/sa) |
as_rid |
String |
Realm ID |
Any keys from UserPass.extra_claims are also present as top-level claims for
username/password sessions, and the attributes mapped by the realm's attribute_claim_map
for SAML sessions.
The JWT payload returned (base64url-encoded, inside the certificate field) by
POST /certify. Deliberately a different shape from ClientClaims — no roles, no
as_*-prefixed names — and signed with a different key, so it cannot be confused with, or
substituted for, a session token.
| Claim | Type | Description |
|---|---|---|
realm_id |
String |
Realm the caller authenticated to |
sub |
String | absent |
Subject (authenticated username). Absent when the request set exclude_sub: true. |
auth_scheme |
String |
Auth scheme used to establish the session (up/jwt/cc/f2/dc/sa) |
verification_key |
String |
The certified PEM public key |
iat |
i64 |
Issued-at (Unix seconds) |
exp |
i64 |
Expiration (Unix seconds) — realm's certificate_max_age_seconds |
Any names listed in the /certify request's claims field are also present as top-level
claims, copied from the session's own extra claims.