# ai‑memory Authentication Ladder: From Static Bearer Tokens to OIDC Device‑Flow Tokens

> Explore the ai-memory auth ladder, evolving from static bearer tokens to OIDC device-flow tokens for robust authentication. Learn how it maps requests to capability tiers.

- Repository: [Fabio Akita/ai-memory](https://github.com/akitaonrails/ai-memory)
- Tags: deep-dive
- Published: 2026-08-30

---

**ai‑memory implements a four‑rung "auth ladder" that resolves every HTTP request to a specific capability tier, with static bearer tokens at the base and OIDC device‑flow tokens enabling per‑developer database users.**

The auth ladder in [akitaonrails/ai‑memory](https://github.com/akitaonrails/ai-memory) provides a deliberately ordered authentication system where the first matching credential wins. This design prevents privilege escalation and ensures predictable capability boundaries across single‑user deployments, multi‑user teams, and SSO‑proxied environments.

## The Four Rungs of the Authentication Ladder

The ladder processes credentials in strict priority order. Each rung represents a distinct trust boundary with specific capability limits.

| Rung | Trigger | Credential Source | Resulting Capability |
|:---|:---|:---|:---|
| **0 – Anonymous** | No `[auth].bearer_token` configured | — | Requests accepted with anonymous `ActorContext` (historic single‑user default) |
| **1 – Root** | `Authorization: Bearer <root‑token>` matches configured `bearer_token` | Static bearer token in server config | **Root** access; writes attributed to `root_username` if configured |
| **1b – Proxy‑Asserted User** | `Authorization: Bearer <proxy‑token>` matches `actor_proxy_bearer_token` **and** request carries `X‑Memory‑Actor‑*` headers | Separate static bearer for trusted SSO proxy | **User** (or **root** if OIDC issuer + subject matches root identity) |
| **2 – DB User** | Bearer token hashes to row in `users.token_hash` | Per‑user bearer tokens in SQLite `users` table | Capabilities of that specific user; `/admin/*` routes remain root‑only |
| **3 – 401** | Bearer present but matches none of above | — | Request rejected with `401 Unauthorized` |

The ladder termination at first match prevents a DB user token from ever being interpreted as root, even if tokens collide by accident.

## Core Implementation: [`crates/ai-memory-mcp/src/auth.rs`](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-mcp/src/auth.rs)

The `AuthState` struct in [`auth.rs`](https://github.com/akitaonrails/ai-memory/blob/main/auth.rs) orchestrates the entire ladder. It holds the root token, optional proxy bearer, and an optional `MultiUserResolver` for database lookups.

```rust
// AuthState construction and configuration methods
// Lines 64-71: Enable trusted proxy bearer
pub fn with_trusted_proxy_bearer(mut self, token: impl Into<Option<String>>) -> Self {
    self.actor_proxy_bearer = token.into();
    self
}

// Lines 73-81: Enable multi-user DB lookups
pub fn with_multiuser(
    mut self,
    pepper: TokenPepper,
    reader: MultiUserReader,
    writer: MultiUserWriter
) -> Self {
    self.resolver = Some(MultiUserResolver::new(pepper, reader, writer));
    self
}

// Lines 88-99: Constructor initializing the root token
pub fn new(expected: impl Into<Option<String>>) -> Self {
    Self {
        expected: expected.into().map(TokenHash::from),
        actor_proxy_bearer: None,
        resolver: None,
    }
}

```

The `require_bearer` middleware performs **constant‑time token comparison** using `subtle::ConstantTimeEq` to prevent timing attacks, then routes through the appropriate rung logic.

### Ladder Execution Flow

1. **Bearer extraction** — Parse `Authorization` header; missing header → Rung 0 (anonymous)
2. **Root check** — Constant‑time compare to `AuthState::expected`; match → Rung 1
3. **Proxy check** — Compare to `actor_proxy_bearer`; on success extract `X‑Memory‑Actor‑Issuer` and `X‑Memory‑Actor‑Sub` headers
4. **DB‑user lookup** — Hash token with server‑wide `TokenPepper`, query `reader.find_active_user_by_token_hash`; hit → Rung 2, miss → Rung 3

## OIDC Device‑Flow Integration

The authentication ladder reaches its most dynamic tier through OIDC device‑flow tokens, implemented across two crates.

### Token Acquisition: [`crates/ai-memory-llm/src/oidc.rs`](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-llm/src/oidc.rs)

The OIDC implementation follows the [RFC 8628](https://tools.ietf.org/html/rfc8628) device authorization flow:

```rust
// Key operations in the OIDC module
pub async fn discover(issuer: &Url) -> Result<OidcMetadata, OidcError>;

pub async fn request_device_code(
    client: &reqwest::Client,
    endpoint: &DeviceAuthorizationEndpoint,
    client_id: &str,
    scopes: &[String]
) -> Result<DeviceCodeResponse, OidcError>;

pub async fn poll_token_once(
    client: &reqwest::Client,
    token_endpoint: &TokenEndpoint,
    client_id: &str,
    device_code: &str
) -> Result<PollResult, OidcError>;

```

The resulting `OidcTokenResponse` is persisted as an `OidcToken` structure containing access token, refresh token, and OIDC metadata.

### CLI Integration: [`crates/ai-memory-cli/src/commands/auth.rs`](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-cli/src/commands/auth.rs)

Developers initiate device flow through the CLI:

```bash
$ ai-memory auth login oidc-device --issuer https://idp.example \
    --client-id ai-memory-dev --timeout 300

```

The command:
- Discovers endpoints via `.well-known/openid-configuration`
- Requests device code and displays user verification URL
- Polls token endpoint until authorization completes
- Saves tokens to `~/.local/share/ai-memory/auth.json` via `OidcToken::save`

The HTTP client (`ai_memory_cli::http_client`) subsequently reads this file to set `Authorization: Bearer <access-token>` headers.

## DB User Rung: Token Storage and Verification

Per‑user bearer tokens populate **Rung 2** of the ladder. These are created during `ai-memory init` and stored with cryptographic hashing:

```rust
// Example: configuring the auth ladder in ai-memory.toml
[auth]
bearer_token = "root-static-token"               # Rung 1

actor_proxy_bearer_token = "proxy-static-token"  # Rung 1b (optional)

token_pepper = "random-pepper-bytes"              # Enables Rung 2 DB-user lookups

secure_cookie = true
root_issuer = "https://idp.example"
root_subject = "root-subject"

```

The `TokenPepper` (server‑wide constant) ensures that token hashes remain non‑portable across server instances even if the `users` table is compromised.

## Trusted Proxy Pattern (Rung 1b)

For SSO‑terminated deployments, a forward proxy authenticates via its own static bearer while asserting the human identity through headers:

```http
GET /api/v1/workspaces/ws/projects/p/handoffs HTTP/1.1
Authorization: Bearer proxy-static-token
X-Memory-Actor-Issuer: https://idp.example
X-Memory-Actor-Sub: oidc-subject-alice

```

The server validates this request by:
- Confirming `proxy-static-token` matches `actor_proxy_bearer_token`
- Extracting the OIDC pair from `X-Memory-Actor-*` headers
- Creating `ActorContext` with `issuer` = `https://idp.example`, `sub` = `oidc-subject-alice`

**Root elevation rule:** If the OIDC pair exactly matches the configured `root_issuer` + `root_subject`, the request is upgraded from **user** to **root**. This is tested in `oidc_root_requires_the_exact_issuer_and_subject_pair` ([[`crates/ai-memory-mcp/tests/auth.rs`](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-mcp/tests/auth.rs)](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-mcp/tests/auth.rs), lines 50‑99).

## Validating the Ladder: Test Coverage

The comprehensive test suite in [`crates/ai-memory-mcp/tests/auth.rs`](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-mcp/tests/auth.rs) exercises each rung:

- **`proxy_asserting_oidc_identity_is_downgraded_to_user_level`** (lines 16‑47): Confirms that proxy‑asserted identities without root‑matching OIDC pairs receive user‑level capabilities
- **`oidc_root_requires_the_exact_issuer_and_subject_pair`** (lines 50‑99): Validates that root elevation requires precise OIDC pair matching, preventing partial matches

These tests use the actual `AuthState` and `MultiUserResolver` implementations to ensure the ladder behaves identically in production and test environments.

## Summary

- **ai‑memory's auth ladder** provides four strictly ordered rungs from anonymous access to root privileges
- **Static bearer tokens** serve root (Rung 1) and proxy authentication (Rung 1b) with constant‑time comparison
- **OIDC device‑flow tokens** enable per‑developer authentication without shared secrets, populating the DB‑user rung (Rung 2)
- **Trusted proxy headers** allow SSO termination while preserving identity attribution through `X-Memory-Actor-*` headers
- **All components** are implemented in [`crates/ai-memory-mcp/src/auth.rs`](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-mcp/src/auth.rs), [`crates/ai-memory-llm/src/oidc.rs`](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-llm/src/oidc.rs), and [`crates/ai-memory-cli/src/commands/auth.rs`](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-cli/src/commands/auth.rs)

## Frequently Asked Questions

### How does the auth ladder prevent token confusion between rungs?

The ladder uses **strict priority ordering** and **early termination**. Each incoming bearer is tested against root, then proxy, then hashed for DB lookup. A token matching the root pattern can never be interpreted as a DB user token, even if an attacker registers a colliding hash. The `subtle::ConstantTimeEq` comparisons prevent timing side channels during these checks.

### Can I use OIDC tokens directly with the ai‑memory server without a proxy?

Yes. When you run `ai-memory auth login oidc-device`, the CLI stores the OIDC access token in [`auth.json`](https://github.com/akitaonrails/ai-memory/blob/main/auth.json). The HTTP client sends this as `Authorization: Bearer <token>`. The server hashes this token with `token_pepper` and looks it up in `users.token_hash`. If you pre‑register the token hash via `ai-memory init`, this places you at **Rung 2** (DB user).

### What happens if my OIDC issuer and subject match the root configuration?

If your proxy‑asserted or directly‑presented OIDC `issuer` + `subject` pair exactly matches the server's `root_issuer` and `root_subject` values, the auth ladder **elevates you to root** regardless of which token (proxy or DB user) authenticated the request. This allows designated administrators to retain root capabilities through OIDC flows rather than static tokens.

### Where are per‑user bearer tokens created and stored?

Per‑user tokens are generated during `ai-memory init` and stored in the SQLite `users` table with **peppered SHA‑256 hashes** (not plaintext). The `TokenPepper` is a server‑wide secret that prevents rainbow table attacks even if the database is exposed. Token verification uses `MultiUserResolver::find_active_user_by_token_hash` against a read pool for performance.