This page sets out the rules the runtime applies to credentials: how it reads one, what a token must hold, what a signed-in person may do and what a refused client is told. It also sets out how the runtime checks calls from a browser and requests in local mode. Authentication and security covers the tasks: turning sign-in on, making API keys, local mode and calls from a browser.
A signed-in person acts in an org, the runtime's name for an organization in the Auto identity service, or, for a person who signs in with none, for an org of their own, whose id is their subject.
- A caller sends its credential as
Authorization: Bearer <credential>. - A header that is not exactly one
Bearer <key or token>is refused400, in local mode too. - A credential that has the form of an API key, which starts with
abk_, is checked as a key. Any other is checked as a token. Keys and sign-in work side by side. - A token is a JWT that the Auto identity service signed, in one of two forms.
With sign-in on, the server reads the identity service before it accepts a request:
- It reads the service's metadata from
/.well-known/oauth-authorization-server. When that cannot be used, it reads/.well-known/openid-configurationinstead. This is the order of the MCP authorization specification. - The metadata must name the issuer exactly as
IDENTITY_ISSUERwrites it, and anhttpsjwks_uri. - It reads the key set at that
jwks_uri, which must hold a key.
Each read waits at most 10 seconds for a response and reads at most 64 KiB. It follows no redirect: an address that responds with one is refused, like an address that does not respond.
The key set may instead be given, for a service that publishes its keys another way, or a server that should read nothing of the service at start:
- With
IDENTITY_JWKS_URL, the server reads no metadata, and reads the key set at thathttpsURL in place of thejwks_uri, as step 3 does, with the same bounds. - With
IDENTITY_JWKS, the JSON of the key set itself, which must hold a key, the server reads nothing of the service, at start or later.
Setting both stops the start with an InvalidSettingsError. The tokens are checked the same way whichever gives the key set.
A service whose metadata or key set cannot be used stops the start with an UnusableIdentityServiceError. The error names the service and each address the server read.
| Form | Holds aud |
Who holds it | Where it is accepted | Permissions |
|---|---|---|---|---|
| Access token | Yes | A third party acting for a person: an agent that obtains it through the authorization flow, or an agent gateway that obtains it on the person's behalf. The person grants its scopes on the identity service's consent screen | Wherever its aud names the runtime; see The audience of a token |
The permissions among the scopes it was granted |
| Session | No | A page the person signs in to: Studio, Auto's own app (signed in to once, with no consent screen), or a page at an origin in ALLOWED_ORIGINS |
Only when its azp names the Studio host's origin, PUBLIC_URL where the runtime serves Studio, or one of ALLOWED_ORIGINS, and only on a request whose Origin header is exactly that azp |
Every permission in the person's org |
A token that holds aud is an access token, even when it also holds an azp.
The runtime accepts a token only when it meets every rule:
| Claim or property | Rule |
|---|---|
| Size | At most 8 KiB |
alg (header) |
RS256, PS256, ES256, or EdDSA on Ed25519. none and every other algorithm are refused |
typ (header) |
at+jwt, application/at+jwt, JWT or application/jwt, in any letter case, or none |
| Signature | By a key of the service's key set that verifies its algorithm, chosen by the token's kid. A token without a kid uses the only key of its type in the set; one that several keys fit is refused |
| Signing key | Never an RSA key under 2048 bits, a key meant for encryption, or a key named for another algorithm |
iss |
The issuer, IDENTITY_ISSUER |
aud |
For an access token, an audience the runtime accepts: see The audience of a token |
azp |
For a session, an origin the runtime allows, equal to the request's Origin header |
exp |
Present and not past, with 60 seconds of leeway |
nbf and iat |
Not ahead, with 60 seconds of leeway |
sub |
A non-empty string of at most 256 bytes, without control characters, unpaired surrogates or noncharacters. These are the bounds of a name in the facts, since sub becomes part of the name recorded for the person. For a token that names no org, an org id other than local too, since it is the org the person acts in |
org_id |
For an access token that names an org: present, and an org id of 1 to 64 letters, digits, _ and -, other than local. It is the id of the organization the person signed in to. The identity service puts it in the token when the person grants user:org:read |
o |
For a session that names an org: an object whose id is an org id under the same rule. Version 2 of the service's session token names the organization in o, beside its slug, the person's role, their permissions and a feature-permission map. The runtime reads only the id |
scope |
For an access token that holds it, a string of scopes separated by spaces, as the identity service issues it. The scope of a session is not read |
The org comes from the org claim of the token's form: org_id for an access token, the id of o for a session. A token that holds neither org_id nor o names no org, as the token of a person signed in with no organization of their own does, such as a session issued while the person has no active organization. It acts in the org whose id is its sub, and in no other, and the runtime's rules then hold there as in any org. A token that holds the org claim of the other form without its own is refused. No org is taken from anything else the token holds. That includes the org_id of a session and the o of an access token.
A key set read at start, through the metadata or at IDENTITY_JWKS_URL, is kept this way:
- The server reads the key set again for a
kidit does not hold, once 60 seconds have passed since the last read that succeeded. - It reads the set again at the first token after the set is ten minutes old. So a key the service removes is trusted for ten minutes at most.
- When it cannot read the set again for an unknown
kid, the keys it held stay in use. - When it cannot read the set once ten minutes have passed, every token is refused. An identity service that is unreachable for longer than that keeps sign-ins out until it responds again.
A key set that IDENTITY_JWKS gives is read from nowhere: its keys are the ones trusted until the server restarts with others, and a token signed with any other key is refused.
A token that fails any rule is refused 401 with invalid_token, and so is an opaque token. The problem's detail names the rule or the claim the token failed, and never a value it holds:
| Token | Detail |
|---|---|
| An expired token | The token was refused: ERR_JWT_EXPIRED on its exp claim |
An access token without org_id that holds an o |
The token was refused: ERR_JWT_CLAIM_VALIDATION_FAILED on its org_id claim |
An access token whose org_id is not an org id other than local |
The token was refused: its org_id claim is not an org id other than local |
A session without o that holds an org_id |
The token was refused: ERR_JWT_CLAIM_VALIDATION_FAILED on its o claim |
A session whose o is no object, or holds no id that is an org id other than local |
The token was refused: its o.id claim is not an org id other than local |
A token that names no org, whose sub is not an org id other than local |
The token was refused: its sub claim is not an org id other than local |
| An access token for another resource | Ends on its aud claim |
| A session given to a page at an origin the runtime does not allow, or naming no origin | Ends on its azp claim |
A session sent without an Origin header, or from another origin than its azp |
The token was refused: the session was not presented by the page it was issued to |
The runtime accepts an access token only when its aud names the runtime as one of these:
PUBLIC_URL, alone or with a final/;- on an MCP endpoint,
PUBLIC_URLfollowed by the path of the endpoint the request reached, such ashttps://runtime.example.com/orgs/acme/mcp. A standards client asks for a token for this resource when the metadata of that endpoint names it.
So the runtime refuses these, though the same service signed them:
- a token the identity service issued for another service;
- an identity token sent by mistake;
- a token that another resource server received and passes on.
A session names no audience. The runtime checks its azp instead, which is the origin of the page that the identity service issued it to:
- A session issued to a page that the runtime does not allow is refused.
- A session issued to a page that it allows is accepted only on a request whose
Originis that page's origin. This holds on the MCP endpoints as on every other path. - A page's calls to a runtime are always cross-origin, so its browser always sends
Origin. - A session serves a page and never an agent.
The audience is the check that a public server needs. Requests reach it through proxies and under any Host, and the audience ties the token to the runtime rather than to the host name a request used. So a public server, whose callers present credentials, does not check Host. Local mode is different, since it trusts every request without a credential. There, the Host check stops a page in a browser from reaching the server through a name that resolves to the loopback address.
The runtime checks the Origin header a browser sends with every request:
- A page may call the API from the Studio host,
STUDIO_ORIGIN, which ishttps://studio.on.autounless it is set, from the runtime's own origin where the runtime serves Studio (Studio served by the runtime), or from an origin listed inALLOWED_ORIGINS. The runtime's own origin isPUBLIC_URLwhere it is set, and otherwise the origin the request came in on, which in local mode is a localhost name by theHostcheck. - A request whose
Originnames any other origin is refused403. - A request with no
Originheader, such as one from a script or an agent, passes this check. - The paths that need no caller skip the
OriginandHostchecks:/health, the OpenAPI document, the guides, the API catalog, the page at/and, where sign-in is on, the protected resource metadata. Under/studio/, where the runtime serves Studio, no caller is needed either, and theOriginandHostchecks still apply. - Being allowed lets a page reach the API. Where credentials are enforced, the page still sends a key or a session in the
Authorizationheader. There is no wildcard origin and no credentials mode.
A page that a person signed in to sends the session that the identity service issued to it, as Authorization: Bearer <session>. It then acts as that person, with every permission in their org.
- A session is accepted only from its own page: the request's
Originheader must be the origin the session names asazp. - A session sent without
Origin(by a script outside a browser, or by an agent) or from another page is refused401. When a token is refused gives the detail it carries. - In local mode no key is needed, so a script served from the Studio host, or by the runtime itself under
/studio/, can act as the local developer. The session it sends is ignored.
For an allowed origin, the server responds to CORS like this:
| Request | Response |
|---|---|
A preflight: OPTIONS with Access-Control-Request-Method |
204, before any key is checked. It allows GET, HEAD, POST, PUT and DELETE, and the headers authorization, content-type, traceparent, tracestate and baggage. The browser may cache it for 600 seconds. A preflight with Access-Control-Request-Private-Network: true, which a browser sends before a page on a public origin reaches a server on a private or loopback address, also gets Access-Control-Allow-Private-Network: true, so the hosted Studio reaches a runtime on your computer; one without it gets no such header |
| Any other request | Access-Control-Allow-Origin with that origin, and Vary: Origin. The page may read x-request-id, x-trace-id, www-authenticate and Date, so a page that is refused can read the challenge and sign in or ask for the scope it lacks, and a page can count the time left on a link by the server's clock rather than the browser's |
A server is in local mode when it listens only on a loopback address and neither PUBLIC_URL nor API_KEYS is set, or when its operator declares it with ACCESS_MODE=local, for a server whose port only this computer reaches, such as a container whose port is published on 127.0.0.1 alone. The declaration takes no API_KEYS, and a PUBLIC_URL only on localhost, 127.0.0.1 or [::1], which names the server's address, signs no one in, and is needed on a server that listens beyond the loopback. The runtime then signs no one in and serves no protected resource metadata. It trusts every request as the local developer, with every permission in whichever org the request names. It still checks how a request arrives:
- The credential a request presents is ignored, since a page signed in to Auto sends its session to every server it calls. A request with a session, an access token or a key acts as the local developer.
- An
Authorizationheader that is not oneBearer <key or token>is still refused400. - A request whose
Hostheader is notlocalhost,127.0.0.1or[::1], with or without a port, is refused403. This stops a web page from driving the server through a name that resolves to the loopback address. - As always, a request whose
Originis neither the Studio host's, nor the runtime's own while it serves Studio, as it does by default in local mode, nor inALLOWED_ORIGINSis refused403. - The paths that need no caller skip the
HostandOriginchecks. - On
/mcp, which names no org, the local developer acts in the orglocal.
A sign-in asks the identity service for five scopes:
user:org:read, which binds the token to the organization the person signs in to;org:read,org:write,playbook:readandplaybook:write, the four permissions.
The identity service defines each permission as a scope of its own, with a sentence that it shows on its consent screen, so a person can read what they grant.
What a token then allows:
- A session admits the person to the org the
idof itsonames, over every playbook, with all four permissions, as they act in Studio. The role and permissions itsonames do not change that. - A token that names no org admits the person to the org whose id is its
sub, over every playbook, with the permissions of its form: every permission for a session, and those among its scopes for an access token. - An access token admits the person to the org its
org_idnames, over every playbook, with the permissions among the scopes in itsscopeclaim. Onlyorg:read,org:write,playbook:readandplaybook:writeare permissions. Any other scope grants none,user:org:readincluded. - An access token that grants none of the four, or holds no
scope, still admits the person to their org, with no permission. Every operation then refuses them403withinsufficient_scope. The challenge'sscopenames the permissions the operation needs, so a client asks the identity service for a token with more. - For an access token,
org_idbinds the org, notuser:org:read. An access token that holdsorg_idand the four permissions, but notuser:org:read, admits the person to that org with all four permissions. - Every other org refuses the person
403withinsufficient_scope, over HTTP and on the MCP endpoints./mcpserves them the org their token names.
API keys cover what sign-in does not: a machine, a pipeline or a trainer's agent that does not sign in, and a grant over only some of an org's playbooks, such as reading one playbook.
A signed-in person acts under the name person:<subject>, where the subject is the token's sub, unchanged. This name lets a reader of the facts tell a person from a key.
- It is the
byof every fact the person records, including the activation of a prediction function version they ask for. - It is the
created_by,started_by,answered_byandapproved_byof what they create, start, answer and approve. - A run event the person sends without a
sourcegets the source/callers/followed by that name, percent-encoded as one path segment.
The runtime verifies a token on every request and keeps none.
- When the identity service stops issuing tokens to a person, the runtime refuses them once their last token expires.
- A token the service revokes before it expires is still accepted until it expires, since the runtime does not ask the service.
- A workflow run keeps acting as the identity that started it, with that identity's permissions and playbooks, for as long as it runs. A run is one unit of work that identity asked for. Revoking the person stops their next request, not the runs they already started. A caller who may write to the playbook can cancel those runs.
With sign-in on, a client finds out from the server itself how to sign in. This is the authorization flow of the MCP specification:
- The client connects to an MCP endpoint without a token, and is challenged.
- It reads the protected resource metadata the challenge points to.
- It asks the authorization server the metadata names for a token for that resource.
- It connects again with that token.
An agent gateway that obtains tokens on a person's behalf does the same as any other client.
Signing in through a browser, registering clients and refreshing tokens are the authorization server's work. The runtime never issues a token. It never passes a token it receives on to a tool server; it calls each tool server under that server's own credential.
With sign-in on, GET /.well-known/oauth-protected-resource returns the protected resource metadata of RFC 9728 without a key. So does the same path followed by each MCP endpoint's path: /mcp, /orgs/{org}/mcp and /orgs/{org}/playbooks/{playbook}/mcp.
| Field | Value |
|---|---|
resource |
PUBLIC_URL followed by the path the metadata describes |
authorization_servers |
The one issuer, IDENTITY_ISSUER |
bearer_methods_supported |
["header"] |
scopes_supported |
["user:org:read", "org:read", "org:write", "playbook:read", "playbook:write"], the scopes a sign-in asks for |
resource_documentation |
The MCP reference |
Without sign-in, the server is not an OAuth resource server, and serves no metadata. Where keys are enforced, the metadata's path needs a key, as every path does.
Where the runtime signs people in at an https PUBLIC_URL, GET /oauth/client.json returns without a key the client ID metadata document by which Auto names its own client at a provider's authorization server, when a person connects the provider by consent and that server takes such a client. The authorization server reads it when a consent starts, so the answer may be kept for five minutes.
| Field | Value |
|---|---|
client_id |
The document's own address, PUBLIC_URL followed by /oauth/client.json |
client_name |
Auto Runtime |
redirect_uris |
The one return address a consent sends the person back to: the Studio host's /connect-return, STUDIO_ORIGIN followed by it, https://studio.on.auto/connect-return unless it is set, or PUBLIC_URL followed by /studio/connect-return where the runtime serves Studio |
grant_types |
["authorization_code", "refresh_token"] |
response_types |
["code"] |
application_type |
web |
token_endpoint_auth_method |
none |
Where the runtime signs nobody in, as in local mode even at an https loopback address, or its PUBLIC_URL is http, it serves no document. Where keys are enforced, its path then needs a key, as every path does.
Where credentials are enforced, the runtime responds to a refused request as RFC 6750 sets out. Where sign-in is on, the challenge says more:
| Status | When | WWW-Authenticate without sign-in |
WWW-Authenticate with sign-in |
|---|---|---|---|
400 |
The Authorization header is not exactly one Bearer <key or token>, in local mode too |
Bearer error="invalid_request" |
Bearer error="invalid_request" |
401 |
No Authorization header |
Bearer |
Bearer resource_metadata="<PUBLIC_URL>/.well-known/oauth-protected-resource", scope="user:org:read org:read org:write playbook:read playbook:write" |
401 |
A key or token was presented and refused | Bearer error="invalid_token" |
The same as above, with error="invalid_token" first |
403 |
The caller lacks the permission, playbook or org the call needs | Bearer error="insufficient_scope" |
error="insufficient_scope", the root metadata as resource_metadata, and, as scope, the permissions the operation needs |
The scope of a 403 from an operation is the same whether the caller lacks the permission or belongs to another org:
scope="playbook:write"for an operation that writes in a playbook;scope="org:read playbook:read"for listing the playbooks, which either permits.
The MCP endpoints' own refusals, of an org or a playbook the caller may not reach, name the four permissions, with the metadata of that endpoint. The problem's detail names a missing permission as well.
A key's permissions are its own. A key that lacks what the scope names is not re-authorized: its holder needs a key that has it.