Skip to content

docs: record the bound_audiences requirement for JWT auth - #174

Merged
kurok merged 1 commit into
masterfrom
docs/jwt-bound-audiences
Sep 1, 2026
Merged

docs: record the bound_audiences requirement for JWT auth#174
kurok merged 1 commit into
masterfrom
docs/jwt-bound-audiences

Conversation

@kurok

@kurok kurok commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Checked our JWT implementation against Vault's JWT auth docs and the API reference.

The client conforms

Doc Us
POST auth/jwt/login POST /auth/${mount}/login, default mount jwt
body: jwt required, role optional sends { jwt }, adds role only when set
"Defaults to configured default_role if not provided" omits role entirely when unset
jwt flow is the simple one, oidc needs a browser only jwt, oidc explicitly out of scope

The two HashiCorp pages disagree on role: the concept page calls it required, the API reference calls it optional with a default_role fallback. We follow the API reference, which is normative — and that path is already covered by a unit test, a demo scenario and the README.

No code change here.

What was missing — on our side

Vault requires a jwt role to bind the audience its tokens carry, and does not catch the omission when the role is created. Reproduced against a live Vault:

role with NO bound constraint        -> 400 at create: "must have at least one bound constraint
                                        when creating/updating a role"          (self-correcting)

role with bound_subject, no aud bind -> created (204)                            (NOT caught)
  └─ login, JWT carrying `aud`       -> 400: "audience claim found in JWT but no audiences
                                        bound to the role"

So the naive mistake is blocked, but the realistic one is not.

Our own GitHub Actions example walked straight into it. It uses core.getIDToken('vault'), which mints aud: vault, so the Vault role must bind that audience — and the README never said so. Anyone copying that snippet and creating a role without bound_audiences=vault got a 400 whose wording doesn't obviously point at their role config.

Change

  • New subsection in JWT auth: the requirement, both error strings verbatim so they are searchable, a vault write .../role/... example that binds the audience, and the note that an aud array matches on any entry.
  • GitHub Actions section now states the audience core.getIDToken('vault') mints, with a matching role example, and warns that calling getIDToken() with no argument uses GitHub's default audience instead.
  • Recorded that Vault does not require an exp claim — a token minted without one is accepted and never expires, which matters for anyone writing their own jwtProvider.

Verification

Every quoted string was taken from a live Vault, not from the docs, and checked on both supported lines:

1.21 2.0.4
must have at least one bound constraint when creating/updating a role
audience claim found in JWT but no audiences bound to the role
error validating token: invalid audience (aud) claim: audience claim does not match any expected audience

Identical on both, so the docs don't need version qualifiers.

I deliberately did not add a test asserting these strings. It would be in this repo's style, but it couples CI to third-party error prose and would fail on a Vault reword that is not our bug — a false alarm rather than a caught defect. The strings are stable across both supported lines today, and the CHANGELOG records where they came from.

lint clean, 391 unit passing, and both README-drift guards still green (#162's error table, #164's public-API coverage).

Checked the implementation against Vault's JWT auth documentation. The client
conforms -- the login endpoint, body, and the optional `role` with its
`default_role` fallback all match the API reference (the concept page calls
`role` required, the API reference does not; the API reference is normative and
we already test the default_role path). What was missing was on our side of the
docs.

Vault requires a `jwt` role to bind the audience its tokens carry, and does not
catch the omission at role-creation time: a role with any other bound constraint
is accepted, and the failure appears only at login. Reproduced against a live
Vault:

  role with no bound constraint      -> 400 at create, "must have at least one
                                        bound constraint when creating/updating a role"
  role with bound_subject, no aud    -> created (204)
    login with a JWT carrying aud    -> 400 "audience claim found in JWT but no
                                        audiences bound to the role"

Our own GitHub Actions example walked into this: it uses
core.getIDToken('vault'), which mints aud: vault, so the role must bind that
audience, and the README never said so. Anyone copying the snippet and creating
a role without it got a 400 whose wording does not obviously point at the role.

The JWT section now carries the requirement, both error strings verbatim so they
are searchable, and a role-creation example; the GitHub Actions section spells
out the audience it mints. Also recorded that Vault does not require an `exp`
claim -- a token minted without one is accepted and never expires -- which
matters for anyone writing their own jwtProvider.

Every quoted string was taken from a live Vault and verified identical on 1.21
and 2.0. No code change; the client already surfaces these as VaultHttpError
with Vault's own message intact.

Signed-off-by: kurok <22548029+kurok@users.noreply.github.com>
@kurok
kurok requested review from m2broth and wRLSS as code owners September 1, 2026 18:53
@kurok
kurok merged commit 9094f2d into master Sep 1, 2026
22 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.

1 participant