# How OIDC Device Authentication Works for Native Hook Commands in AI-Memory

> Learn how AI-Memory CLI secures native hook commands with OIDC device authentication. Discover token storage and bearer token attachment for robust identity management.

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

---

**The AI-Memory CLI authenticates native hook commands using the OpenID Connect (OIDC) Device Authorization Grant, storing tokens in [`auth.json`](https://github.com/akitaonrails/ai-memory/blob/main/auth.json) and attaching them as bearer tokens while treating the issuer-subject pair as the canonical identity for permission checks.**

The `akitaonrails/ai-memory` repository implements a complete OIDC device flow for CLI authentication that enables native hook commands to execute authenticated HTTP requests without embedding long-lived credentials. This mechanism leverages the standard OAuth 2.0 Device Authorization Grant to support providers like Google, Okta, or any OIDC-compliant identity source.

## The Device Authorization Flow

The implementation follows the RFC 8628 standard, beginning with discovery and concluding with a stored token pair that identifies the user across all hook invocations.

### Discovery and Device Code Request

In [`crates/ai-memory-llm/src/oidc.rs`](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-llm/src/oidc.rs), the system performs **OIDC discovery** to locate the provider's device authorization endpoint. The CLI sends a `DeviceAuthorizationResponse` request using the default scope `openid profile offline_access` defined in `OIDC_DEFAULT_SCOPE`.

When you run `ai-memory auth login`, the CLI prints a verification URL and user code to stdout. The user visits the URL on another device while the CLI enters a polling loop against the token endpoint.

```rust
// crates/ai-memory-llm/src/oidc.rs
let discovery = OidcDiscovery::discover("https://accounts.google.com")?;
let device = discovery.request_device_code(OIDC_DEFAULT_SCOPE)?;
// Prints verification URL and user code
println!("Open {} and enter code: {}", device.verification_uri, device.user_code);

```

### Polling and Token Storage

The `OidcTokenResponse` struct stores the access token, optional refresh token, expiration timestamp, and the OIDC issuer and subject claims. Upon successful authentication, the system writes this data to the shared [`auth.json`](https://github.com/akitaonrails/ai-memory/blob/main/auth.json) file under the `oidc` key.

The polling mechanism continues until the user completes verification or the device code expires. If the provider does not return a `refresh_token`, the system logs this condition for later handling.

## Token Lifecycle and Refresh Mechanisms

Native hooks require valid tokens at execution time, necessitating automatic refresh logic that operates transparently to the user.

### Automatic Refresh on Expiry

When a hook command executes, the CLI checks token expiration. If expired, [`crates/ai-memory-llm/src/oidc.rs`](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-llm/src/oidc.rs) initiates a refresh token grant using the stored refresh token. The new access token (and potentially new refresh token) updates the [`auth.json`](https://github.com/akitaonrails/ai-memory/blob/main/auth.json) entry before the HTTP request proceeds.

```rust
// crates/ai-memory-cli/src/commands/hook_spool.rs
fn resolve_oidc_token() -> Result<OidcTokenResponse> {
    let mut token = auth::load_stored_oidc()?;
    if token.is_expired() {
        // Refreshes via OIDC refresh_token grant
        token = oidc::refresh_token(&token.refresh_token.unwrap())?;
        auth::store_oidc_token(&token)?;
    }
    Ok(token)
}

```

### Handling Missing Refresh Tokens

Some OIDC providers omit refresh tokens in device flows. The system detects this state during the initial token response parsing. If no refresh token exists, the user must re-run `ai-memory auth login` when the access token expires, as noted in the error handling logic that warns "OIDC token response carried no refresh_token".

## Native Hook Command Integration

Hook commands operate as subprocesses that must inherit the CLI's authentication state without explicit credential passing.

### Token Resolution at Runtime

In [`crates/ai-memory-cli/src/commands/hook_spool.rs`](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-cli/src/commands/hook_spool.rs), the system first checks for an explicit `--auth-token` argument. If absent, it loads the stored OIDC token from [`auth.json`](https://github.com/akitaonrails/ai-memory/blob/main/auth.json) and resolves it via the refresh logic described above. The resolved bearer token attaches to all outbound HTTP requests from the hook process.

```rust
// crates/ai-memory-cli/src/commands/hook_spool.rs
fn run_hook() -> Result<()> {
    let token = auth::resolve_oidc_token()?;
    let client = HttpClient::new()
        .bearer(token.access_token); // Authorization: Bearer <token>
    client.post("/api/v1/handoff")?.send()?;
    Ok(())
}

```

### HTTP Header Injection

The bearer token travels in the `Authorization` header. Additionally, the system propagates identity metadata via custom headers `X-Memory-Actor-Iss` and `X-Memory-Actor-Sub` (implemented in [`crates/ai-memory-mcp/src/server.rs`](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-mcp/src/server.rs)), allowing downstream services to identify the operator without token introspection.

## Actor Identity and Security Model

The system treats OIDC identities as the root of trust for all permission calculations, superseding username-based identification.

### OIDC as Canonical Identity

In [`crates/ai-memory-core/src/actor.rs`](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-core/src/actor.rs), an actor is defined by the tuple `(issuer, sub)`. This pair "wins whenever both fields are present", meaning OIDC authentication takes precedence over any display username. The namespace calculations use the prefix `o-` (for OIDC) followed by a hash of the issuer-subject pair to ensure uniqueness across providers.

```rust
// crates/ai-memory-core/src/actor.rs
fn actor_from_headers(headers: &HeaderMap) -> Actor {
    if let (Some(iss), Some(sub)) = (
        headers.get("X-Memory-Actor-Iss"),
        headers.get("X-Memory-Actor-Sub")
    ) {
        return Actor::Oidc {
            issuer: iss.to_string(),
            subject: sub.to_string()
        };
    }
    // Fallback to username-based identity
}

```

### Validation Rules in MCP Auth

The [`crates/ai-memory-mcp/src/auth.rs`](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-mcp/src/auth.rs) module enforces strict validation: it only accepts complete OIDC pairs where both issuer and subject are present. Partial pairs are rejected. Additionally, the root operator configuration requires both `root_issuer` and `root_subject` to be set simultaneously, as the subject claim is only unique within its specific issuer context.

## Implementation Reference

The following files contain the core logic for OIDC device authentication:

| File | Responsibility |
|------|----------------|
| [`crates/ai-memory-llm/src/oidc.rs`](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-llm/src/oidc.rs) | OIDC discovery, device code requests, token polling, and refresh token grants |
| [`crates/ai-memory-cli/src/commands/auth.rs`](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-cli/src/commands/auth.rs) | CLI entry point for `ai-memory auth login` handling the device flow |
| [`crates/ai-memory-cli/src/commands/hook_spool.rs`](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-cli/src/commands/hook_spool.rs) | Token resolution and bearer token attachment for hook execution |
| [`crates/ai-memory-core/src/actor.rs`](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-core/src/actor.rs) | Actor identity construction from OIDC claims |
| [`crates/ai-memory-mcp/src/auth.rs`](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-mcp/src/auth.rs) | Security validation requiring complete issuer-subject pairs |

## Summary

- **OIDC Device Flow**: The CLI implements RFC 8628 via [`crates/ai-memory-llm/src/oidc.rs`](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-llm/src/oidc.rs), polling for tokens after presenting the user with a verification URL and code.
- **Token Storage**: Valid tokens persist in [`auth.json`](https://github.com/akitaonrails/ai-memory/blob/main/auth.json) under the `oidc` key as `OidcTokenResponse` structs containing access tokens, refresh tokens, and identity claims.
- **Automatic Refresh**: Hook commands trigger automatic token refresh when expired, though flows without refresh tokens require manual re-authentication.
- **Identity Model**: The tuple `(issuer, sub)` forms the canonical actor identity, stored in the `o-` namespace and propagated via `X-Memory-Actor-*` headers.
- **Security Enforcement**: The MCP auth layer validates that both issuer and subject are present, rejecting partial OIDC assertions.

## Frequently Asked Questions

### What happens if the OIDC provider doesn't issue a refresh token?

The system logs that "OIDC token response carried no refresh_token" during the initial device flow. When the access token expires, the hook command fails to refresh automatically, requiring you to run `ai-memory auth login` again to obtain a new access token.

### How does the system handle token expiration during hook execution?

Before making any HTTP request, the hook runner in [`crates/ai-memory-cli/src/commands/hook_spool.rs`](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-cli/src/commands/hook_spool.rs) checks token expiration. If expired, it attempts to use the stored refresh token to obtain a new access token transparently before attaching the bearer token to the request.

### Where are OIDC tokens stored locally?

Tokens are stored in the [`auth.json`](https://github.com/akitaonrails/ai-memory/blob/main/auth.json) file located in the AI-Memory configuration directory. This file contains the `OidcTokenResponse` data including access tokens, refresh tokens, expiry times, and the issuer-subject identity pair under the `oidc` key.

### Can native hooks override the stored OIDC token?

Yes. Native hook commands accept an explicit `--auth-token` argument that takes precedence over the stored OIDC token. If provided, the CLI uses this token directly without loading or refreshing the device flow token from [`auth.json`](https://github.com/akitaonrails/ai-memory/blob/main/auth.json).