Skip to main content

OAuth Federation

Each agent is a registered OAuth client of the marketplace. The marketplace is the OIDC Identity Provider, each agent's backend is the Relying Party.

This is the only trust boundary between the marketplace and agent services. Get it right and everything else falls into place.

OIDC endpoints

PathPurpose
/api/auth/.well-known/openid-configurationOIDC discovery (use this from your OIDC client library)
/api/auth/.well-known/oauth-authorization-serverRFC 8414 OAuth metadata
/api/auth/jwksJWKS for ID token verification. EdDSA (Ed25519). Auto-rotates.
/api/auth/oauth2/authorizeAuthorization endpoint. PKCE-S256 mandatory.
/api/auth/oauth2/tokenToken exchange. form-urlencoded body, not JSON.
/api/auth/oauth2/userinfoUserInfo. Bearer token.
/api/auth/oauth2/endsessionRP-initiated logout.
/api/auth/oauth2/revokeRFC 7009 token revocation.
/api/auth/oauth2/introspectRFC 7662 token introspection.
/api/auth/oauth2/consentConsent submission (SPA-only).
/api/auth/oauth2/callback/marketplace(on the agent backend, not marketplace) — your callback

Client provisioning

When you POST /agents, the marketplace creates a better-auth oauthClient row:

PropertyValue
typepublic
tokenEndpointAuthMethodnone — no client_secret
grantTypes['authorization_code', 'refresh_token']
responseTypes['code']
skipConsenttrue (first-party agents)
redirectUrismirrored from the agent row

Public clients are correct here because each agent backend can keep secrets, but the same OAuth flow runs through the browser (you'll always want PKCE).

Scopes

ScopeEffect
openidRequired; issues an ID token
profileAdds name, picture to the ID token
emailAdds email to the ID token
marketplace:roleAdds the user's RBAC role (platform_admin, org_admin, org_member)
marketplace:installsAdds list of installed agent IDs to UserInfo (not the ID token, to keep it small)
marketplace:agentTiersAdds a { [agentId]: tierName } map — a hint, not source of truth
offline_accessIssues a refresh_token

Request all of them except marketplace:installs for most agents.

Tokens

TokenAlgorithmLifetimeCarries
ID tokenEdDSA (Ed25519) signed10hsub, email, name, picture, marketplace:role, marketplace:agentTiers
Access tokenOpaque10hBearer credential for marketplace API calls
Refresh tokenOpaqueLong-lived, rotated on every useUsed to mint new access tokens

ID token validation must use the JWKS endpoint. Use createRemoteJWKSet from jose (Node) or your library's equivalent — it caches and respects key rotation.

Authorization Code + PKCE flow

Refresh

When the access token expires, your client calls POST /api/auth/oauth2/token with grant_type=refresh_token. The response includes a new refresh token. The old one is invalid immediately (rotation). Don't reuse refresh tokens — that's a reuse-detection signal that better-auth treats as compromise.

Logout

Two-tier:

  1. Agent-side: clear your own session cookie.
  2. Marketplace-side: GET /api/auth/oauth2/endsession?post_logout_redirect_uri=...&id_token_hint=... to end the user's marketplace session.

Most agents just do (1) and let the marketplace session live. If a user signs out on the marketplace, they're signed out of every agent that tries to refresh after their session ends.

First-party vs third-party

The marketplace currently treats every agent as first-party (skipConsent: true). If you ever onboard third-party agents, set skipConsent: false and the user will see a consent screen at /consent on first authorize.

See also