Skip to content

Support JWT claims in EDFS subscription subject templates ({{ claims.* }} alongside {{ args.* }}) #2545

@vasily-polonsky

Description

@vasily-polonsky

Component(s)

router

Is your feature request related to a problem? Please describe.

Summary

Allow EDFS @edfs__natsSubscribe (and other @edfs__*Subscribe) subject templates to reference JWT claims — e.g. {{ claims.sub }} or {{ claims.userId }} — in addition to the existing {{ args.* }} syntax.

Problem

A very common subscription pattern is "subscribe to events for the currently authenticated user":

# Current approach — client must pass their own ID as an argument
type Subscription {
  messageReceived(myUserId: UUID!): Message!
    @edfs__natsSubscribe(
      subjects: ["edfs.messages.{{ args.myUserId }}"]
    )
}

This forces every client to explicitly provide its own user ID as a subscription argument. In practice, the server already knows who the user is from the JWT token. Exposing this as a required argument creates several issues:

1. Security concern

The argument is untrusted by default. A client could subscribe with someone else's ID. To prevent this, you must write a custom Go module that compares args.myUserId against the JWT claim and rejects mismatches. This is arguably the most common authorization check for per-user subscriptions, yet it requires a custom module to implement:

// custom Go module — runs on every subscription start
if args.MyUserId != claims.Sub {
    return errors.New("unauthorized")
}

2. Go knowledge and custom module overhead

Not every team has Go expertise. Writing, compiling, and maintaining a custom router module is a significant barrier for teams using Node.js/TypeScript/Java backends.

Beyond writing the code, maintaining a custom module introduces ongoing friction:

  • Building the router — every deployment requires compiling a custom Go binary instead of using the official Docker image
  • Version bumps — upgrading the router is no longer just pulling a new image. You must ensure your custom module compiles against the new router version
  • CI/CD complexity — the build pipeline needs Go toolchain, module dependencies, and a custom Dockerfile
  • Risk of errors — a misconfigured or broken module can silently break subscriptions in production

3. Schema pollution

myUserId is not a meaningful API argument. The client doesn't choose whose messages to receive — it's always their own. The argument exists solely as a workaround for the template engine's limitation.

4. Duplication across subscriptions

Every per-user subscription needs the same boilerplate argument and the same validation in the custom module. In my project, 14 out of 53 subscriptions follow this pattern.

Describe the solution you'd like

Proposed Solution

Support a claims namespace in subject templates, covering both the standard JWT sub claim and custom claims:

type Subscription {
  # Using standard JWT "sub" claim
  messageReceived: Message!
    @authenticated
    @edfs__natsSubscribe(
      subjects: ["edfs.messages.{{ claims.sub }}"]
    )

  # Using a custom JWT claim
  characterUpdated: Character!
    @authenticated
    @edfs__natsSubscribe(
      subjects: ["edfs.characters.{{ claims.characterId }}"]
    )
}

The router already has access to JWT claims via request.auth.claims (used in template expressions for headers and configuration). Extending this to EDFS subject templates would be a natural addition.

Supported template variables

Template Source
{{ args.myUserId }} GraphQL argument (existing)
{{ claims.sub }} Standard JWT sub claim
{{ claims.<custom> }} Any custom JWT claim (e.g. userId, characterId)

Both {{ claims.sub }} and custom claims like {{ claims.userId }} should be supported, since different teams store user identity in different claims depending on their auth provider setup.

Benefits

  • Zero custom modules for the most common auth pattern
  • No custom router builds — use the official Docker image as-is, trivial version bumps
  • Secure by default — no mismatch between argument and token possible
  • Cleaner schema — no dummy arguments that exist solely for routing
  • Lower barrier to entry — works out of the box, no Go required

Describe alternatives you've considered

No response

Additional context

Current Workaround

Today I use a custom Go module (RouterMiddlewareHandler) to intercept subscription requests, extract the user ID from JWT claims, and validate it matches the subscription argument.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions