The Login Flow

The Login Flow

This guide explains the full authentication cycle against Vault using no SDK and no specific language — just HTTP, redirects, and a queue of if-else checks. Once you understand the flow, implementing it in any language or framework is a mechanical translation.

The flow has three parts: a setup phase that happens once at service startup, a redirect phase that sends unauthenticated users to Vault, and a request phase that runs on every incoming request.

Phase 1 — Startup

Store your client credentials

Before anything else, your service needs its client credentials available in configuration — an environment variable file, a secrets manager, or your framework’s configuration layer. Whatever mechanism you use, the two values it must provide are:

VAULT_CLIENT_ID=<client_id>
VAULT_CLIENT_SECRET=<client_secret>
Server-side only

Your client_secret authenticates privileged calls to Vault — including the keyset retrieval below. Store it in server-side configuration only, never in frontend code, and never commit it to source control.

Retrieve the public keys

With credentials in place, your service retrieves the public keys from Vault’s keyset endpoint for the availability it is linked to:

Property Value
Method POST
Path /api/v1/client/keyset

Body parameters:

Parameter Required Description
clientId Yes The client’s public identifier
clientSecret Yes The client’s secret

Response:

[{ "kid": "<key_id>", "key": "<public_key>" }]

A valid Vault-signed token carries a kid in its header, and that kid must correspond to one of the entries in your keyset — that’s how you know which public key to verify the token’s signature against. Verification itself never requires a round-trip to Vault.

Hold the keys in memory for the lifetime of the process.

Fetching keys once at startup means token verification is fast: a local cryptographic check with no network involved. There is exactly one case where you fetch them again — key rotation, covered below.

Phase 2 — Redirecting to Vault

Vault follows the classic OAuth2 flow with some nice quality of life improvements.

The redirect URL

The Vault login page is the OAuth gateway your unauthenticated users get sent to. Construct it by appending the following query parameters to the gateway URL:


{vault-base-url}/login?redirect_uri=<callback-url>&client_id=<client_id>&coverage=uuid&coverage=oid&state=<random-string>

Parameter Required Meaning
client_id ✅ Identifies your application to Vault
redirect_uri ✅ The endpoint on your side that receives the session code
coverage ❌ Which user identifiers Vault embeds in the tokens. Defaults to uuid; add another coverage param to also include the user’s object ID
state ❌ Random string echoed back on callback — an extra security layer against CSRF attacks

Coverage and user identity

Every user in Vault has a uuid as their primary key. Optionally, a user can also have an object ID (oid) attached — useful when you correlate Vault users with entities in your own distributed systems.

The coverage parameter controls which identifiers end up in the issued access token. Multiple coverage values are sent as separate query parameters, not comma-separated:

  • coverage=uuid (default) — tokens carry the user’s uuid
  • coverage=uuid&coverage=oid — additionally attaches the user’s oid, if the user has one. If they don’t, the oid claim is simply absent/null — requesting oid never fails a login for users who lack one

Only uuid and oid are valid coverage values. Any other value is rejected with an error before the login proceeds.

Generate state as a random string per login attempt. Vault stores the state in its session store and verifies it on callback. If the returned state doesn’t match, Vault will reject the exchange.

When a request arrives without a valid session, respond with a redirect (HTTP 302) to this URL and let Vault walk the user through registration or authentication.

The state parameter (optional)

state is an optional security parameter that provides CSRF protection during the authorization flow.

Behavior With state Without state
Validation Vault validates that the callback state matches the original redirect No validation — Vault accepts any callback
CSRF risk Mitigated by Vault’s server-side check Client accepts all responsibility
Recommendation ✅ Recommended for production ⚠️ Acceptable for dev/testing only
State is optional, CSRF is not

If you omit state from the redirect URL, Vault will not validate the callback — you’re protecting against CSRF attacks on your own. For production systems, always include a random state value and verify the match before exchanging the code.

The callback endpoint

After the user authenticates, Vault redirects them back to your redirect_uri with two query parameters:

The redirect back to your callback

Property Value
Method GET
Path <your-callback-url> (the redirect_uri you sent)

Query parameters:

Parameter Always present Description
code Yes The session code — exchange it via the callback endpoint
state Only if you sent one Echoed back verbatim; validate it matches your original value

Your callback handler exchanges the code for a session object with a form-encoded POST:

Exchange the code for a session

Property Value Description
Method POST —
Path /oauth/callback Relative to the Vault base URL
Content-Type application/x-www-form-urlencoded The body is form-encoded, not JSON

Body parameters:

Parameter Required Description
grant_type Yes Must be authorization_code
code Yes The session code received on the redirect back
state Only if you sent one Validated only when state was present in the original redirect
  • The code is single-use and short-lived, so exchange it as soon as the callback fires.

If the exchange succeeds, you receive the session object: an access_token, a refresh_token, and the token metadata. Store this session server-side — in an encrypted cookie or a session store — and never expose the tokens to the browser.

Once stored, the user is logged in. From here on, every subsequent request runs through the verification chain below.

