User Session Authentication
HTTP MCP clients authenticate users through the OAuth authorization server. The official Willow MCP client (stdio) uses a different path: it mints a one-time token (OTT) locally, asks the gateway for a browser sign-in URL that carries that token, and polls until the user finishes SSO. This page documents that flow and the two gateway endpoints that bracket it: POST /generate-auth-url and GET /get-access-token.
The result is the same runtime identity as every other human sign-in. Tool permissions, toolkits, and audit logs resolve against the Willow user who completed SSO.
The two credentials
Every authenticated call from this client carries:
Authorization: <userAccessKey>:<OTT>
The two halves answer different questions:
- Access key — who you are. A stable per-user value created when the user is provisioned (signup, invite, SCIM, or the public Users API) and changed only by an explicit key rotation.
- OTT — proof that you recently signed in. Generated by the client (
crypto.randomBytes(32).toString("hex")) and, after SSO, stored as that user's session token.
Despite the name, the OTT is not single-use and not issued by the server. It is a client-proposed session token. Until SSO binds it to a user it is meaningless; after that it is reused on every call until it expires. Re-authenticating mints a fresh OTT and overwrites the stored one.
Default lifetime is 24 hours. The client also tracks that window locally so it can start a new sign-in before tool calls start failing with 401.
End-to-end sequence
- The client generates a 64-character hex token and keeps it in memory.
- It asks the gateway for a sign-in URL that carries that token, HMAC-signed so the browser cannot change it.
- The user signs in through the organization's identity provider. Connect binds the OTT to the signed-in user's record — never to an email the client supplied.
- The client polls
GET /get-access-tokenwith the bare OTT until the gateway returns that user's access key. - The client persists
{ userAccessKey, token, expiresAt }under~/.willow/token.json(or$WILLOW_HOME/token.json) and usesAuthorization: <userAccessKey>:<OTT>for every later request.
Nothing is written to the user record until step 3. That is what makes the poll a completion signal: get-access-token 401s until SSO finishes, then returns the access key.
POST /generate-auth-url
POST https://{your-org}.mcp-s.com/mcp/generate-auth-url
Authorization: <OTT>
or, when the client already knows the access key (re-auth):
Authorization: <userAccessKey>:<OTT>
A {org}-prefixed variant, POST /{org}/generate-auth-url, exists for gateways that do not infer the organization from the host.
The gateway does not store the OTT here. It signs { userAccessKey?, token, signature } and returns a Connect URL:
https://{connect-host}/{org}/auth?token=<signed>
If the organization has an SSO provider pinned, the URL also includes provider=... so Connect skips the provider picker.
{ "data": { "url": "https://..." } }
GET /get-access-token
GET https://{your-org}.mcp-s.com/mcp/get-access-token
Authorization: <OTT>
Returns the access key of the user whose session token now equals the OTT:
{ "data": { "userAccessKey": "..." } }
Until SSO binds the token, the response is 401. This endpoint does not check expiry; expiry is enforced later on tool calls. The official client polls every 5 seconds for up to 60 attempts (~5 minutes).
Unlike generate-auth-url, this endpoint does not split on :. The entire Authorization value is looked up as a session token, so it must be the bare OTT. Sending userAccessKey:OTT never matches and the poll fails until it times out.
After sign-in
Tool calls authenticate with both halves. The gateway resolves the user by access key, then checks that the OTT matches the stored session token and has not expired.
| Status | Meaning |
|---|---|
401 | The token is missing, does not match, or has expired — the client should re-authenticate |
403 | The credentials are valid but the user is not allowed (for example, SCIM-deactivated) — do not re-authenticate |
The official client treats 401 as a signal to start startReAuthFlow: it clears the access key, mints a new OTT, deletes the saved token file, opens a fresh sign-in URL, and restarts polling. Two guards prevent a storm of browser tabs — an in-process lock, plus a disk-persisted 2-hour throttle that holds across restarts and concurrent client processes.
SCIM deactivation also invalidates the session out of band by clearing the stored token.
Flows that skip the browser
The official client (@mcp-s/mcp) picks an auth mode at startup:
| Mode | Trigger | OTT source | Browser? |
|---|---|---|---|
| Auto-authenticate | AUTO_AUTHENTICATE_TOKEN set | the env var itself | no |
| Explicit access key | USER_ACCESS_KEY set | freshly generated, never bound | no |
| Saved token | ~/.willow/token.json present | reused from disk | no |
| Unauthenticated | none of the above | freshly generated | yes — only the authenticate tool is exposed |
AUTO_AUTHENTICATE_TOKEN is a shared organization secret accepted in place of a user's session token, with no expiry check. Machine users are a different identity entirely: their access key behaves like a client id and their secret like a pre-provisioned session token. See How Identities Authenticate and Programmatic Gateway Access.