The observation
The feature server's OIDC token parser verifies the token signature (JWKS via discovery) and expiry, but audience verification is explicitly off and there is no issuer check (sdk/python/feast/permissions/auth/oidc_token_parser.py, _decode_token):
audience="account",
options={
"verify_aud": False,
"verify_signature": True,
"verify_exp": True,
},
The consequence is that any validly-signed, unexpired token from the configured IdP authenticates against the feature server, regardless of which resource it was minted for. In a multi-app tenant, a token issued to a completely different application passes authentication, and role matching in RoleBasedPolicy is the only remaining gate. Authorization ends up doing all of the work; authentication is only proving the token came from the right IdP, not that it was meant for Feast.
Why the current default is reasonable
This looks deliberate, and changing the default would break real deployments. Identity providers commonly put claim values in tokens that differ from their discovery metadata. The clearest case is Microsoft Entra ID, which issues v1.0 tokens (iss: https://sts.windows.net/<tenant-id>/, aud: api://<app-id-uri>) that are routinely validated against a v2.0 discovery URL. That works today precisely because discovery is only used to source JWKS keys and no aud/iss comparison runs. #6631 added Entra support on top of that behavior, so this is proposing configurability, not calling the default a bug.
Proposal
Two optional OidcAuthConfig fields, audience and issuer, both unset by default:
- When set, the corresponding token claim must match or the token is rejected at authentication.
- When unset, the decode options are identical to today's, so nobody is affected without opting in.
- Operators state the expected values explicitly rather than the code inferring them from the discovery document, which is what keeps the v1-token-against-v2-discovery setup working.
The accompanying PR includes a test that pins the v1/v2 Entra setup so a future refactor cannot silently regress it.
One scope note: the feast-operator copies a whitelist of OIDC secret keys into the generated feature_store.yaml (OidcOptionalSecretProperties), so exposing these fields through the operator CRD would be a small follow-up if there is interest. The PR keeps to the Python SDK and docs.
The observation
The feature server's OIDC token parser verifies the token signature (JWKS via discovery) and expiry, but audience verification is explicitly off and there is no issuer check (
sdk/python/feast/permissions/auth/oidc_token_parser.py,_decode_token):The consequence is that any validly-signed, unexpired token from the configured IdP authenticates against the feature server, regardless of which resource it was minted for. In a multi-app tenant, a token issued to a completely different application passes authentication, and role matching in
RoleBasedPolicyis the only remaining gate. Authorization ends up doing all of the work; authentication is only proving the token came from the right IdP, not that it was meant for Feast.Why the current default is reasonable
This looks deliberate, and changing the default would break real deployments. Identity providers commonly put claim values in tokens that differ from their discovery metadata. The clearest case is Microsoft Entra ID, which issues v1.0 tokens (
iss: https://sts.windows.net/<tenant-id>/,aud: api://<app-id-uri>) that are routinely validated against a v2.0 discovery URL. That works today precisely because discovery is only used to source JWKS keys and noaud/isscomparison runs. #6631 added Entra support on top of that behavior, so this is proposing configurability, not calling the default a bug.Proposal
Two optional
OidcAuthConfigfields,audienceandissuer, both unset by default:The accompanying PR includes a test that pins the v1/v2 Entra setup so a future refactor cannot silently regress it.
One scope note: the feast-operator copies a whitelist of OIDC secret keys into the generated
feature_store.yaml(OidcOptionalSecretProperties), so exposing these fields through the operator CRD would be a small follow-up if there is interest. The PR keeps to the Python SDK and docs.