Here is a diagram that summarize this Phase:

Vault OAuth2 Flow
sequenceDiagram autonumber participant U as User (browser) participant C as Client participant V as Modikit Vault C->>U: 302 Redirect to auth gateway U->>V: GET /login (client_id, redirect_uri, state) V-->>U: Login / registration form U->>V: Submit credentials V-->>U: 302 Redirect to redirect_uri (code) U->>C: GET /callback?code=... C->>V: Exchange code for session V-->>C: 200 OK (Session Object)

Phase 3 — Every Request

When a request arrives at a protected route, evaluate the following chain of checks, in order. Conceptually it is just a queue of if-elses.

flowchart TD A[Request to a protected route] --> B{Session present?} B -- no --> R[Redirect to Vault login] B -- yes --> C[Verify access_token with cached public keys] C -- ok --> OK1[Return the protected resource] C -- fail --> D[Refetch public keys from Vault, verify again] D -- ok --> OK2[Return the protected resource] D -- fail --> E[Refresh session using refresh_token] E -- ok --> OK3[Return the protected resource] E -- fail --> F[Invalidate the session] style R fill:#dc3545,color:#fff style F fill:#dc3545,color:#fff style OK1 fill:#198754,color:#fff style OK2 fill:#198754,color:#fff style OK3 fill:#198754,color:#fff

Step 1 — No session? Redirect

Inspect your session store — a cookie, a bearer token, whatever your transport carries. If the request carries no session at all, don’t run any verification. Redirect the user to the Vault login URL, constructed as described in Phase 2.

Step 2 — Verify the access token

With a session present, read the kid from the token’s header and locate the matching public key in your cached keyset. Then verify the token’s signature with that key. This is a local operation — no network call to Vault.

If the signature checks out, the request is authenticated: return the protected resource.

A token whose kid matches no entry in your keyset is a special kind of verification failure — it means the token was signed with a key you don’t hold yet. That case is exactly what Step 3 handles.

Step 3 — Verification failed? Refetch the keys

There is exactly one benign reason a well-formed token can fail verification: the token was signed with a newer key than the ones you cached at startup — visible as a kid you have never seen. Vault rotates the keys behind its availabilities, and your process may hold keys from before the rotation.

So on the first failure, call the keyset endpoint again with your client credentials:

Fetch the availability’s keyset

Property Value Description
Method POST —
Path /api/v1/client/keyset Relative to the Vault base URL

Body parameters:

Parameter Required Description
clientId Yes The client’s public identifier
clientSecret Yes The client’s secret — keep it confidential

The clientSecret is long-lived and must be kept secure. It authenticates your service to Vault.

Once fetched, the keyset is cached locally. Re-fetch on key rotation (unseen kid), or periodically if you maintain your own refresh cadence.

Replace your cached keyset with the response and verify the token once more. If verification now passes, the token was indeed signed with a rotated key. Continue serving as normal — and keep the refreshed keys cached, since every other token from this rotation will verify against them.

Refetch sparingly

Refetching keys is a recovery step, not a habit — rotations are not frequent. Beyond the one retry per verification failure, set a minimum interval between refetch attempts: a threshold of 2–3 minutes is plenty to fail fast on genuinely invalid sessions without hammering the endpoint on every failed request.

Step 4 — Refresh the session

If the token still doesn’t verify, it is genuinely stale or invalid — most commonly it has expired. Attempt to renew the session using the refresh token held inside the session object:

Refresh an expired access token

Property Value Description
Method GET —
Path /oauth/refresh Relative to the Vault base URL

Authentication:

Header Value
Authorization Bearer <refresh-token>

If the refresh succeeds, you receive a new session with a fresh access token. Return the protected resource. The renewed tokens carry the same coverage the login was originally requested with — no need to re-specify it.

What’s inside the access token

Vault embeds user data inside the access token itself, for quick access and entity sync across services. If you need user-related data, decode the (verified) access_token payload:


{
realm: <realmId>,
client: <clientId>,
roles: <list of role codes>,
oid: <user ObjectId, if coverage included oid and user has one — else null>,
firstName: <user first name>,
lastName: <user last name>,
avl: <availabilityId>,
email: <user email>,
sub: <user id - uuid>
}

Because tokens are self-contained, services can derive the user’s identity without additional lookups.

Step 5 — Give up

If the refresh fails too, there is nothing left to salvage — do not refetch the keys a third time, as the key cache is not the problem. Invalidate the session entirely, dropping both tokens, and redirect the user to the Vault login page to authenticate again from scratch.

Why this order

Each check in the chain is ordered by cost:

  1. A session lookup is a local read — cheapest possible rejection
  2. Signature verification is local computation — still no network
  3. A key refetch costs one network round-trip, and happens at most once per rotation window
  4. A session refresh costs a round-trip and mints new tokens — only for genuinely expired sessions

Cheap local checks reject or accept the vast majority of requests; network calls only happen in the rare rotation and expiry cases. A service following this order can serve almost all traffic without ever talking to Vault.