This article was originally published on Jo4 Blog.
Our OAuth implementation worked perfectly. Every test passed. Users authorized apps, got tokens, refreshed them. Textbook OAuth 2.0.
Then a Pipedream integration broke.
The Problem
A user reported that their Pipedream workflow couldn't access certain API endpoints. The token was valid, the scopes were granted — but the API returned 403 Forbidden.
The error logs showed the token had zero scopes. That's impossible — we confirmed the user authorized read:urls write:urls during the consent flow.
The Root Cause
OAuth 2.0 (RFC 6749) defines scopes as space-delimited:
scope = "read:urls write:urls"
But some OAuth clients send them comma-delimited:
scope = "read:urls,write:urls"
Our scope parser split on spaces. Pipedream sent commas. The parser saw "read:urls,write:urls" as a single unknown scope, which mapped to zero valid scopes.
One character. Comma vs space.
// Before: only splits on space
String[] scopes = scopeString.split(" ");
// After: splits on comma OR space
String[] scopes = scopeString.split("[,\\s]+");
That's the fix. One regex character class. The rest of this post is about making sure it never happens again.
The Test Suite
We wrote a full end-to-end OAuth integration test: 1,605 lines covering the complete flow.
The test covers:
- Authorization code request — with various scope formats
- Token exchange — authorization code → access token
- Token refresh — refresh token → new access token
- Scope validation — comma-delimited, space-delimited, mixed, duplicates
- Error cases — invalid codes, expired tokens, revoked grants
- Real API calls — using the token against actual protected endpoints
The scope parsing tests specifically:
// Space-delimited (RFC 6749 standard)
assertScopeParsed("read:urls write:urls", Set.of("read:urls", "write:urls"));
// Comma-delimited (common in practice)
assertScopeParsed("read:urls,write:urls", Set.of("read:urls", "write:urls"));
// Mixed (yes, this happens)
assertScopeParsed("read:urls, write:urls", Set.of("read:urls", "write:urls"));
// Duplicates
assertScopeParsed("read:urls read:urls", Set.of("read:urls"));
Lessons Learned
- RFCs are prescriptive, clients are creative — the spec says space-delimited, but real clients do whatever they want. Parse generously.
- E2E tests catch what unit tests miss — our unit tests for scope parsing passed because they all used space-delimited scopes. The integration path through the actual OAuth flow with a real client exposed the mismatch.
-
One-character bugs hide in plain sight — the scope string looked correct in logs. You had to know that
read:urls,write:urlswas one scope, not two, to spot the problem. - Test the integration, not the unit — for auth flows especially, the value is in testing the full chain: consent → code → token → API call. Mocking any part of that chain hides bugs like this one.
Ever been bitten by a delimiter bug? What's the smallest change that broke your production? Drop it in the comments.
Building jo4.io — a URL shortener with a developer API that actually works.
Top comments (0)