Skip to content

feat: add jwtDirect filter for JWT validation with direct JWKS URL#3922

Draft
ivan-digital wants to merge 3 commits intozalando:masterfrom
ivan-digital:feature/jwt-direct-filter
Draft

feat: add jwtDirect filter for JWT validation with direct JWKS URL#3922
ivan-digital wants to merge 3 commits intozalando:masterfrom
ivan-digital:feature/jwt-direct-filter

Conversation

@ivan-digital
Copy link

Summary

  • Add new jwtDirect filter that verifies JWT Bearer tokens using a JWKS URL directly, without requiring OIDC discovery via .well-known/openid-configuration
  • Supports arbitrary claim key-value validation (all must match)
  • Reuses existing keyfunc library and JWKS caching infrastructure
  • Stores validated claims in StateBag for downstream filter consumption

Motivation

The existing jwtValidation filter only supports JWKS discovery via OIDC .well-known/openid-configuration. Services like Google Chat bots publish JWKS keys at non-standard URLs and require iss/aud claim validation, which is currently not possible.

Usage

jwtDirect(
    "https://www.googleapis.com/service_accounts/v1/jwk/chat@system.gserviceaccount.com",
    "iss", "chat@system.gserviceaccount.com",
    "aud", "123456789"
)
  • First argument: JWKS URL (fetched and cached)
  • Remaining arguments: claim key-value pairs that must all match
  • Returns 401 on missing/invalid token or failed claim validation

Closes #3921

Test plan

  • Unit tests for spec validation (missing args, odd claim args, non-string args)
  • Integration tests with real JWKS server: valid tokens, claim matching, wrong issuer, wrong audience, missing claims, expired tokens
  • Tests for missing/empty/malformed Bearer tokens

Add a new jwtDirect filter that verifies JWT Bearer tokens using a
JWKS URL directly, without requiring OIDC discovery. Supports
arbitrary claim key-value validation (e.g. iss, aud).

This enables use cases like Google Chat webhook signature verification
where the JWKS endpoint is not behind a standard .well-known/openid-configuration.

Closes zalando#3921

Signed-off-by: ivan-digital <root@ivan.digital>
@ivan-digital ivan-digital force-pushed the feature/jwt-direct-filter branch from 688f555 to 9eb5971 Compare March 15, 2026 07:56
}, nil
}

func (f *jwtDirectFilter) Request(ctx filters.FilterContext) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi. Existing jwtValidation filter only validates token and delegates claims check to oidcClaimsQuery.
I think it makes sense to do the same here, moreover Request logic seems to be exactly the same and therefore could be reused - you can simply return an instance of jwtValidationFilter from the spec.
As for the name I'd suggest jwtValidationKeys as "direct" is a bit obscure.
Please also add docs to filters.md nearby jwtValidation. Thanks!

(PS: I am not actively involved in Skipper development anymore but I recently saw your great work on ASR/TTS so decided to chime in 👋)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the review and the valuable simplifications! Addressed all points: renamed to jwtValidationKeys, reusing jwtValidationFilter from the spec, moved registration next to jwtValidation in skipper.go, and added docs to filters.md. Claims validation delegated to oidcClaimsQuery as suggested. Thanks for the attention :)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@AlexanderYastrebov it's always a great contribution if you review code, much appreciated :)

accesslog.NewEnableAccessLog(),
auth.NewForwardToken(),
auth.NewForwardTokenField(),
auth.NewJwtDirect(),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Register it nearby existing jwtValidation filter instead of builtin.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done, moved to skipper.go next to jwtValidation.

Add a new jwtValidationKeys filter that verifies JWT Bearer tokens
using a JWKS URL directly, without requiring OIDC discovery. Reuses
jwtValidationFilter for token parsing and validation logic. Claims
checking is delegated to oidcClaimsQuery as per existing convention.

Renamed from jwtDirect to jwtValidationKeys for clarity. Registered
alongside jwtValidation in skipper.go instead of builtin. Added docs
to filters.md.

Closes zalando#3921

Signed-off-by: ivan-digital <root@ivan.digital>
Use correct @_ modifier and == operator for exact claim matching.
Chain separate oidcClaimsQuery filters for AND logic since queries
within a single argument are OR-matched.

Signed-off-by: ivan-digital <root@ivan.digital>
@szuecs
Copy link
Member

szuecs commented Mar 16, 2026

code looks fine I would move it to the same file as the implementation that you borrow from or rename to filter name

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add JWT verification filter with direct JWKS URL and claims validation

3 participants