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

The AI-Memory CLI authenticates native hook commands using the OpenID Connect (OIDC) Device Authorization Grant, storing tokens in 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, 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.

// 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 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 initiates a refresh token grant using the stored refresh token. The new access token (and potentially new refresh token) updates the auth.json entry before the HTTP request proceeds.

// 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, the system first checks for an explicit --auth-token argument. If absent, it loads the stored OIDC token from 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.

// 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), 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, 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.

// 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 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 OIDC discovery, device code requests, token polling, and refresh token grants
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 Token resolution and bearer token attachment for hook execution
crates/ai-memory-core/src/actor.rs Actor identity construction from OIDC claims
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, polling for tokens after presenting the user with a verification URL and code.
  • Token Storage: Valid tokens persist in 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 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 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.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →