Authentication
Amodal auth has two independent layers. Keep them straight and everything else follows.
| Layer | Question it answers | Field | Amodal label |
|---|---|---|---|
| Access mode (deploy protection) | Who can open the hosted app URL? | hosted_auth_mode | Configuration → Auth → Deploy Protection |
| End-user identity | Who does the runtime think the chat caller is? | end_user_auth.type | Configuration → Auth → End-User Identity |
Access mode — hosted_auth_mode:
public— anyone with the URL can open the app shell. (Amodal: Public.)user_auth— gated; only signed-in members of the agent's organization can open it. The edge worker redirects unauthenticated browser requests to the hosted login. (Amodal: Team only.) This is the default for a new agent.
End-user identity — end_user_auth.type: none (anonymous), jwks, jwt_secret, oidc, header, or custom. This selects which identity the runtime assigns to a caller. It does not decide whether auth is required.
The runtime always requires an authenticated caller on chat/runtime requests. Identity mode picks who the caller is; it never makes auth optional.
type: "none"assigns a synthesizedanonymousidentity — it does not mean "no token."
Browser ──▶ Edge worker ───────────────▶ Runtime
• gated? check amodal_session • verifies the injected/bearer
cookie, else redirect to identity per end_user_auth.type
/auth/login • runs the agent
• injects Authorization
server-side for valid
cookies / lets valid
agent-bound bearer tokens
throughBrowser requests: the hosted cookie flow
For an app served by Amodal at the agent's own origin — the default chat shell or a custom SPA — hosted auth is cookie-backed and handled at the edge:
- The browser hits a gated route with no session. The edge worker redirects (a full-page navigation, not an in-page fetch) to the agent's
/auth/login. - The user logs in through the hosted flow. The platform sets an HttpOnly
amodal_sessioncookie on the agent origin. - On every subsequent same-origin call, the edge worker validates the cookie and injects the
Authorizationbearer server-side before proxying to the runtime.
The browser app never sees, reads, or stores the token. Concretely:
- Use an absolute same-origin
runtimeUrl, normallywindow.location.originin the browser. Do not pass"",undefined, or a cleared env var into the SDK; that can produce requests such as/undefined/chat/stream. - Omit
getTokenfor hosted cookie auth. Do not call/auth/sessionto fetch a token into JS. - The hosted login is a top-level redirect. A cross-origin OAuth flow (e.g. the hosted IdP) cannot run as an in-page
fetchor sub-resource — the browser will block it.
Pass getToken only when an external system owns identity (see the table below).
API clients
Non-browser callers (CLI, CI, your server, another product) send Authorization: Bearer themselves:
| Credential | Use for | Created from |
|---|---|---|
| Short-lived runtime token | Smoke tests, CI, server-to-server calls to the agent runtime. Agent-bound and expiring. | POST /api/agents/{agent_id}/tokens with an operator/admin pk_ token. |
ak_ agent runtime API key | A persistent credential for an external product/service calling the runtime. | Agent API key settings. |
A valid runtime token reaches the runtime even when the agent is user_auth (gated) — the edge lets a platform-signed, agent-bound bearer token through while still redirecting browser users to the hosted login. So you do not need to make an agent public to smoke-test it.
pk_platform automation tokens are for the Platform API control plane (creating, configuring, and deploying agents). They are not used to call the agent runtime — mint a runtime token for that. See Build With A Coding Agent for the end-to-end automation flow.
Choose a setup
| You're building | Access mode | Identity | How the client authenticates |
|---|---|---|---|
| Team-internal app/SPA served by Amodal | user_auth ("Team only") | none, or your IdP | Hosted cookie. Same-origin calls, omit getToken. One login covers the app shell and chat. |
| External product embedding the agent | public or user_auth | jwt_secret / oidc / jwks | Your backend mints the JWT (with scope_id/scopeContext claims); pass it via getToken. See Embedding. |
| Public demo (no per-user identity) | public | none | No token from the browser; mint tokens only server-side. |
| CLI / CI / server-to-server | any | any | Minted runtime token or ak_ key as Authorization: Bearer. |
End-user identity modes
| Mode | Use when |
|---|---|
none (anonymous) | Demo or any app where per-user identity is not required. Synthesizes anonymous. |
jwks (hosted preset) | The platform host owns login and token issuance (the hosted WorkOS path). |
jwt_secret | Your backend mints HMAC JWTs for users. |
oidc | Full OAuth/OIDC login against Okta, Azure AD, Auth0, Keycloak, etc. |
header | An upstream proxy authenticates users and injects an identity header. |
custom | A custom runtime AuthStrategy module. |
Configure these in Amodal (Configuration → Auth) or programmatically with PUT /api/agents/{agent_id} (hosted_auth_mode, end_user_auth). Apply auth changes before deploying — or redeploy afterward — so the runtime machine picks them up.
Common mistakes
- Don't fetch or store a session token in JS for hosted auth. It is cookie-backed and injected at the edge; reading it from JS is neither possible nor needed. Omit
getToken. - Don't make an agent
publicjust to call it from a script. A minted runtime token reaches a gated agent. - "Anonymous" still needs a token on the API. It removes per-user identity, not the runtime's bearer requirement.
- The hosted login is a full-page redirect, not an in-page request.
- Don't pass an empty runtime URL to the React SDK. Use
window.location.originfor an Amodal-hosted SPA, and pass that same value toAmodalProvider runtimeUrlandAmodalChat serverUrl.