Skip to Content
DocsOne-Time Launch Auth

One-time launch-code authentication

Agor can accept a generic external launch handoff when another trusted app has already authenticated the user. The browser carries only an opaque, short-lived launch_code; the daemon exchanges that code over a server-to-server backchannel, verifies the returned assertion, maps a local user, and issues the same runtime access and refresh tokens used by normal login.

Flow

  1. The external launch provider opens the runtime UI with /ui/?launch_code=<opaque-code>.
  2. The UI calls POST /auth/launch once with { "launchCode": "..." }.
  3. The daemon posts the code to the configured exchange endpoint with its runtime audience, instance ID, and optional service credential.
  4. The exchange endpoint returns a signed assertion for the authenticated subject.
  5. The daemon verifies issuer, audience, expiration, subject, and the configured instance ID; then it maps or creates a local user by (provider, issuer, subject).
  6. The daemon returns normal runtime auth tokens. The UI stores those tokens and removes launch_code from the URL with replaceState.

Configuration

external_launch: enabled: true exchange_url: https://launch.example.com/runtime/exchange issuer: https://launch.example.com audience: agor-runtime:my-instance instance_id: my-instance # Production: configure exactly one assertion verification method. jwks_url: https://launch.example.com/.well-known/jwks.json # Optional daemon-to-provider bearer credential. Prefer env vars for secrets. service_credential_env: AGOR_EXTERNAL_LAUNCH_SERVICE_TOKEN # Optional: allow role claims above member. Defaults to false. allow_admin_roles: false # Optional backchannel timeout, 1-120000 ms. Defaults to 10000. request_timeout_ms: 10000 # Optional non-empty allow-list. HS* is symmetric-only; RS*/PS*/ES* is # asymmetric-only. Defaults to HS256 for a dev secret and RS256 otherwise. algorithms: [RS256] # Optional: primary action shown when launch sign-in cannot continue. # Must be http:// or https://. login_redirect_url: https://workspace.example.com/open # Optional host-bound launch for a single daemon that serves several hosts. # When enabled, the daemon forwards the normalized inbound browser Host to the # exchange endpoint as an opaque `request_host`, letting the issuer bind a code # to the exact route the browser entered. forward_request_host: false # Header the daemon reads the browser Host from. The trusted proxy / edge owns # host normalization and must overwrite it. Default: host. trusted_host_header: host # Query param appended to login_redirect_url carrying the current host as an # opaque return context for direct-host entry. Default: return_host. return_host_param: return_host

External user and role authority

External launch authentication does not disable Agor user administration by itself. A deployment whose launch provider is also authoritative for user lifecycle, profile fields, and roles must opt into the identity contract:

identity: user_lifecycle: external role_authority: claims local_auth: disabled external: provider: external_launch provisioning: jit

With this profile, verified launches may still create a missing local user and synchronize JWT-owned email, name, avatar, execution-home key, and role. Those fields update when the next external launch succeeds; the daemon does not call the provider on every request. User and administrator calls through REST, Socket.IO, CLI, and MCP cannot create or delete users or edit those fields. Agor-owned preferences, onboarding state, AI-provider credentials and defaults, environment variables, personal API keys, and MCP selections/OAuth grants stay editable.

The trusted (tenant, provider, issuer, subject) binding is stored in a tenant-owned relation with database uniqueness enforcement. PostgreSQL JIT projection is one tenant-scoped transaction and serializes the same subject across replicas. Execution-home keys are also unique per tenant, preventing two users from sharing one delegated credential context.

The role claim is required and exact in this mode. Administrative claims still require external_launch.allow_admin_roles; a superadmin claim also requires execution.allow_superadmin. A disabled role fails authentication rather than being silently downgraded.

Omitting identity preserves normal local user, role, and password authority. The current contract does not synchronize external deactivation or revoke already-issued Agor credentials: the external provider can prevent a new launch, while cleanup and revocation remain a separate lifecycle concern.

Before enabling this profile, apply the external-identity migration and upgrade every daemon replica. The migration fails closed if legacy external bindings or non-null execution-home keys conflict. New daemons can bind a legacy JSON link on the next successful launch, but old daemons do not maintain the new binding relation.

For local development only, a symmetric assertion secret can be used:

external_launch: enabled: true exchange_url: http://localhost:4000/exchange issuer: http://localhost:4000 audience: agor-runtime:dev dev_shared_secret_env: AGOR_EXTERNAL_LAUNCH_SHARED_SECRET

