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>
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’suuidcoverage=uuid&coverage=oid— additionally attaches the user’soid, if the user has one. If they don’t, theoidclaim is simply absent/null — requestingoidnever 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 |
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
codeis 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:
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.
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.
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.
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:
- A session lookup is a local read — cheapest possible rejection
- Signature verification is local computation — still no network
- A key refetch costs one network round-trip, and happens at most once per rotation window
- 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.