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: 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

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.

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 / public_key verification defaults to an RS256 allow-list to prevent algorithm confusion; and 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 asymmetric signing must be declared before upgrading. Asymmetric verification (jwks_url / public_key) defaults to an RS256-only allow-list and refuses HS* algorithms. If your issuer signs assertions with another asymmetric algorithm (RS384, ES256, PS256, …) and previously relied on library defaults, set algorithms explicitly to the intended asymmetric algorithm before upgrading, or assertions will stop verifying.
  • 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