Problem
aud is modeled as a Go slice (domain/token.go:14 — Audience []string) and is always set from a []string (internal/service/credential.go:382). lestrrat-go/jwx serializes a slice as a JSON array regardless of length, so even a single-audience access token emits the array form:
"aud": ["https://rs.example.com"]
rather than the bare string "aud": "https://rs.example.com".
RFC 7519 §4.1.3 permits aud to be either a single case-sensitive string or an array. For the OAuth access-token profile a single string is the safer default: a multi-valued aud is a bearer pattern that enables lateral movement — a token valid for both RS-A and RS-B can be replayed from one to the other by whoever holds it. Sender-constraining via DPoP (RFC 9449) mitigates replay, but a sender-constrained token and a multi-audience token do not compose cleanly, so single-audience is the right default for the AT profile.
Proposed change
- For the OAuth access-token profile, serialize
aud as a single string when there is exactly one value.
- Keep array semantics where SPIFFE JWT-SVID consumers expect them (JWT-SVID requires a non-empty array).
- Treat multi-audience as an explicit, policy-gated opt-in rather than the default path.
- No verify-side change needed — jwx handles the polymorphic (string-or-array)
aud transparently on parse.
Relationship to other issues
References
- RFC 7519 §4.1.3 —
aud is a single string OR an array
- RFC 8725 (JWT BCP) / RFC 9700 (OAuth 2.0 Security BCP) — audience-restriction guidance
- Reported by Andrii Deinega via WIMSE WG review of ZeroID.
Problem
audis modeled as a Go slice (domain/token.go:14—Audience []string) and is always set from a[]string(internal/service/credential.go:382). lestrrat-go/jwx serializes a slice as a JSON array regardless of length, so even a single-audience access token emits the array form:rather than the bare string
"aud": "https://rs.example.com".RFC 7519 §4.1.3 permits
audto be either a single case-sensitive string or an array. For the OAuth access-token profile a single string is the safer default: a multi-valuedaudis a bearer pattern that enables lateral movement — a token valid for both RS-A and RS-B can be replayed from one to the other by whoever holds it. Sender-constraining via DPoP (RFC 9449) mitigates replay, but a sender-constrained token and a multi-audience token do not compose cleanly, so single-audience is the right default for the AT profile.Proposed change
audas a single string when there is exactly one value.audtransparently on parse.Relationship to other issues
audnaturally; this issue is best sequenced after OAuth access tokens: aud defaults to the issuer — adopt RFC 8707 resource indicators so it names the target resource server #199.at+jwttyp header (config-overridable) #189 (theat+jwtprofile move). Same access-token profile, complementary change — OAuth access tokens: use RFC 9068at+jwttyp header (config-overridable) #189 covers thetypheader only, this coversaudserialization.References
audis a single string OR an array