Problem
When the credential service issues a token without an explicit audience, aud falls back to the issuer URL — so by default aud == iss. That is a present-but-nominal placeholder, not a real audience. Per RFC 7519 §4.1.3, aud identifies the recipients the token is intended for (the target resource server), not the party that minted it (that is iss).
// internal/service/credential.go:375-382
// JWT-SVID §3 requires `aud` to be present on every issued token. Default
// to the issuer URL when no audience was supplied so tokens remain
// interoperable with spec-compliant verifiers (e.g., pkg/authjwt).
aud := req.Audience
if len(aud) == 0 {
aud = []string{s.issuer}
}
_ = token.Set(jwt.AudienceKey, aud)
The root cause is that the OAuth token endpoint gives a caller no way to name the target resource. TokenInput.Body (internal/handler/oauth.go:26-65) exposes no resource or audience parameter on any grant, including token-exchange (RFC 8693). So at mint time the AS does not know the intended RS, and the JWT-SVID §3 "aud MUST be present" rule forces a non-empty fallback — the issuer.
Impact
- Semantically wrong: the token effectively says "intended for the issuer," which is never the real relying party.
- Latent rejection risk: a strict resource server that validates
aud == <itself> per RFC 7519 §4.1.3 would reject a aud = issuer token. Today these tokens validate only because audience checking is optional — pkg/authjwt enforces aud exclusively when a deployment configures an expected Audience (pkg/authjwt/verifier.go:58-60, 163). In effect the tokens lean on lax or issuer-configured verifiers.
- It makes the single-audience direction we discussed on the WIMSE thread nominal rather than meaningful — there is no real audience to scope down to.
Proposed change
Adopt RFC 8707 — Resource Indicators for OAuth 2.0:
- Accept a
resource parameter (and optionally audience) on /oauth2/token and the token-exchange grant.
- Validate it against an allowed-resource list (per tenant/client).
- Set
aud to the requested resource(s) instead of the issuer.
- Decide the no-
resource behaviour explicitly — reject, or fall back to a configured default — rather than silently substituting the issuer.
Scope this to the OAuth access-token profile; JWT-SVID / WIMSE assertion paths keep their own audience semantics.
References
Problem
When the credential service issues a token without an explicit audience,
audfalls back to the issuer URL — so by defaultaud == iss. That is a present-but-nominal placeholder, not a real audience. Per RFC 7519 §4.1.3,audidentifies the recipients the token is intended for (the target resource server), not the party that minted it (that isiss).The root cause is that the OAuth token endpoint gives a caller no way to name the target resource.
TokenInput.Body(internal/handler/oauth.go:26-65) exposes noresourceoraudienceparameter on any grant, including token-exchange (RFC 8693). So at mint time the AS does not know the intended RS, and the JWT-SVID §3 "audMUST be present" rule forces a non-empty fallback — the issuer.Impact
aud == <itself>per RFC 7519 §4.1.3 would reject aaud = issuertoken. Today these tokens validate only because audience checking is optional —pkg/authjwtenforcesaudexclusively when a deployment configures an expectedAudience(pkg/authjwt/verifier.go:58-60, 163). In effect the tokens lean on lax or issuer-configured verifiers.Proposed change
Adopt RFC 8707 — Resource Indicators for OAuth 2.0:
resourceparameter (and optionallyaudience) on/oauth2/tokenand the token-exchange grant.audto the requested resource(s) instead of the issuer.resourcebehaviour explicitly — reject, or fall back to a configured default — rather than silently substituting the issuer.Scope this to the OAuth access-token profile; JWT-SVID / WIMSE assertion paths keep their own audience semantics.
References
audaudonat+jwtaccess tokens (relates to OAuth access tokens: use RFC 9068at+jwttyp header (config-overridable) #189)