# Auth Runtime Behavior

This guide explains how Athena Auth behaves at runtime today, beyond the route map in this folder.

It covers four recurring points of confusion:

- what the admin API actually authenticates with
- where roles are stored
- why direct browser calls can fail on CORS before auth runs
- how to trace `"No handler found for this request"` and other auth errors in logs

All external URLs are relative to `AuthConfig.base_path`. Internally, `AthenaAuth::handle_request(...)` strips that prefix before core and plugin route matching. If your configured base path is `/api/auth`, the external URL is `/api/auth/admin/has-permission` while the admin plugin itself matches `/admin/has-permission`.

## Admin API Authentication Model

Admin endpoints do not use a separate admin credential format.

The current admin plugin path is:

1. Extract a normal auth session token from the request.
2. Resolve it to an active session.
3. Load the user attached to that session.
4. Compare `user.role()` against `AdminConfig.admin_role`.

In other words:

- the admin API uses the normal Athena Auth session system
- the bearer value is the session token itself, not a separate admin token format
- the role check happens after session resolution

### Accepted credential shapes

The current session extraction order is:

1. `Authorization: Bearer <session-token>`
2. `Cookie: <configured-session-cookie-name>=<session-token>`

If both are present, the `Authorization` header wins.

The default session cookie name is:

```text
athena-auth.session-token
```

That means the two common working forms are:

```http
Authorization: Bearer session_xxx
```

or:

```http
Cookie: athena-auth.session-token=session_xxx
```

There is no separate `Athena-Auth.Session-Token` request header contract in the current Rust implementation. The dot-separated name above is the cookie name.

### What fails where

- No bearer token and no configured session cookie: unauthenticated
- Token present but no active session resolves: session not found
- Active session resolves but `user.role` does not match the configured admin role: forbidden

The default admin role is `"admin"`. The default fallback non-admin role used by the admin plugin is `"user"`.

## Where Role Is Stored

There are multiple role columns in the schema, and they do different jobs.

### Global auth/admin role

`athena.users.role` is the global user role.

This is the role the admin plugin checks when deciding whether a caller is an admin. If you are asking "where does the admin API decide whether this user is admin?", this is the column that matters.

### Organization membership role

`athena.member.role` is the organization membership role.

This controls the user's role inside a specific organization membership record. It is not the same thing as the global auth/admin role.

### Invitation role

`athena.invitation.role` is the role attached to a pending invitation.

That is the role intended to be granted when the invitation is accepted. It is also not the global auth/admin role.

### Practical distinction

- changing `athena.users.role` affects admin-plugin authorization
- changing `athena.member.role` affects organization-scoped authorization
- changing `athena.invitation.role` affects what a new member should receive on acceptance

If someone can manage organization membership but still cannot call `/admin/*`, the first thing to check is whether `athena.users.role` is actually set to the configured admin role.

## Direct Browser Requests, CORS, And Shared Session Cookies

When a browser on one origin calls the Rust auth host on another origin, the auth host must pass the CORS preflight before any sign-in or admin logic runs.

That means a failure like:

```text
Request header field x-athena-sdk is not allowed by Access-Control-Allow-Headers in preflight response.
```

is a CORS configuration problem on the auth host, not an auth-session problem.

### Current default allowed headers

The default Rust CORS middleware now includes:

- `Content-Type`
- `Authorization`
- `X-Requested-With`
- `X-Athena-Sdk`
- `X-Athena-Tenant`
- `X-Athena-Client`
- `X-Athena-Proxy-Request-Id`
- `X-Request-Id`

So `x-athena-sdk` is now part of the default allowlist.

### Current allowed-origin behavior

At server startup, the runtime builds the CORS origin allowlist from:

- the origin portion of `base_url`
- every entry in `trusted_origins`

Credentials are allowed, so cookies and authorization headers are intended to work once the origin is trusted.

### Trusted origins are not cookie scope

`trusted_origins` only decides whether a browser origin may call the auth host.
It does not widen the session cookie beyond the auth host itself.

If the browser calls `https://auth.formations.company` directly and your app runs at
`https://app.formations.company`, you need both:

- `TRUSTED_ORIGINS=https://app.formations.company`
- `ATHENA_AUTH_COOKIE_DOMAIN=.formations.company`

Without the shared parent-domain cookie, the browser may successfully sign in
against the auth host, but SSR on the sibling app host will not receive that
host-only auth cookie.

See [Shared session cookies for direct auth](./shared-session-cookies.md) for the
full deployment contract and exact `Set-Cookie` shape.

### What to check when a browser call still fails

1. Confirm the browser origin is included in `TRUSTED_ORIGINS` or matches the configured `base_url` origin.
2. Confirm the request is hitting the Rust auth host you actually updated.
3. Confirm the browser is sending only headers the auth host allows.

`x-athena-sdk` is request metadata. It is not an auth credential by itself.

## Trace IDs And Finding Errors

Standardized Athena Auth errors carry a trace id in both the response and the logs.

### Where the trace id comes from

The current request trace selection order is:

1. `x-athena-proxy-request-id`
2. `x-request-id`
3. generated UUID

If your proxy already attaches a request id, Athena Auth will reuse it instead of inventing a new one.

### Where the trace id appears

On standardized auth errors, the same identifier is echoed in:

- the JSON response body as `traceId`
- the `x-request-id` response header
- the `x-athena-trace-id` response header
- warning/error logs

Example error shape:

```json
{
  "message": "No handler found for this request",
  "version": "<crate-version>",
  "traceId": "<request-trace-id>"
}
```

### What `"No handler found for this request"` means

This is the standardized not-found shape returned when no core route or plugin route matched after base-path normalization.

When that happens, the runtime logs a warning with the same trace id plus routing context, including:

- external path
- internal path after `base_path` stripping
- HTTP method
- configured `base_path`
- whether the request matched a core route
- whether the request matched a plugin route

That is the trace you want to search for in logs or Loki.

For the exact public entry-point contract, see [handleRequestAthenaAuth](../contracts/handleRequestAthenaAuth.md).

## API Keys Are Not The Same Thing As Admin Sessions

A plain `x-api-key` header is not automatically equivalent to an authenticated admin session.

By default, the API-key plugin does not inject a session into protected routes. There is an optional plugin mode that can emulate session access for API-key requests, but that is an explicit plugin configuration choice, not the normal admin-auth contract.

For admin routes, the stable contract to document and depend on is still:

- resolve a normal session
- then verify the caller's global user role

## Source Of Truth

- Session extraction and bearer-vs-cookie precedence: `crates/core/src/session.rs`
- Session-required auth resolution: `crates/core/src/plugin.rs`
- Default session cookie name: `crates/core/src/config.rs`
- Admin auth and role check: `crates/api/src/plugins/admin/mod.rs`
- Global and organization role schema: `sql/provision.sql`
- User and organization role models: `crates/core/src/types.rs`, `crates/core/src/types_org.rs`
- CORS defaults: `crates/core/src/middleware/cors.rs`
- Server CORS origin assembly: `src/bin/server.rs`
- Trace id headers and JSON error shape: `src/bin/server/request_tracing.rs`, `crates/core/src/error.rs`, `src/core/auth.rs`
- End-to-end request contract: [handleRequestAthenaAuth](../contracts/handleRequestAthenaAuth.md)
