Connect custom OAuth: completed_authorization_request and a live provider token, but /v1/connect/token returns user_authorization_required
Setup
- Custom OAuth connector (type
oauth) against WHOOP's OAuth 2.0 server,tokenEndpointAuthMethod: client_secret_post, user authorization + refresh tokens enabled - Project linked for production, preview, development; calls made from development with the project's OIDC token
@vercel/connect2.0.0, Next.js 16.3.3
What happens
startAuthorization(connector, { subject: { type: "user", id: "<our-user-id>" }, scopes: [...] }, { callbackUrl }), redirect the user to the returned URL.- User clicks Continue on the Connect page, signs in at the provider, taps Allow.
- Connect redirects to
callbackUrl. Observability logscompleted_authorization_requestfor the subject with atokenGroupId, all requested scopes, environment development. getTokenResponse(connector, { subject: { type: "user", id: "<our-user-id>" } })and the rawPOST /v1/connect/token/<connector>both return 401user_authorization_required. Same withforceRefresh, explicit scopes,scopes: ["*"], and with the provider's own user id as the subject.
Reproduced three times on a clean slate (revoked between runs; the provider showed its consent screen again each time).
Why I'm confident Connect holds a token
DELETE /v1/connect/connectors/<connector>/tokens with the same subject returned
{"tokensFound":1,"deleted":1,"providerRevoked":1,"providerSkipped":0,"providerFailed":0}
providerRevoked: 1 means Connect sent a valid token to the provider's revocation endpoint and it was accepted, so a live token existed while token requests were being refused.
Ruled out
- Client secret: one early attempt had a mistyped secret; Connect correctly surfaced "OAuth token request failed: Client authentication failed". Fixed via
PATCH /v1/connect/connectors/:id(data.clientSecret). Failures above are with the correct secret. - Provider: the same client id/secret driven directly (standard authorization-code flow) works end to end: exchange 200, refresh token issued, all scopes,
userinfo200. - Request Connect sends the provider (captured in browser): exactly the requested scopes,
redirect_uri https://connect.vercel.com/callback, no PKCE, noopenid. Provider accepts it. - Caller auth: same OIDC token succeeds for
startAuthorizationand for the revoke call.
Questions
- Under what conditions does
/v1/connect/tokenreturnuser_authorization_requiredfor a subject that has acompleted_authorization_requestand a revocable provider token? - Does
completed_authorization_requestmean the provider token exchange succeeded, or only that the user finished consent?