Authentication
The platform has four separate credential types, and mixing them up is the most common first-day mistake. Each surface accepts exactly one.
| Surface | Credential | Looks like |
|---|---|---|
Management API (/v1/*) | Org API key | sk_live_… |
| Protocol + credential endpoints | OAuth access token | a JWT, Bearer or DPoP |
Directory sync (/scim/v2/*) | SCIM bearer token | scim_… |
Hosted screens (/login/*, /sessions, /mfa/*, /connect/*) | Session cookie | set by the platform |
Management API keys
Section titled “Management API keys”Every /v1 call authenticates with an org API key as a bearer token:
GET /v1/organizations/org_01J…/members HTTP/1.1Host: acme.oauth.workAuthorization: Bearer sk_live_…Keys are stored only as a SHA-256 hash. The full value is returned once, at creation, and cannot be read back afterwards — not through the API, not through the console. Losing one means minting a replacement and revoking the old.
Each key carries a scope list drawn from the permission set, and is bound either to one org
or to the platform. A key scoped to an org may only act on that org: a request for another org’s
resources fails with 403 forbidden, even when the key holds the right permission.
// POST /v1/organizations/org_01J…/api_keys{ "name": "ci-deploy", "scopes": ["credentials:issue", "credentials:read"] }A key can only mint keys at or below its own privilege. Requesting a scope the calling key does not
hold returns 403 insufficient_scope, and requesting a scope that is not a known permission returns
400 invalid_scope — so a typo fails loudly instead of creating a key that silently grants nothing.
Keys record a last_used_at, written at roughly five-minute resolution to keep the read path cheap.
Use it to find keys nobody is calling any more, not to audit individual requests — the
audit log is the record of what a key actually did.
Access tokens
Section titled “Access tokens”The protocol surface — /userinfo, /credentials/*, /mcp, /introspect — takes an OAuth access
token issued by /token. Tokens are EdDSA-signed JWTs, and a resource server should
verify them against the JWKS resolved from the token’s own iss:
GET /userinfo HTTP/1.1Host: acme.oauth.workAuthorization: Bearer eyJhbGciOiJFZERTQSIs…If the token was issued under a DPoP proof, the scheme changes to DPoP and every request
must carry a fresh proof bound to the same key:
GET /userinfo HTTP/1.1Authorization: DPoP eyJhbGciOiJFZERTQSIs…DPoP: eyJ0eXAiOiJkcG9wK2p3dCIs…Presenting a DPoP-bound token as Bearer, or with a proof signed by a different key, is rejected.
That is the point of the binding: possession of the token alone is not enough.
Tokens issued with a resource indicator carry that value as aud, and a resource server must
reject a token whose audience is not its own — otherwise a token minted for one service is
replayable at another. See scopes, claims, and discovery.
SCIM tokens
Section titled “SCIM tokens”Directory sync uses its own bearer token, minted per connection so that revoking a customer’s SCIM integration does not disturb anything else they have:
POST /v1/organizations/org_01J…/directory/scim_tokenAuthorization: Bearer sk_live_…
{ "connection_id": "conn_…" }{ "token": "scim_…", "scim_base_url": "https://acme.oauth.work/scim/v2"}Hand both values to the customer’s identity provider. Like API keys, the token is shown once. It
authorizes the SCIM endpoints only — it cannot call /v1. See SCIM 2.0.
Session cookies
Section titled “Session cookies”The hosted sign-in screens, the end-user account screens, and the connected-accounts flow run on a
session cookie the platform sets after a successful login. The cookie is host-scoped to the tenant,
so a flow that starts on acme.oauth.work finishes there.
These endpoints are same-origin by design and send no CORS headers, so a separate frontend cannot
call them directly. To sign a user in from your own application, send them to
/authorize — the hosted screens run the login, and you receive a code. The endpoints are
documented in login methods so you can reason about what the hosted flow does and
what it records, not as an API to call cross-origin.
The public discovery documents (/.well-known/openid-configuration,
/.well-known/oauth-authorization-server, /.well-known/oauth-protected-resource, and
/.well-known/jwks.json) do send CORS headers, because browser-based relying parties and MCP
inspectors have to fetch them cross-origin.
Client authentication
Section titled “Client authentication”A confidential OAuth client authenticating at the token endpoint is a different thing again from any
of the above — it proves the client’s identity, not a user’s or an operator’s. The platform accepts
client_secret_basic, client_secret_post, private_key_jwt, and none (public clients, PKCE
enforced). See client authentication.