OAO supports three authentication modes behind the same application boundary:
- Development auth is the credential-free local default. The console signs
in to the seeded development tenant through an HTTP-only session cookie.
- Google IAP verifies the signed assertion injected at the application edge,
then resolves the principal or onboards a human through the application policy.
- WorkOS AuthKit is the hosted human-auth adapter. OAO still requires a
matching PostgreSQL principal and project membership; a valid WorkOS identity
alone does not grant tenant access.
Server-to-server integrations use organization-scoped OAO API keys. One key
works against every project of its organization, including projects created
after the key.
Google IAP
IAP and OAO enforce separate gates: IAP decides who may reach the service, then
OAO verifies the signed sub and applies its initial-owner/member policy.
Set AUTH_PROVIDER=iap with the exact Cloud Run resource audience. The default
organization/project are initialized by the app with names defined in code and
IDs stored in PostgreSQL; no tenant or onboarding-domain environment variables
are required.
On a fresh installation, the first verified human login becomes owner;
subsequent IAP-authorized humans join as members with explicit developer scopes.
IAP’s human allowlist is the set of possible initial owners—restrict it to
trusted people. Service accounts and API keys cannot claim ownership.
The existing login path handles this without a setup screen or separate API.
Membership and audit writes are atomic; concurrent logins produce one owner.
Existing users retain their roles. Restarts and removed owners never reopen the
ownership claim. Removed memberships stay revoked while identity records remain.
See GCP deployment for scope restrictions and rollout.
Organization roles for IAP users
Open Members to manage a signed-in IAP user’s Organization role. The
separate Project role column shows their membership in the selected project.
IAP identity matching uses the immutable Google subject, including older internal
principal aliases. Switching projects requires an identity-linked membership;
unlinked same-name principals cannot retain access after demotion.
Roles shown in the console come from stored memberships, never from a wildcard
scope or a shared display-name fallback.
- Owner has full platform access (
*). An existing organization Owner can
appoint additional Owners and can change another Owner’s access.
- Admin has the explicit current administration permissions, including
provider/MCP configuration, event webhooks, audit access, API-key creation and organization
project management. Owners and Admins can grant other users Admin access.
Admins cannot grant Owner access or change/remove an Owner, and receive no wildcard.
- Member retains the automatic developer permissions for agents, skills,
sessions and runs. Viewer has only the explicit read permissions.
Selecting a role updates organization membership, effective scopes, and all
existing IAP project memberships for that person atomically. Use Add member
inside a project to grant an existing verified IAP organization user access to
that project; their project role and scopes inherit their current organization
profile. Refresh the user’s console to display the
new role; subsequent authenticated requests resolve current permissions.
Lowering access revokes API keys created by that person and their derived keys, including keys created
through their other project identities. Rotate any affected integrations before
relying on them again. Remove revokes their organization/project memberships
and their keys while retaining identity records to prevent automatic rejoining.
Also remove their Google IAP access as part of offboarding.
You cannot change or remove your own access; the last Owner must remain.
Only IAP-authenticated humans with the required organization role can manage
these grants. API keys cannot grant human access. Users first appear after
signing in through IAP; arbitrary subject/scope entry is disabled in this mode.
Remove from project revokes only the selected project membership and keeps
the organization role, other project memberships, and organization-scoped keys.
The IAP default project’s membership cannot be removed because it anchors
console authentication, and the project itself cannot be deleted. Use
Remove organization access to offboard that user instead.
Remove organization access retains the existing organization-wide offboarding
behavior described above.
Existing stored roles are preserved during rollout. If an existing person really
has Owner membership, they will still display as Owner until another Owner
explicitly changes it. This feature does not rewrite existing users or repair
operator-created/shared identity mappings automatically. First-installation
ownership still comes from the first verified human IAP login; project creation
is unchanged and does not promote a member’s organization role. On older
installations where one person has differently named internal records across
projects, create/delete projects from that person’s original project; those
legacy records are not migrated by this change. Their IAP role changes and
revocations still apply across identity-linked project records.
The console discovers the configured application provider from
GET /v1/auth/provider. This keeps a promoted container image independent of
the hosted authentication provider.
For service-to-service calls through IAP, place the short-lived IAP credential
in Proxy-Authorization and the OAO API key in Authorization. This preserves
edge authentication and OAO authorization as independent checks. Never use a
service-account key.
Create an integration key
A human principal needs both project:admin (or *) and an organization
owner or admin membership to create a key from API keys in the console.
Project membership and wildcard scopes alone do not grant that organization role.
The console label All scopes describes permissions, not organization ownership.
Enter a descriptive name, select the least-privilege scopes, and save the secret
from the acknowledgement dialog. The console removes the plaintext
when that dialog closes and cannot reveal it later.
API keys are stored per organization. The key list and revocation act on the
same organization-wide pool from any project.
For automated setup, a caller authenticated with an API key has principal kind
api_key and qualifies through project:admin or *, without an organization
membership check. Callers with principal kind human or the internal service
kind need both organization owner/admin membership and project:admin (or *). Every creator can grant only scopes they already hold; * permits any
scope. Use the project API. In local development, first create a
cookie session and read the seeded project ID:
For a client that creates and watches sessions, grant only:
Add tool_call:claim and tool_call:submit only when the integration executes
caller-owned tools. Add run:cancel only when it should cancel runs.
Skill administration requires explicit permissions: skill:read to list and export
packages, skill:write to create drafts and publish versions, skill:bind together
with agent:write to attach versions to agents, and skill:revoke to deprecate or
revoke versions. Delegation inspection uses delegation:read. Messaging requires
both delegation:message and run:create; cancellation requires both
delegation:cancel and run:cancel.
Both the API-key and member permission pickers include these scopes.
If a key receives 403 Principal lacks the required scope, inspect
GET /v1/context with that key and compare principal.scopes with the operation’s
requirements. Keys created before these picker options were available keep their
original permissions. Create a replacement with the needed scopes, update the
integration, verify access, then revoke the old key. Selecting all listed scopes
creates an explicit list; it does not grant * or future permissions.
If creation returns Organization owner or admin role is required, ask an
organization owner to check your organization membership. A project admin role
or the All scopes label does not satisfy that membership check.
MCP administration uses separate least-privilege scopes: mcp:read,
mcp:write, mcp:discover, and mcp:bind. Credential metadata, creation,
rotation, and revocation use credential:read_metadata, credential:write,
credential:rotate, and credential:revoke. A run creator needs mcp:execute
at call time; OAO rechecks this live before decrypting the bound credential.
Create and capture the key once:
The first successful console or API response contains shown: true and
secret. A replay of the same API request deliberately omits the secret and
returns shown: false. OAO stores a keyed hash, not plaintext, so copy the
secret into your secret manager immediately.
Send the key as a bearer credential:
Cross-project key resolution
An API key resolves its organization and scopes from the key record, then
selects the acting project per request: a /v1/projects/{projectId}/... path
picks that project (it must belong to the key’s organization); any other path
falls back to the organization’s earliest-created project. On first use inside
a project, OAO materialises a per-project mirror principal of kind api_key
for the key, so audit entries, idempotency records, and runs always reference
a project-local principal. The same key therefore works in new projects
without any additional setup.
Project creation and deletion require an organization owner or admin
membership for human principals. API keys qualify through their scopes: * or
project:admin. For DELETE /v1/projects/{projectId} an API key deliberately
acts from its default (earliest-created) project rather than the addressed
one, so it can delete the project named in the path — except its own acting
project, and no caller can delete the organization’s last project.
Keep OAO API keys on a server. Do not embed them in browser JavaScript, mobile
bundles, repository files, query strings, logs, or SSE URLs.
Write safety
Every project-state mutation requires a caller-generated Idempotency-Key header.
Retry the same logical request with the same key and identical body. Reusing a
key with a different body produces a conflict instead of creating ambiguous
duplicate work.
Keys are scoped to the authenticated principal and route. Keep them opaque,
non-empty, and at most 200 characters.
Browser sessions
The React console uses secure HTTP-only cookies in WorkOS mode. IAP browser
requests are authenticated by the signed edge assertion. Unsafe browser writes
are origin-checked in both modes. API-key clients do not need
the browser cookie or a custom tenant header: the key resolves its organization,
project, principal, and scopes before tenant queries run.
The context response keeps the PostgreSQL principal subject as the stable
authorization identity and may add provider-supplied displayName for the UI.
For example, a provisioned WorkOS user can display as Ben Selleslagh while
still authorizing as the existing development-user principal. The display
name does not grant scopes or change tenant membership.
The console receives its configured authentication provider at startup. In
WorkOS mode, a missing session goes directly through POST /v1/auth/login and
the HTTPS redirectUrl returned by the API; it does not probe or establish the
development identity. The callback sets the HTTP-only session cookies and
redirects back to the configured app origin. While that navigation is in
progress, concurrent console requests reuse the same login attempt so they
cannot replace its one-time state cookie. If the provider request fails before
navigation starts, the console releases that failed attempt so a later retry can
request a new login without requiring a page reload.
When the short-lived access session expires, the console uses
POST /v1/auth/refresh once and retries the interrupted request. Concurrent
requests share that refresh attempt. The refresh credential stays in its own
HTTP-only, strict-same-site cookie, whose lifetime is configured with
WORKOS_COOKIE_MAX_AGE and defaults to 400 days. A rejected refresh starts a
fresh hosted login; a transient provider failure remains retryable without
forcing a logout or requiring a page refresh.
In WorkOS mode, the console shows Log out beside the signed-in user. It
posts to POST /v1/auth/logout, which clears OAO’s HTTP-only session and
refresh cookies. When WorkOS returns a hosted logout URL, the console follows
that HTTPS redirect to end the provider session too. Development auth omits the
button because local requests intentionally resolve to the deterministic
development principal without an external session.
If a WorkOS browser session is missing, cannot be refreshed, or belongs to a
different configured application origin, the console starts a fresh hosted
sign-in flow instead of leaving the current page in an error state. Recovery is limited to
401 unauthenticated responses and the specific 403 forbidden response
Request origin is not allowed; ordinary authorization failures remain visible
and are not converted into login loops. POST /v1/auth/login accepts this
recovery even when stale cookies are present and still uses only the
server-configured callback and application origin. The successful callback
replaces the stale local session.
The callback state is single-use and expires after ten minutes. If a WorkOS
callback is reopened after that state was consumed, arrives without its state
cookie, or contains a mismatched state value, OAO creates a new state cookie and
redirects directly to a fresh hosted login instead of displaying a JSON
Invalid authentication state error. Development-provider callbacks remain
strict and return the explicit 400 response for invalid state.
Manage project members
The console’s Organization and Projects pages read the active tenant
and project directly from the API. The Projects page can create a project
(New project) and delete a non-active project; creation provisions the
creator’s principal, owner membership, identity records, and tenant links in
the new project within one transaction. The console switches its active
project through POST /auth/switch-project: the endpoint verifies that the
signed-in subject has a provisioned principal and membership in the target
project, then sets an oao_active_project cookie. Every later request
re-resolves that principal, so revoked membership or a deleted project falls
back to the session’s own project, and logout clears the cookie. For WorkOS
bearer identities provisioned into several projects, the request’s URL path
selects the project; without one, the earliest-created project is used.
The Members page lists project principals with their roles, scopes, and safe
WorkOS display metadata. A project administrator can add an OAO principal,
change another member’s role, or remove another member. The active principal’s
role and removal actions are disabled in the console to prevent accidental
self-lockout.
Adding an OAO member is not a WorkOS invitation. For hosted sign-in, first add
the project principal, then run the explicit WorkOS provisioning flow with that
principal’s ID, the existing organization and project IDs, and the verified
WorkOS user and organization IDs.
See apps/api/README.md in the repository for the exact WorkOS callback,
webhook, cookie, and local proxy environment variables.