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
- The external launch provider opens the runtime UI with
/ui/?launch_code=<opaque-code>. - The UI calls
POST /auth/launchonce with{ "launchCode": "..." }. - The daemon posts the code to the configured exchange endpoint with its runtime audience, instance ID, and optional service credential.
- The exchange endpoint returns a signed assertion for the authenticated subject.
- The daemon verifies issuer, audience, expiration, subject, and the configured
instance ID; then it maps or creates a local user by
(provider, issuer, subject). - The daemon returns normal runtime auth tokens. The UI stores those tokens and
removes
launch_codefrom the URL withreplaceState.
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_hostFor 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_SECRETFailure 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 issuersub: stable subject at that issueraud: expected runtime audienceexp: short expiration time
Optional claims:
email,name,avatarorpicturerole:viewerormemberby default;admin/superadminonly whenallow_admin_rolesis explicitly enabledprovider: stable provider label used in local identity mappingjtiornonce: accepted for audit/correlation; one-time replay prevention remains the exchange endpoint’s responsibility
Required when external_launch.instance_id is configured:
instance_idorruntime_instance_id: must match configuredinstance_id
Compatibility and upgrade notes
- Non-RS256 asymmetric signing must be declared before upgrading. Asymmetric
verification (
jwks_url/public_key) defaults to anRS256-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, setalgorithmsexplicitly to the intended asymmetric algorithm before upgrading, or assertions will stop verifying. login_redirect_urldeployments begin receiving a return-host parameter. Whenlogin_redirect_urlis enabled, direct-host entry appends the configuredreturn_host_param(defaultreturn_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. Leavelogin_redirect_urlunset 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_urlmust be an HTTP(S) URL; malformed URLs and schemes such asjavascript:are rejected during config loading.- Assertions should be audience-bound to the runtime, instance-bound when
instance_idis configured, and expire quickly. - Configure exactly one assertion verification method (
jwks_url,public_key, or dev-onlydev_shared_secret). - Local users are mapped by stable external identity
(provider, issuer, subject). A matching email alone never merges identities.