Found while verifying #198 end to end against the committed local Keycloak (docker/keycloak/).
With HFS_AUTH_AUDIENCE=hfs-api configured, a token that carries no aud claim at all is accepted:
# Keycloak client_credentials token (the committed realm has no audience mapper -> no aud claim)
GET /Patient (Bearer <token without aud>) -> 200 # expected: 401 Invalid audience
Root cause: crates/auth/src/provider/jwks_bearer.rs calls validation.set_audience(&[aud]), but jsonwebtoken's default required_spec_claims is {"exp"} — audience (and issuer) validation only runs when the claim is present. A token missing aud bypasses the check entirely, which defeats the purpose of audience restriction (cross-service token reuse is exactly the attack aud exists to stop, and an attacker-friendly IdP config is one with no audience mapper).
Fix sketch: when expected_audience is configured, add "aud" to validation.required_spec_claims (and similarly "iss" when expected_issuer is set, for defense in depth — HFS already refuses to boot without an issuer, but the required-claim flag is what makes a missing iss fail).
Also worth doing alongside: the committed realm (docker/keycloak/realm.json) should gain a hardcoded-audience protocol mapper so the local setup can exercise audience validation at all — tracked in the #198 write-up.
Found while verifying #198 end to end against the committed local Keycloak (docker/keycloak/).
With
HFS_AUTH_AUDIENCE=hfs-apiconfigured, a token that carries noaudclaim at all is accepted:Root cause:
crates/auth/src/provider/jwks_bearer.rscallsvalidation.set_audience(&[aud]), butjsonwebtoken's defaultrequired_spec_claimsis{"exp"}— audience (and issuer) validation only runs when the claim is present. A token missingaudbypasses the check entirely, which defeats the purpose of audience restriction (cross-service token reuse is exactly the attackaudexists to stop, and an attacker-friendly IdP config is one with no audience mapper).Fix sketch: when
expected_audienceis configured, add"aud"tovalidation.required_spec_claims(and similarly"iss"whenexpected_issueris set, for defense in depth — HFS already refuses to boot without an issuer, but the required-claim flag is what makes a missingissfail).Also worth doing alongside: the committed realm (
docker/keycloak/realm.json) should gain a hardcoded-audience protocol mapper so the local setup can exercise audience validation at all — tracked in the #198 write-up.