Rho.Auth.OAuthFlow (rho v0.1.0)

Copy Markdown View Source

The OAuth authorization-code flows omp signs in with (/login anthropic, /login openai-codex), built as omp's declarative engine builds them from pi-catalog's rules/auth/anthropic.kdl and openai-codex.kdl:

  • Anthropic (Claude Pro/Max): https://claude.ai/oauth/authorize with code=true, PKCE S256, redirect http://localhost:54545/callback; the token endpoint https://api.anthropic.com/v1/oauth/token takes a JSON body (the exchange also sends state; a refresh sends Claude Code's anthropic-beta and User-Agent headers). Access tokens live ~8 h and are stored 5 min short; each refresh rotates the refresh token, and the grant dies ~30 days after the interactive login.
  • OpenAI Codex (ChatGPT Plus/Pro): https://auth.openai.com/oauth/authorize, PKCE S256, redirect exactly http://localhost:1455/auth/callback (the only one OpenAI allows); https://auth.openai.com/oauth/token takes a form body. The ChatGPT account id comes from the access/id token's https://api.openai.com/auth claim.

Credentials are pi's auth.json entry: %{"type" => "oauth", "access", "refresh", "expires" (ms since the epoch), "accountId", "email"} (the last two only when known).

HTTP: opts[:respond] (tests) answers a request instead of the network, fn %Req.Request{} -> %Req.Response{} end, seeing it fully encoded.

Summary

Functions

PKCE S256: base64url(sha256(verifier)), unpadded.

The provider's registered localhost redirect URI (the one start/2 normally gets).

Exchanges an authorization code for credentials. A code#state (what Anthropic's code page shows) is split, the pasted state winning, as omp does. opts: :respond, and :retry_until (a monotonic ms deadline: transport errors are retried every 2 s until then, for a phone whose network Android cuts while the browser is in front).

Trades creds' refresh token for new credentials. The refresh token rotates when the response carries one; identity the response lacks is kept from creds.

A new PKCE verifier and state, and the authorize URL for them: the standard parameters in omp's order, then the provider's extras.

Types

creds()

@type creds() :: %{required(String.t()) => term()}

flow()

@type flow() :: %{verifier: String.t(), state: String.t()}

provider()

@type provider() :: :anthropic | :openai_codex

Functions

challenge(verifier)

@spec challenge(String.t()) :: String.t()

PKCE S256: base64url(sha256(verifier)), unpadded.

default_redirect(atom)

@spec default_redirect(provider()) :: String.t()

The provider's registered localhost redirect URI (the one start/2 normally gets).

exchange(provider, code, map, redirect_uri, opts \\ [])

@spec exchange(provider(), String.t(), flow(), String.t(), keyword()) ::
  {:ok, creds()} | {:error, String.t()}

Exchanges an authorization code for credentials. A code#state (what Anthropic's code page shows) is split, the pasted state winning, as omp does. opts: :respond, and :retry_until (a monotonic ms deadline: transport errors are retried every 2 s until then, for a phone whose network Android cuts while the browser is in front).

refresh(provider, creds, opts \\ [])

@spec refresh(provider(), creds(), keyword()) :: {:ok, creds()} | {:error, String.t()}

Trades creds' refresh token for new credentials. The refresh token rotates when the response carries one; identity the response lacks is kept from creds.

start(provider, redirect_uri)

@spec start(provider(), String.t()) :: %{
  url: String.t(),
  verifier: String.t(),
  state: String.t()
}

A new PKCE verifier and state, and the authorize URL for them: the standard parameters in omp's order, then the provider's extras.