Skip to content

Conversation

dotansimha
Copy link
Member

@dotansimha dotansimha commented Sep 27, 2025

Closes #314
Closes #457
Fixes #458
Closes #452 (because it's using it)

Overview

This PR introduces a JWT authentication flow. The following are supported:

  • Multiple JWKS that can be loaded from a local file or remote JWKS set
  • JWT token lookup based on HTTP header (with prefix validation), or HTTP Cookie (default: Authorization: Bearer XYZ)
  • Issuer/audeince validation
  • Multiple algorithm support
  • Expiration validation
  • Enforce token validation (default: false)
  • Error handling and standard rejection in case of error

In addition, this PR have some required changes and bug fixes:

  • Easy setup to register and run async background tasks
  • Config file will now automatically resolve paths mentioned (supergraph, jwks file) relative to the config file path.
  • Added e2e tests setup and a testkit and allow us to simplify the testing of different features in e2e testing that's based on a config file + gql operation
  • Added a mechanism to store and modify per-request context (using req.extensions)

TODO

  • figure out background tasks
  • figure out per-request context
  • jwks fetching
  • prefetching jwks
  • decode jwt
  • jwt validation
  • error handling
  • context injection of token and payloads
  • forwarding
  • e2e testing

Copy link

github-actions bot commented Sep 29, 2025

k6-benchmark results

     ✓ response code was 200
     ✓ no graphql errors
     ✓ valid response structure

     █ setup

     checks.........................: 100.00% ✓ 211080      ✗ 0    
     data_received..................: 6.2 GB  205 MB/s
     data_sent......................: 83 MB   2.7 MB/s
     http_req_blocked...............: avg=4.17µs   min=681ns   med=1.9µs   max=4.73ms   p(90)=2.91µs  p(95)=3.41µs  
     http_req_connecting............: avg=1.27µs   min=0s      med=0s      max=3.83ms   p(90)=0s      p(95)=0s      
     http_req_duration..............: avg=20.82ms  min=2.03ms  med=19.85ms max=111.2ms  p(90)=28.72ms p(95)=32.08ms 
       { expected_response:true }...: avg=20.82ms  min=2.03ms  med=19.85ms max=111.2ms  p(90)=28.72ms p(95)=32.08ms 
     http_req_failed................: 0.00%   ✓ 0           ✗ 70380
     http_req_receiving.............: avg=157.03µs min=25.83µs med=42.64µs max=68.38ms  p(90)=103.3µs p(95)=446.43µs
     http_req_sending...............: avg=26.04µs  min=5.83µs  med=11.34µs max=23.12ms  p(90)=18.84µs p(95)=30.87µs 
     http_req_tls_handshaking.......: avg=0s       min=0s      med=0s      max=0s       p(90)=0s      p(95)=0s      
     http_req_waiting...............: avg=20.64ms  min=1.94ms  med=19.71ms max=69.93ms  p(90)=28.44ms p(95)=31.75ms 
     http_reqs......................: 70380   2341.05654/s
     iteration_duration.............: avg=21.31ms  min=6.32ms  med=20.22ms max=203.12ms p(90)=29.21ms p(95)=32.62ms 
     iterations.....................: 70360   2340.391278/s
     vus............................: 50      min=50        max=50 
     vus_max........................: 50      min=50        max=50 

@dotansimha dotansimha marked this pull request as ready for review September 29, 2025 09:55
Copy link

github-actions bot commented Sep 29, 2025

🐋 This PR was built and pushed to the following Docker images:

Image Names: ghcr.io/graphql-hive/router

Platforms: linux/amd64,linux/arm64

Image Tags: ghcr.io/graphql-hive/router:pr-455 ghcr.io/graphql-hive/router:sha-86e34fd

Docker metadata
{
"buildx.build.ref": "builder-e23c6151-fc29-44af-9aa4-20c7804c7e0b/builder-e23c6151-fc29-44af-9aa4-20c7804c7e0b0/wrvopepresvmohpp4wng0vsef",
"containerimage.descriptor": {
  "mediaType": "application/vnd.oci.image.index.v1+json",
  "digest": "sha256:1bb42e81947f2f4a1aec0f8ecaf64ad7e865605710bcc0611f879060a77491d5",
  "size": 1609
},
"containerimage.digest": "sha256:1bb42e81947f2f4a1aec0f8ecaf64ad7e865605710bcc0611f879060a77491d5",
"image.name": "ghcr.io/graphql-hive/router:pr-455,ghcr.io/graphql-hive/router:sha-86e34fd"
}

feat(router): added a mechanism to store per-request context using req.extensions
feat(router): jwt authentication config
feat(config): added jwt config
fix(config): load references files relative to the config directory, or current if not specified
chore: added e2e test setup and testkit
}

if self.config.forward_claims_to_upstream_header.enabled {
jwt_ctx.payload = Some((
Copy link
Member

@ardatan ardatan Oct 12, 2025

Choose a reason for hiding this comment

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

Does it really make sense to forward the payload with headers? Couldn't it be better to forward it through extensions as in Hive Gateway?
https://the-guild.dev/graphql/hive/docs/gateway/authorization-authentication#in-upstream-graphql-subgraphs

Copy link
Member Author

Choose a reason for hiding this comment

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

I wasn't sure what would be the best here. Other HTTP servers like Envoy pass it as headers (forward_payload_header). But there it's also base64. https://www.envoyproxy.io/docs/envoy/latest/api-v3/extensions/filters/http/jwt_authn/v3/config.proto
I'm not sure if it should be in extensions as JSON object, or as headers (raw? base64?)

Copy link
Member

@ardatan ardatan Oct 12, 2025

Choose a reason for hiding this comment

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

extensions is GraphQL-native way to do this independent from the transport and payload format. Headers can be bloated with the payload if it is big.

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

Labels

None yet

Projects

None yet

2 participants