# How to Implement OIDC Device Authentication for ai-memory Shared Server Hook Writes

> Learn to implement OIDC device authentication for ai-memory server hook writes. Securely obtain JWTs with mcp:read realm role using the Device Authorization Grant. Avoid static tokens.

- Repository: [Fabio Akita/ai-memory](https://github.com/akitaonrails/ai-memory)
- Tags: how-to-guide
- Published: 2026-08-20

---

**The ai-memory server requires a JWT containing the `mcp:read` realm role for hook writes, which the CLI obtains via the OIDC Device Authorization Grant (RFC 8628) instead of static bearer tokens.**

The akitaonrails/ai-memory repository supports per-developer authentication for shared server hook writes using the OpenID Connect (OIDC) Device Flow. This eliminates shared secrets by allowing headless clients to obtain scoped JWTs through a browser-based authorization flow, storing credentials securely in a local JSON file for automatic reuse and refresh.

## Why Use OIDC Device Flow for ai-memory?

Static bearer tokens create security risks in shared environments. The **OIDC Device Authorization Grant** solves this by enabling authentication on devices without browsers (like remote servers or containers) while the actual approval happens on a separate user device. This flow provides **offline_access** scope support, giving the CLI long-lived refresh tokens that avoid repetitive re-authorization while maintaining strict per-user access control. The server validates each JWT against the configured OIDC realm (Keycloak, Auth0, etc.), ensuring the client possesses the mandatory `mcp:read` role before accepting hook writes.

## Configuring Your OIDC Provider

Before running the CLI, configure your identity provider to support the device flow:

1. Create a **public client** (no client secret required) in your OIDC provider admin console.
2. Enable the **Device Authorization Grant** flow for this client.
3. Add the **offline_access** scope to allow refresh tokens.
4. Record the **issuer URL** (e.g., `https://kc.example.com/realms/ai-memory`) and **client ID** (e.g., `my-cli-client`).

These credentials allow the ai-memory client to discover endpoints and initiate the device flow without hardcoded secrets.

## The Client-Side Authentication Flow

The implementation in [`crates/ai-memory-llm/src/oidc.rs`](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-llm/src/oidc.rs) follows the standard RFC 8628 flow through four distinct phases.

### Discovery and Endpoint Detection

The `discover()` function fetches the provider's OpenID Connect discovery document (`.well-known/openid-configuration`) to locate the device authorization and token endpoints dynamically. This ensures the client always uses the current provider configuration without hardcoded URLs.

### Requesting the Device Code

The CLI calls `request_device_code()` to POST the client ID and requested scopes to the device authorization endpoint. The provider returns a `device_code`, `user_code`, and `verification_uri`. The CLI displays these to the user, instructing them to open the URI in a browser and enter the code.

### Polling for Authorization

Once the user visits the verification URI, the CLI enters a polling loop using `poll_token_once()`. This function repeatedly POSTs the `device_code` to the token endpoint at intervals specified by the provider, returning a `PollOutcome` enum indicating:

- **Pending**: Authorization not yet complete; wait and retry.
- **SlowDown**: Polling too fast; increase interval.
- **Denied** or **Expired**: Flow failed; abort.
- **Token**: Success; contains the access and refresh tokens.

### Token Persistence with OidcToken

Upon success, the CLI creates an `OidcToken` struct (a specialization of `StoredOAuthToken` defined in [`crates/ai-memory-llm/src/stored_token.rs`](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-llm/src/stored_token.rs)). This struct stores the access token, refresh token, expiration time, issuer, client ID, and token endpoint metadata (`OidcExtras`). The `OidcToken::save()` method writes this data to `$HOME/.config/ai-memory/auth.json` under the **`"oidc"`** key, preserving existing entries for other providers like OpenAI.

## Implementing the Device Flow in Code

The following Rust example demonstrates the complete flow using the `ai-memory-llm` crate:

```rust
use std::time::Duration;
use ai_memory_llm::oidc::{self, OidcToken};

// 1️⃣ Discover endpoints
let client = reqwest::Client::new();
let discovery = oidc::discover(&client, "https://kc.example.com/realms/ai-memory")
    .await?;

// 2️⃣ Request a device code
let dev = oidc::request_device_code(
    &client,
    &discovery,
    "my-cli-client",
    oidc::OIDC_DEFAULT_SCOPE, // "openid profile offline_access"
)
.await?;
println!("Open {} and enter code {}", dev.verification_uri, dev.user_code);

// 3️⃣ Poll until authorized
loop {
    match oidc::poll_token_once(&client, &discovery, "my-cli-client", &dev.device_code).await? {
        oidc::PollOutcome::Pending => {
            tokio::time::sleep(Duration::from_secs(dev.interval.unwrap_or(5))).await;
        },
        oidc::PollOutcome::SlowDown => {
            tokio::time::sleep(Duration::from_secs(10)).await;
        },
        oidc::PollOutcome::Token(tok) => {
            let token = OidcToken::from_token_response(
                &tok, 
                "https://kc.example.com/realms/ai-memory", 
                "my-cli-client", 
                &discovery.token_endpoint, 
                None
            )?;
            token.save(&std::path::Path::new(&format!("{}/.config/ai-memory/auth.json", std::env::var("HOME")?)))?;
            break;
        },
        other => {
            eprintln!("Authorization failed: {:?}", other);
            break;
        }
    }
}

```

Alternatively, use the **CLI command** provided in [`crates/ai-memory-cli/src/commands/generate_auth_token.rs`](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-cli/src/commands/generate_auth_token.rs):

```bash
ai-memory auth login oidc-device \
    --issuer https://kc.example.com/realms/ai-memory \
    --client-id my-cli-client \
    --scope "openid profile offline_access"

```

## Automatic Token Refresh

The `OidcToken` implementation includes the `needs_refresh()` method, which checks if the token expiration (`expires_at_ms`) is within a configurable threshold. When the CLI executes a hook write, it loads the stored token via `OidcToken::load()`, checks refresh status, and—if necessary—calls `refresh_access_token()` to perform an OAuth2 refresh grant using the stored refresh token. The new access token immediately overwrites the old entry in [`auth.json`](https://github.com/akitaonrails/ai-memory/blob/main/auth.json), ensuring seamless long-lived sessions without manual re-authentication.

## Sending Authenticated Hook Writes

When performing writes, the CLI automatically loads the valid OIDC token and attaches it to HTTP requests. According to the implementation in [`crates/ai-memory-llm/src/auth.rs`](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-llm/src/auth.rs), the request includes:

- The header **`X-Memory-Actor-User`** derived from the JWT's `preferred_username` claim.
- The header **`Authorization: Bearer <access_token>`** containing the JWT itself.

The server receives the request at endpoints like `/hook` or `/handoff`, validates the JWT signature against the OIDC realm, and verifies the payload contains the **`mcp:read`** realm role before processing the write.

Example CLI usage that automatically handles authentication:

```bash
ai-memory hook capture --path notes/example.md --content "Hello world"

```

## Key Implementation Files in the Source Code

Understanding these specific files helps when customizing the authentication flow:

- **[`crates/ai-memory-llm/src/oidc.rs`](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-llm/src/oidc.rs)**: Full implementation including `discover()`, `request_device_code()`, `poll_token_once()`, and token refresh logic.
- **[`crates/ai-memory-llm/src/auth.rs`](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-llm/src/auth.rs)**: Defines the `AuthProvider` trait and integrates `OidcToken` into request headers.
- **[`crates/ai-memory-cli/src/commands/generate_auth_token.rs`](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-cli/src/commands/generate_auth_token.rs)**: CLI entry point that orchestrates the device flow interactive login.
- **[`crates/ai-memory-llm/src/stored_token.rs`](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-llm/src/stored_token.rs)**: Generic storage format used by `OidcToken` to persist credentials in [`auth.json`](https://github.com/akitaonrails/ai-memory/blob/main/auth.json).
- **[`crates/ai-memory-hooks/src/sanitizer.rs`](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-hooks/src/sanitizer.rs)**: Ensures JWT validation occurs before hook payloads are processed by the server.

## Summary

- The ai-memory server requires JWTs with the `mcp:read` realm role for hook writes, rejecting unauthenticated or under-privileged requests.
- The CLI implements the **OIDC Device Authorization Grant** (RFC 8628) in [`crates/ai-memory-llm/src/oidc.rs`](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-llm/src/oidc.rs) to obtain per-developer tokens without browser access on the client device.
- Tokens are stored as **`OidcToken`** entries in `$HOME/.config/ai-memory/auth.json`, supporting automatic refresh via the **offline_access** scope.
- Use **`ai-memory auth login oidc-device`** to initiate the flow, then execute hook commands normally; the CLI handles JWT attachment and renewal automatically.
- The server validates tokens against your configured OIDC provider (Keycloak, Auth0, etc.), ensuring secure, auditable access control for shared ai-memory instances.

## Frequently Asked Questions

### Which OIDC providers are compatible with ai-memory device authentication?

Any provider supporting the **Device Authorization Grant** (RFC 8628) and the **offline_access** scope works, including Keycloak, Auth0, Okta, and Azure AD. The implementation uses standard OIDC discovery, so it automatically adapts to provider-specific endpoints.

### How does the ai-memory CLI know when to refresh the access token?

The `OidcToken` struct tracks expiration via the `expires_at_ms` field. Before each hook write, the CLI calls `needs_refresh()`; if the token is near expiry, `refresh_access_token()` uses the stored refresh token to obtain a new access token without user interaction.

### What happens if I don't have the `mcp:read` realm role?

The server rejects the request with an authorization error. The JWT must contain the **`mcp:read`** role in its realm access claims, as enforced by the authentication middleware when validating the `Authorization` or `X-Memory-Actor-User` headers.

### Where is the authentication state stored on disk?

The CLI persists tokens in **`$HOME/.config/ai-memory/auth.json`** (or platform equivalent). The file contains a JSON object with an `"oidc"` key holding the `OidcToken` data, alongside potential entries for other providers like OpenAI. Use `OidcToken::remove()` to clear stored credentials.