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
| Path | Purpose |
|---|---|
/api/auth/.well-known/openid-configuration | OIDC discovery (use this from your OIDC client library) |
/api/auth/.well-known/oauth-authorization-server | RFC 8414 OAuth metadata |
/api/auth/jwks | JWKS for ID token verification. EdDSA (Ed25519). Auto-rotates. |
/api/auth/oauth2/authorize | Authorization endpoint. PKCE-S256 mandatory. |
/api/auth/oauth2/token | Token exchange. form-urlencoded body, not JSON. |
/api/auth/oauth2/userinfo | UserInfo. Bearer token. |
/api/auth/oauth2/endsession | RP-initiated logout. |
/api/auth/oauth2/revoke | RFC 7009 token revocation. |
/api/auth/oauth2/introspect | RFC 7662 token introspection. |
/api/auth/oauth2/consent | Consent 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:
| Property | Value |
|---|---|
type | public |
tokenEndpointAuthMethod | none — no client_secret |
grantTypes | ['authorization_code', 'refresh_token'] |
responseTypes | ['code'] |
skipConsent | true (first-party agents) |
redirectUris | mirrored 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
| Scope | Effect |
|---|---|
openid | Required; issues an ID token |
profile | Adds name, picture to the ID token |
email | Adds email to the ID token |
marketplace:role | Adds the user's RBAC role (platform_admin, org_admin, org_member) |
marketplace:installs | Adds list of installed agent IDs to UserInfo (not the ID token, to keep it small) |
marketplace:agentTiers | Adds a { [agentId]: tierName } map — a hint, not source of truth |
offline_access | Issues a refresh_token |
Request all of them except marketplace:installs for most agents.
Tokens
| Token | Algorithm | Lifetime | Carries |
|---|---|---|---|
| ID token | EdDSA (Ed25519) signed | 10h | sub, email, name, picture, marketplace:role, marketplace:agentTiers |
| Access token | Opaque | 10h | Bearer credential for marketplace API calls |
| Refresh token | Opaque | Long-lived, rotated on every use | Used 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:
- Agent-side: clear your own session cookie.
- 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
- Track A — Build the agent service
- Flow: OAuth login
- Security: OAuth threat model
- The existing internal doc
marketplace-fleapoai-service/docs/OAUTH.md.