DEV Community

Anand Rathnas
Anand Rathnas

Posted on • Originally published at jo4.io

The One-Character OAuth Bug That Broke Our API

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"
Enter fullscreen mode Exit fullscreen mode

But some OAuth clients send them comma-delimited:

scope = "read:urls,write:urls"
Enter fullscreen mode Exit fullscreen mode

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]+");
Enter fullscreen mode Exit fullscreen mode

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:

  1. Authorization code request — with various scope formats
  2. Token exchange — authorization code → access token
  3. Token refresh — refresh token → new access token
  4. Scope validation — comma-delimited, space-delimited, mixed, duplicates
  5. Error cases — invalid codes, expired tokens, revoked grants
  6. 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"));
Enter fullscreen mode Exit fullscreen mode

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:urls was 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)