Where your OpenFeed keypair comes from.
Under the Recommended client profile a single RSA key does two jobs: it
authenticates the client (private_key_jwt) and it signs every DPoP proof.
OpenFeed holds the public half, which you register as a JWKS.
Keys are never generated implicitly
This is the important part, and it is a deliberate departure from the obvious design. A store that lazily generates a key when it finds none is convenient on one machine and broken on two: each node mints a different key, signs proofs with it, and OpenFeed rejects them because that key is not in the registered JWKS. The failure is intermittent and load-balancer-dependent, which makes it about as unpleasant as bugs get.
So private_jwk/1 returns {:error, {:no_key, ...}} when no key exists.
Generating one is an explicit, operator-driven act:
mix openfeed.gen.key # generate and persist
mix openfeed.jwks # print the public JWKS to registerChoosing a store
| Store | Multi-node safe | Notes |
|---|---|---|
OpenFeed.KeyStore.Env | yes | Key supplied out of band. Good for releases and 12-factor deploys. |
AshOpenFeed.KeyStore.Ash | yes | Key in your database, shared by every node. Can be encrypted with ash_cloak. |
OpenFeed.KeyStore.File | no | Single-node and local dev only, unless the path is shared storage. |
Caching
Every API call signs a DPoP proof, and every proof needs the private key, so
the key is cached in :persistent_term after first read. Pass cache: false
in the store options to disable, and call invalidate/1 after rotating a key
in a running VM. The key material lives in memory either way.
Implementing your own
defmodule MyApp.VaultKeyStore do
@behaviour OpenFeed.KeyStore
@impl true
def private_jwk(opts) do
case MyApp.Vault.read(opts[:path]) do
{:ok, json} -> {:ok, OpenFeed.KeyStore.from_json!(json)}
:error -> {:error, {:no_key, opts[:path]}}
end
end
end
Summary
Functions
The only signing algorithm OpenFeed accepts (PS256).
Parse a JWK from JSON. Returns {:error, {:invalid_key, reason}} on bad input.
Like from_json/1 but raises.
Generate a fresh PS256 signing key.
Drop any cached key for this config. Call after rotating a key in a live VM.
The JWKS to register with OpenFeed, as a map.
Load the private JWK for a config.
The public half of the key, as a bare JWK map.
Serialise a private JWK to JSON, for storage.
The public half of a JWK, as a bare map.
Types
Callbacks
@callback private_jwk(opts :: keyword()) :: {:ok, JOSE.JWK.t()} | {:error, error()}
Load the private JWK.
Must return {:error, {:no_key, detail}} — not generate a key — when none
exists, so callers can tell "not set up yet" apart from "misconfigured".
@callback put_private_jwk(jwk :: JOSE.JWK.t(), opts :: keyword()) :: :ok | {:error, term()}
Persist a private JWK, for mix openfeed.gen.key.
Optional: read-only stores (Env) should not implement it.
Functions
@spec alg() :: String.t()
The only signing algorithm OpenFeed accepts (PS256).
@spec from_json(String.t()) :: {:ok, JOSE.JWK.t()} | {:error, {:invalid_key, term()}}
Parse a JWK from JSON. Returns {:error, {:invalid_key, reason}} on bad input.
@spec from_json!(String.t()) :: JOSE.JWK.t()
Like from_json/1 but raises.
@spec generate() :: JOSE.JWK.t()
Generate a fresh PS256 signing key.
In memory only — persisting it is the store's job. The kid is the RFC 7638
thumbprint, so the JWS header and the registered JWKS agree on how to name
the key without any extra bookkeeping.
@spec invalidate(OpenFeed.Config.t()) :: :ok
Drop any cached key for this config. Call after rotating a key in a live VM.
@spec jwks(OpenFeed.Config.t()) :: {:ok, map()} | {:error, error()}
The JWKS to register with OpenFeed, as a map.
Register the JSON of this at your app registration so OpenFeed can verify
your private_key_jwt assertions and DPoP proofs.
@spec private_jwk(OpenFeed.Config.t()) :: {:ok, JOSE.JWK.t()} | {:error, error()}
Load the private JWK for a config.
Caches in :persistent_term unless the store options say cache: false.
@spec public_jwk_map(OpenFeed.Config.t()) :: {:ok, map()} | {:error, error()}
The public half of the key, as a bare JWK map.
@spec to_json(JOSE.JWK.t()) :: String.t()
Serialise a private JWK to JSON, for storage.
@spec to_public_map(JOSE.JWK.t()) :: map()
The public half of a JWK, as a bare map.