Failure behavior and login redirects

When a launch code is missing, expired, already used, invalid, or cannot be exchanged because of a non-transient authentication failure, the UI removes the one-time code from the URL and shows a clear failure message.

If external_launch.login_redirect_url is configured, the unauthenticated screen makes Return to workspace the primary action so the user can open a fresh launch link from the external workspace. The button appends a return_to query parameter containing the current Agor path, allowing the launcher to preserve deep links such as /ui/s/<session>/ when it issues a fresh launch_code. Local username/password login is still available as a secondary fallback unless external identity authority disables it.

If login_redirect_url is omitted, the normal local login screen is unchanged.

Direct-host entry

A browser can also navigate straight to a workspace host. If a valid session already exists for that host it opens immediately with no new code. Runtime sessions are stored in origin-scoped localStorage — never a Domain-wide cookie — so a session on one host is never sent to another. If there is no host-local session, the unauthenticated screen sends the browser to login_redirect_url with return_to (the current relative Agor route) and, when return_host_param is configured, the current host. The issuer allow-lists that host, mints a fresh code and returns to the same host for the normal exchange. Agor only ever redirects to the operator-configured URL, so it adds no open redirect.

Exchange contract

The daemon sends a JSON POST to exchange_url:

{ "launch_code": "opaque-one-time-code", "audience": "agor-runtime:my-instance", "instance_id": "my-instance", "request_host": "primary.workspace.example.com" }

request_host is included only when forward_request_host is enabled; it is the normalized inbound Host read from the trusted trusted_host_header (default Host), never a client-supplied body field or arbitrary forwarded header. audience and instance_id are compatibility echoes — a correct issuer derives authority from its own records and the authenticated exchange credential.

If service_credential or service_credential_env is configured, the daemon also sends Authorization: Bearer <credential>. This server-to-server exchange credential is never returned to the browser, exposed in the public /health settings, or written to logs.

Production verification fails closed: the none algorithm is always rejected; jwks_url verification defaults to an RS256 allow-list, while a static public_key derives a compatible default from its key type and EC curve. An explicit allow-list must match that key. A missing issuer, audience, exp, sub, or required tenant claim creates no session.

The exchange endpoint should consume the launch code exactly once and return:

{ "assertion": "<signed JWT>" }

Required assertion claims:

  • iss: expected issuer
  • sub: stable subject at that issuer
  • aud: expected runtime audience
  • exp: short expiration time

Optional claims:

  • email, name, avatar or picture
  • role: viewer or member by default; admin/superadmin only when allow_admin_roles is explicitly enabled
  • provider: stable provider label used in local identity mapping
  • jti or nonce: accepted for audit/correlation; one-time replay prevention remains the exchange endpoint’s responsibility

Required when external_launch.instance_id is configured:

  • instance_id or runtime_instance_id: must match configured instance_id

Compatibility and upgrade notes

  • Non-RS256 JWKS signing must be declared before upgrading. jwks_url verification defaults to an RS256-only allow-list and refuses HS* algorithms. Static public_key verification instead derives RS256 for RSA or the matching ES algorithm for a supported EC curve; an explicit algorithms list must be compatible with the key. If a JWKS issuer signs with another asymmetric algorithm (RS384, ES256, PS256, …), set algorithms explicitly before upgrading.
  • login_redirect_url deployments begin receiving a return-host parameter. When login_redirect_url is enabled, direct-host entry appends the configured return_host_param (default return_host) to that URL. Existing deployments are therefore not unchanged: the issuer’s launch-init endpoint will start receiving this query parameter and should tolerate and/or consume it. Leave login_redirect_url unset if the issuer should not receive it.

Security notes

  • Put only an opaque, short-lived, one-time code in the browser URL.
  • Do not put runtime bearer tokens or external provider tokens in URLs.
  • The daemon-to-provider exchange should require HTTPS and an authenticated backchannel in production.
  • login_redirect_url must be an HTTP(S) URL; malformed URLs and schemes such as javascript: are rejected during config loading.
  • Assertions should be audience-bound to the runtime, instance-bound when instance_id is configured, and expire quickly.
  • Configure exactly one assertion verification method (jwks_url, public_key, or dev-only dev_shared_secret).
  • Local users are mapped by stable external identity (provider, issuer, subject). A matching email alone never merges identities.
Last updated on