Skip to main content

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 keywho 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.
  • OTTproof 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

  1. The client generates a 64-character hex token and keeps it in memory.
  2. It asks the gateway for a sign-in URL that carries that token, HMAC-signed so the browser cannot change it.
  3. 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.
  4. The client polls GET /get-access-token with the bare OTT until the gateway returns that user's access key.
  5. The client persists { userAccessKey, token, expiresAt } under ~/.willow/token.json (or $WILLOW_HOME/token.json) and uses Authorization: <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).

Header format

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.

StatusMeaning
401The token is missing, does not match, or has expired — the client should re-authenticate
403The 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:

ModeTriggerOTT sourceBrowser?
Auto-authenticateAUTO_AUTHENTICATE_TOKEN setthe env var itselfno
Explicit access keyUSER_ACCESS_KEY setfreshly generated, never boundno
Saved token~/.willow/token.json presentreused from diskno
Unauthenticatednone of the abovefreshly generatedyes — 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.