Skip to content

Accept a JWT audience sent as a string, not only an array - #16

Merged
northernpolaris merged 4 commits into
mainfrom
fix/access-aud-string-or-array
Aug 28, 2026
Merged

northernpolaris merged 4 commits into
mainfrom
fix/access-aud-string-or-array

Conversation

@aendra-rininsland

@aendra-rininsland aendra-rininsland commented Aug 27, 2026 •

Copy link
Copy Markdown
Contributor

Follow-up to #14. The endpoint it added has never successfully authenticated a real request — this is why.

What happens

AccessClaims types aud as Vec<String>. RFC 7519 §4.1.3 allows aud to be either a single string or an array of strings, and Cloudflare Access uses the string form when a token is scoped to one application — the ordinary case. So the claims fail to deserialize against every real token:

Auth Error: Invalid Access token: JSON error: invalid type: string
"<aud tag>", expected a sequence at line 1 column 181

Why it was hard to read

The failure happens while parsing the claims, inside the same decode call that verifies the signature — so it surfaces as "Invalid Access token". That reads like a rejected credential, and sent the first caller checking service tokens, Access policies and CF_ACCESS_ALLOWED_SERVICE_TOKENS, all of which were correct. Access had authenticated the request and handed over a valid assertion; this code then declined to parse it.

The fix

aud deserializes from one-or-many into a list. Nothing downstream changes, and the security property is untouched: the audience is checked by Validation::set_audience, which handles both shapes itself. This field only ever needed to deserialize.

A number or an object is still rejected — tolerating the two shapes the spec allows is not the same as tolerating anything.

Tests

Three: string form, array form, and neither. Placeholder audience values throughout. 48 unit + 3 integration passing, clippy and fmt clean.

aendra-rininsland and others added 4 commits August 27, 2026 22:56
AccessClaims typed `aud` as Vec<String>. RFC 7519 §4.1.3 allows either a
single string or an array, and Cloudflare Access uses the string form
when a token is scoped to one application — which is the ordinary case.
So the claims failed to deserialize against every real token.

The failure was badly disguised. It happened while parsing the claims,
inside the same `decode` call that verifies the signature, so it
surfaced as "Invalid Access token: JSON error: invalid type: string
… expected a sequence". That reads like a rejected credential, and sent
the first caller looking at service tokens and Access policies — all of
which were correct. Access had authenticated the request and handed us a
valid assertion; this code then declined to parse it.

The audience is still checked by Validation::set_audience, which handles
both shapes itself. This field only needs to deserialize, so it now
normalises one-or-many to a list and nothing downstream changes. A
number or an object is still rejected: tolerating the two shapes the
spec allows is not the same as tolerating anything.

Found by the first real request through Access, from the vetting
Worker's service token.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The value was a real Access AUD tag from the deployment that surfaced
this bug. It is not a credential — it identifies an Access application
and cannot mint, sign or forge anything — but it is deployment
configuration and this repository is public, so it has no business
being here when any 64-character string proves the same point.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
.vscode/settings.json held only Peacock colour customisations — a
per-person choice about what tint one editor window has. Committing it
pushes one contributor's theme onto everyone who opens the repository,
and the file churns whenever Peacock runs.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The field was documented as carrying "the token's name". Cloudflare sets
it to the service token's Client ID (CF-Access-Client-Id), and
CF_ACCESS_ALLOWED_SERVICE_TOKENS is matched against it — so an operator
following the comment would fill the allowlist with dashboard names that
can never match, and the rejection would name a token that looks
entirely correct.

Nothing changes at runtime; the code was already comparing against the
right value. Only the comments were wrong, which is the kind of wrong
that surfaces as a misconfigured deployment rather than a failing test.

Also records why the audience fix accepts both shapes: Cloudflare's own
documentation shows `aud` as an array, and the deployment that hit this
sent a string. The RFC permits either, so the parser takes either rather
than trusting one source over the other.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@northernpolaris
northernpolaris merged commit 180e6cf into main Aug 28, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants