> ## Documentation Index
> Fetch the complete documentation index at: https://docs.oao.sh/llms.txt
> Use this file to discover all available pages before exploring further.

# Authentication

> Authenticate browser users and project integrations without mixing identity with tenant authorization.

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](/getting-started/gcp) 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:

```bash theme={null}
API=http://127.0.0.1:3000
ORIGIN=http://127.0.0.1:8080
COOKIE_JAR=/tmp/oao-docs-cookies.txt

curl -sS -c "$COOKIE_JAR" -X POST \
  -H "Origin: $ORIGIN" \
  "$API/v1/auth/development/login" >/dev/null

PROJECT_ID="$(curl -sS -b "$COOKIE_JAR" "$API/v1/context" | jq -r '.project.id')"
```

For a client that creates and watches sessions, grant only:

```text theme={null}
agent:read
session:read
session:write
run:create
run:read
```

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:

```bash theme={null}
curl -sS -b "$COOKIE_JAR" -X POST \
  -H "Origin: $ORIGIN" \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: create-codebase-integration-key-v1' \
  "$API/v1/projects/$PROJECT_ID/api-keys" \
  --data-binary '{
    "name": "Codebase integration",
    "scopes": [
      "agent:read",
      "session:read",
      "session:write",
      "run:create",
      "run:read"
    ]
  }' | jq
```

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:

```http theme={null}
Authorization: Bearer oao_your_secret
```

## 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.

<Warning>
  Keep OAO API keys on a server. Do not embed them in browser JavaScript, mobile
  bundles, repository files, query strings, logs, or SSE URLs.
</Warning>

## 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.

```http theme={null}
Idempotency-Key: 7f44a7de-f878-4c4c-b477-3f680481dc38
```

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.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.