Sessions and Tokens
Vouch sessions are time-limited, DPoP-bound OAuth 2.0 access tokens (ES256 JWTs per RFC 9068) that prove recent hardware presence verification.
Session Lifecycle
- Creation —
vouch loginperforms FIDO2 assertion with YubiKey touch + PIN - Active — Access token stored in agent memory, valid for 8 hours (default)
- Usage — Credential helpers exchange the access token for service-specific credentials
- Expiry — Session expires automatically after the configured duration
- Revocation —
vouch logoutexplicitly ends the session
Session Duration
Default: 8 hours. Configurable via:
VOUCH_SESSION_HOURS=8
Where sessions live
Server-side, every session is a database record holding a hash of the token, never the token itself. The token exists in full only on the client.
On the client, the access token is held in the vouch-agent process and, as a fallback, in files
under the user’s XDG directories. That is client-side territory and is documented with the CLI at
vouch.sh/docs — it is not something you configure or operate on the
server.
Expiry and cleanup
Expired sessions are cleaned up automatically by a background task:
# Cleanup interval in minutes (default: 15, set to 0 to disable)
VOUCH_CLEANUP_INTERVAL=15
Security Properties
- Presence-bound — Every session traces to a FIDO2 assertion with user verification
- Time-limited — Sessions cannot be renewed; a new login is required after expiry
- DPoP-bound — Access tokens are bound to the client’s DPoP key; token theft without the key is useless
- Non-transferable — Sessions are bound to the client that created them
- Audience-restricted — Tokens narrowed to a specific resource are rejected at every other resource (see below)
- Audited — Every session creation and usage is logged
Audience Enforcement (RFC 8707 Resource Indicators)
Access tokens carry an aud (audience) claim per RFC 9068. By default the
audience equals the requesting client_id and the token is valid at every
Vouch resource endpoint — this covers all standard flows (vouch login,
browser sessions, device flow, client credentials).
A client may instead narrow a token to a specific resource with the RFC 8707
resource parameter: at the authorization endpoint, on any token request
(client credentials, device code, and FIDO2 assertion), or with
audience/resource at token exchange (RFC 8693). Vouch’s
resource endpoints (/v1/credentials/*, /v1/keys, /api/v1/*, RFC 7592
client management) enforce that narrowing: a narrowed token is accepted only
when its audience names this deployment (same scheme, host, and port as the
configured base URL) and its path covers the request at a path-segment
boundary. An audience of the deployment root (the base URL itself) covers
every endpoint; {base_url}/v1/keys covers /v1/keys and everything below
it, but nothing else. Requests failing the check receive 401 invalid_token
with the standard WWW-Authenticate challenge, and the rejection is logged
with the client ID, audience, and request path.
Per their RFCs, the authorization-server endpoints remain audience-agnostic:
/oauth/userinfo accepts tokens from any client, and /oauth/introspect and
/oauth/revoke answer about any token the server issued. Token exchange
accepts narrowed subject tokens, since re-scoping them is its purpose, but
keeps them narrowed, the way it keeps a bound subject token bound. Without an
audience or resource the exchanged token carries the subject’s audience,
and a request for an audience Vouch itself accepts (the exchanging client’s
default, or any resource on this deployment) is refused with
invalid_request unless it is the subject’s own. A narrowed token can
therefore be re-scoped to another external service, but never turned back
into one Vouch’s endpoints accept.
The __Host-vouch_session cookie holds only a browser session: a token
issued to this deployment itself (browser sign-in, enrollment, and
certification all mint one), covering the whole deployment, and not
sender-constrained. The UI pages and /oauth/authorize treat any other token
in the cookie, such as an access token issued to an OAuth client or narrowed
to a resource, as signed out.
Clients registered without resource_uris may request any resource value
at issuance. This is safe under enforcement: a token narrowed to an external
resource server is less usable at Vouch, not more — it can only be spent at
the external service it names. Registering resource_uris additionally
restricts which values a client may request at all. That includes the default:
an RFC 8693 exchange for an ID token with no audience gets aud set to the
issuer, so a client with resource_uris must list the issuer to use it.
resource_uris entries are compared as exact strings, because the value
becomes the token’s aud and RFC 7519 §2 compares those “as case-sensitive
strings with no transformations or canonicalizations applied”. Register the
issuer exactly as /.well-known/openid-configuration publishes it, with no
trailing slash: https://vouch.example.com/ does not match.
Scopes
Vouch supports two scopes, openid and email. Unknown scope values in a
request are ignored. RFC 6749 §3.3 asks the server to “document its scope
requirements and default value”; the defaults when a request omits scope are:
| Grant | Default when scope is omitted |
|---|---|
Authorization code (/oauth/authorize, /oauth/par) | openid |
FIDO2 assertion (vouch login) | openid email |
Device authorization (vouch enroll) | openid email. The requested scope is ignored, so this is always the grant |
| Token exchange (RFC 8693) | The subject token’s scope; openid when the subject token carries none |
| Client credentials | No scope. openid and email describe a user and are never granted to a client acting for itself |
Every token response that grants a scope reports it in scope. A request
whose scope leaves nothing to grant — only unknown values, or only
openid/email on the client-credentials grant — is refused with
invalid_scope, because an empty scope cannot be reported.