Summary
Vercel Connect (getToken / getTokenResponse) keeps returning a Workday access token that Workday rejects with 401, while Connect still succeeds (no needs_auth / no_token / invalid_grant). Connect advances expiresAt ~15 minutes into the future but the access-token fingerprint does not change, which looks like Connect is renewing its own lease of a stale bearer instead of exchanging the stored Workday refresh token.
We are not using Non-Expiring refresh tokens. Workday Refresh Token Timeout is 30 days.
Upgrading @vercel/connect to latest (0.4.3) did not fix this.
I can share connector UID + subject + fingerprints (and related timestamps) privately with Vercel support.
Questions for Connect / Vercel
- For a user grant on a Workday Custom OAuth connector, does Connect store the Workday refresh token and call Workday’s token endpoint with
grant_type=refresh_tokenwhen the access token is expired/near expiry? - Can someone check Connect logs (with privately shared connector UID + subject + fingerprints/timestamps) and confirm whether a refresh was attempted and what Workday returned?
- If refresh failed, why does
getTokenstill return a bearer instead ofuser_authorization_required/no_token? - Is there a known issue with Workday + rotating 30-day refresh tokens (Non-Expiring off) where Connect reuses a dead access token?
Environment
- App: Next.js on Vercel
- Sandbox deployment of our staff hub
- SDK:
@vercel/connect@0.4.3(also reproduced on0.2.7) - Connector: Workday Custom OAuth (sandbox /
impl.workday.com) - Subject: Connect
{ type: "user", id: <work-email> }(lowercased) - Scopes sent:
System,Time Off and Leave - REST audience/base: Workday tenant REST
/ccx/api/v1/<tenant>(details privately)
Connect connector config
- User Authorization: ON
- Refresh Tokens: ON
- Token Auth Method:
client_secret_basic - PKCE Required: OFF (matches Workday client)
- User Authorization Scopes: empty
- Default Audience: empty
Workday API client
- Auth Code Grant
- Refresh Token Timeout: 30 days
- Non-Expiring Refresh Tokens: OFF
Our app never stores the Workday refresh token. We only call Connect for access tokens.
Reproduction / evidence
Debug endpoint returns token metadata + Workday userInfo probe results.
Fingerprint = first 16 hex chars of SHA-256(access_token) (we do not log raw tokens).
Observed pattern (UTC; exact fingerprint/subject privately):
| Relative time | Fingerprint | Connect expiresAt | Workday userInfo |
|---|---|---|---|
| T+0 (post-reauth) | same | ~15 min ahead | 200 |
| T+16 min | same | ~15 min ahead | 200 |
| T+65 min | same | ~15 min ahead | 200 |
| T+~10 hr | same | ~15 min ahead | 401 Invalid access token |
Same fingerprint overnight; Connect never surfaces refresh failure.
Failure shape (redacted):
{ "tokenMetadata": { "fingerprint": "", "expiresAtIso": "<~15 min ahead>", "expiresInMs": 899954, "isExpired": false }, "probes": [{ "url": "/userInfo", "status": 401, "bodyPreview": "{\"error\":\"Invalid access token.\"}" }]}