Skip to main content

OAuth troubleshooting

"invalid_redirect_uri"

The redirect_uri you sent to /authorize doesn't match any entry in the agent's oauthClient.redirect_uris (which is mirrored from agents.redirectUris).

Check:

curl https://api.fleapo.ai/agents/by-slug/<slug> | jq '.data.redirectUris'

The string must match exactly — including protocol, host, port, and path. https://app.example.com/cbhttps://app.example.com/cb/.

Update via POST /agents/:id with the corrected list.

"code_challenge missing" / "PKCE required"

You're not sending PKCE. The marketplace requires code_challenge (S256). Better Auth sends it automatically; if you're rolling your own:

const codeVerifier = base64url(crypto.randomBytes(32));
const codeChallenge = base64url(sha256(codeVerifier));
// include code_challenge + code_challenge_method=S256 in /authorize
// include code_verifier in /token

"invalid_grant" at /token

The most common causes:

  • Reusing an already-used code (codes are single-use).
  • Mismatched code_verifier (PKCE didn't match).
  • The code expired (codes are short-lived — minutes, not hours).

Solution: restart the OAuth flow from /authorize.

"Refresh token reuse detected"

You used an old refresh token after it was rotated. better-auth invalidates the entire chain. The user must sign in fresh.

Why this might happen:

  • Two processes are sharing the same refresh token and both refreshed concurrently.
  • A snapshot/restore brought back stale tokens.
  • An attacker actually got your token.

Investigate audit_logs / PostHog for the signature.

"Callback succeeds but session doesn't persist"

The agent backend exchanged the code, got tokens, hit userinfo — but the user lands on / and appears unauthenticated.

Almost always a cookie issue:

  • Cross-origin agent (api.my-agent.comapp.my-agent.com) needs SameSite=None; Secure.
  • Browser silently drops cookies that don't match SameSite policy.
  • Open DevTools → Application → Cookies → see if the cookie is set on the agent backend's domain.

"JWT verification fails with 'no signing key'"

Your JWKS cache is stale. The marketplace rotates keys; if you cached a static set you'll miss new ones.

Use createRemoteJWKSet(new URL(jwksUrl)) from jose. It auto-refreshes.

"Token has expired"

Your access_token is past 10h TTL. Refresh:

POST /api/auth/oauth2/token
grant_type=refresh_token
refresh_token=...

The response includes a new refresh token. Save it and discard the old.

"marketplace:agentTiers claim is wrong"

The claim is a hint, not source of truth. It's frozen at token issue time. If the user upgraded after token issue, the claim is stale.

Always call GET /me/subscriptions/:agentId for live state.

"User can't sign out of the marketplace"

POST /api/auth/sign-out from any UI that has a session cookie. The browser must include credentials: 'include'.

For RP-initiated logout from an agent:

GET ${MP}/api/auth/oauth2/endsession?id_token_hint=<token>&post_logout_redirect_uri=<your-page>

The user is bounced to the marketplace, signed out, then back to the redirect URI.

For first-party agents the OAuth client has skipConsent: true. If you're seeing /consent, it's because:

  • The client was created without skipConsent: true (audit the row).
  • A new scope was requested that the user hasn't granted before.

Re-provision the client with the desired skipConsent setting.