JWT plugin: Support token “templates” (client-selected), e.g. authClient.token({ template: "backend1h" }) #2197

Closed
opened 2026-03-13 09:33:46 -05:00 by GiteaMirror · 2 comments
Owner

Originally created by @hasirciogluhq on GitHub (Oct 24, 2025).

Is this suited for github?

  • Yes, this is suited for github

Summary
Today, the JWT plugin exposes a single, predefined token shape/expiry via definePayload and global options. We often need multiple token variants (e.g., backend1h, backend2, m2m) with different payload, aud, and TTL. The current client API authClient.token() does not accept parameters, so we cannot choose a variant at call time. I’m proposing template-based tokens selectable from the client in a secure, backward-compatible way.

Problem

  • Different consumers require different claims/TTLs (internal backend vs. partner API vs. M2M short-lived tokens).
  • We can “hack” around it with multiple Better Auth instances or custom endpoints, but that fragments config and JWKS.
  • Docs show definePayload({ user }) and authClient.token() (no params), which limits flexibility. ([Better Auth][2])

Proposed API

Client (no breaking change, param optional):

// current stays valid: await authClient.token()
const { data, error } = await authClient.token({ template: "backend1h" });

Server (JWT plugin options):

jwt({
  jwks: { /* existing config */ },
  jwt: {
    // NEW: named templates
    templates: {
      backend1h: { expiresIn: "1h", aud: "internal-backend" },
      backend2:  { expiresIn: "2h", aud: "internal-backend" },
      m2m:       { expiresIn: "15m", aud: "machine", scope: ["m2m"] }
    },
    // NEW: authorize which templates a caller can request
    allowedTemplates: async ({ user, ctx }) => {
      // example: only org admins can mint m2m
      return user.role === "admin" ? ["backend1h","backend2","m2m"] : ["backend1h","backend2"];
    },
    // extended: definePayload also receives template
    definePayload: ({ user, ctx, template }) => {
      const base = { id: user.id, email: user.email, role: user.role };
      if (template === "m2m") return { ...base, m2m: true };
      return base;
    }
  }
})

Route behavior

  • /token continues to work without params (default template).
  • If template is provided (body JSON or query), plugin validates it via allowedTemplates, merges with templates[template], and signs accordingly.
  • OIDC mode: accept token_template as an extension param on /oauth2/token. ([Better Auth][3])

Security Considerations

  • Template allow-list per user/org (allowedTemplates).
  • Distinct aud and optional key rotation per template (kid), so verifiers can scope usage.
  • Rate-limit /token by template; include jti + replay-protection if needed.
  • OpenAPI plugin should document the optional template parameter. ([Better Auth][7])

DX & Backward Compatibility

  • Zero-break: authClient.token() çalışmaya devam eder.
  • Types: authClient.token(opts?: { template?: string }).
  • Docs: Update JWT plugin page (Modify Payload, JWKS, /token section) with examples. ([Better Auth][2])

Example Use Case

// Frontend needs a 1h token for our Go backend
const { token } = (await authClient.token({ template: "backend1h" })).data!;

// Internal cron worker wants m2m-short token
const { token: m2m } = (await authClient.token({ template: "m2m" })).data!;

References

  • JWT plugin docs (definePayload, /token) ([Better Auth][2])
  • OIDC provider + JWT/JWKS interaction (param lives under /oauth2/token when OIDC mode) ([Better Auth][3])
  • Plugins & routing guidance (unique paths, OpenAPI) ([Better Auth][6])

Acceptance Criteria

  • authClient.token({ template }) supported on web/node clients.
  • Server validates template against config and permissions.
  • Returned JWT reflects template’s TTL/claims/audience.
  • Docs & OpenAPI updated; examples for Next.js/Hono/Express.

Describe the solution you'd like

I’d like the JWT plugin to support named token templates that can be selected by the client when calling authClient.token({ template }).

Each template would define its own claims, TTL, and audience inside the server configuration, and the selected template name would be securely passed to the server.

Example:

// client
const { token } = await authClient.token({ template: "backend1h" });

// server
jwt({
  jwt: {
    templates: {
      backend1h: { expiresIn: "1h", aud: "internal" },
      m2m: { expiresIn: "15m", aud: "machine" },
    },
    allowedTemplates: ({ user }) =>
      user.role === "admin" ? ["backend1h", "m2m"] : ["backend1h"],
    definePayload: ({ user, template }) => ({
      id: user.id,
      email: user.email,
      role: user.role,
      template,
    }),
  },
});

This would let developers issue short-lived or specialized JWTs for different backends, machine-to-machine flows, or partner integrations without deploying multiple Better Auth instances.

Describe alternatives you've considered

  • Custom endpoints:
    Create /api/auth/token/<type> routes that manually sign tokens using internal utilities.
    → Works but breaks the unified /token flow and duplicates config logic.

  • Multiple Better Auth setups:
    Run separate auth servers (each with different JWT config).
    → Heavy, unscalable, inconsistent JWKS.

  • Custom middleware:
    Intercept /token and add logic based on query/body.
    → Hacky and fragile because it relies on undocumented internals.

None of these alternatives provide a clean, secure, and declarative way to manage multiple token variants.

Additional context

  • Related docs: [JWT plugin — Modify JWT Payload](https://www.better-auth.com/docs/plugins/jwt#modify-jwt-payload)

  • Current limitation: authClient.token() does not accept parameters.

  • Motivation: Need to generate different tokens (e.g., backend1h, backend2, m2m) with distinct lifetimes, audiences, or scopes, all signed by the same JWKS keyset.

  • Expected benefits:

    • Easier integration between Better Auth and backend microservices.
    • Reusable, centralized JWT signing configuration.
    • Backward-compatible API (no breaking changes).

If implemented, this feature would align Better Auth’s JWT plugin with the flexibility of systems like Auth0 “token exchange” or Clerk “custom session tokens,” while keeping the simplicity of authClient.token().

Originally created by @hasirciogluhq on GitHub (Oct 24, 2025). ### Is this suited for github? - [x] Yes, this is suited for github ### Is your feature request related to a problem? Please describe. **Summary** Today, the JWT plugin exposes a single, predefined token shape/expiry via `definePayload` and global options. We often need **multiple token variants** (e.g., `backend1h`, `backend2`, `m2m`) with different payload, `aud`, and TTL. The current client API `authClient.token()` **does not accept parameters**, so we cannot choose a variant at call time. I’m proposing **template-based tokens** selectable from the client in a secure, backward-compatible way. **Problem** * Different consumers require different claims/TTLs (internal backend vs. partner API vs. M2M short-lived tokens). * We can “hack” around it with multiple Better Auth instances or custom endpoints, but that fragments config and JWKS. * Docs show `definePayload({ user })` and `authClient.token()` (no params), which limits flexibility. ([[Better Auth](https://www.better-auth.com/docs/plugins/jwt?utm_source=chatgpt.com)][2]) **Proposed API** *Client (no breaking change, param optional):* ```ts // current stays valid: await authClient.token() const { data, error } = await authClient.token({ template: "backend1h" }); ``` *Server (JWT plugin options):* ```ts jwt({ jwks: { /* existing config */ }, jwt: { // NEW: named templates templates: { backend1h: { expiresIn: "1h", aud: "internal-backend" }, backend2: { expiresIn: "2h", aud: "internal-backend" }, m2m: { expiresIn: "15m", aud: "machine", scope: ["m2m"] } }, // NEW: authorize which templates a caller can request allowedTemplates: async ({ user, ctx }) => { // example: only org admins can mint m2m return user.role === "admin" ? ["backend1h","backend2","m2m"] : ["backend1h","backend2"]; }, // extended: definePayload also receives template definePayload: ({ user, ctx, template }) => { const base = { id: user.id, email: user.email, role: user.role }; if (template === "m2m") return { ...base, m2m: true }; return base; } } }) ``` *Route behavior* * `/token` continues to work without params (default template). * If `template` is provided (body JSON or query), plugin validates it via `allowedTemplates`, merges with `templates[template]`, and signs accordingly. * **OIDC mode**: accept `token_template` as an extension param on `/oauth2/token`. ([[Better Auth](https://www.better-auth.com/docs/plugins/oidc-provider?utm_source=chatgpt.com)][3]) **Security Considerations** * Template allow-list per user/org (`allowedTemplates`). * Distinct `aud` and optional key rotation per template (`kid`), so verifiers can scope usage. * Rate-limit `/token` by template; include `jti` + replay-protection if needed. * OpenAPI plugin should document the optional `template` parameter. ([[Better Auth](https://www.better-auth.com/docs/plugins/open-api?utm_source=chatgpt.com)][7]) **DX & Backward Compatibility** * Zero-break: `authClient.token()` çalışmaya devam eder. * Types: `authClient.token(opts?: { template?: string })`. * Docs: Update JWT plugin page (Modify Payload, JWKS, `/token` section) with examples. ([[Better Auth](https://www.better-auth.com/docs/plugins/jwt?utm_source=chatgpt.com)][2]) **Example Use Case** ```ts // Frontend needs a 1h token for our Go backend const { token } = (await authClient.token({ template: "backend1h" })).data!; // Internal cron worker wants m2m-short token const { token: m2m } = (await authClient.token({ template: "m2m" })).data!; ``` **References** * JWT plugin docs (definePayload, /token) ([[Better Auth](https://www.better-auth.com/docs/plugins/jwt?utm_source=chatgpt.com)][2]) * OIDC provider + JWT/JWKS interaction (param lives under /oauth2/token when OIDC mode) ([[Better Auth](https://www.better-auth.com/docs/plugins/oidc-provider?utm_source=chatgpt.com)][3]) * Plugins & routing guidance (unique paths, OpenAPI) ([[Better Auth](https://www.better-auth.com/docs/concepts/plugins?utm_source=chatgpt.com)][6]) **Acceptance Criteria** * `authClient.token({ template })` supported on web/node clients. * Server validates `template` against config and permissions. * Returned JWT reflects template’s TTL/claims/audience. * Docs & OpenAPI updated; examples for Next.js/Hono/Express. ### Describe the solution you'd like I’d like the **JWT plugin** to support **named token templates** that can be selected by the client when calling `authClient.token({ template })`. Each template would define its own claims, TTL, and audience inside the server configuration, and the selected template name would be securely passed to the server. **Example:** ```ts // client const { token } = await authClient.token({ template: "backend1h" }); // server jwt({ jwt: { templates: { backend1h: { expiresIn: "1h", aud: "internal" }, m2m: { expiresIn: "15m", aud: "machine" }, }, allowedTemplates: ({ user }) => user.role === "admin" ? ["backend1h", "m2m"] : ["backend1h"], definePayload: ({ user, template }) => ({ id: user.id, email: user.email, role: user.role, template, }), }, }); ``` This would let developers issue short-lived or specialized JWTs for different backends, machine-to-machine flows, or partner integrations **without deploying multiple Better Auth instances**. ### Describe alternatives you've considered * **Custom endpoints:** Create `/api/auth/token/<type>` routes that manually sign tokens using internal utilities. → Works but breaks the unified `/token` flow and duplicates config logic. * **Multiple Better Auth setups:** Run separate auth servers (each with different JWT config). → Heavy, unscalable, inconsistent JWKS. * **Custom middleware:** Intercept `/token` and add logic based on query/body. → Hacky and fragile because it relies on undocumented internals. None of these alternatives provide a **clean, secure, and declarative** way to manage multiple token variants. ### Additional context * Related docs: [[JWT plugin — Modify JWT Payload](https://www.better-auth.com/docs/plugins/jwt#modify-jwt-payload)](https://www.better-auth.com/docs/plugins/jwt#modify-jwt-payload) * Current limitation: `authClient.token()` does not accept parameters. * Motivation: Need to generate different tokens (e.g., `backend1h`, `backend2`, `m2m`) with distinct lifetimes, audiences, or scopes, all signed by the same JWKS keyset. * Expected benefits: * Easier integration between Better Auth and backend microservices. * Reusable, centralized JWT signing configuration. * Backward-compatible API (no breaking changes). If implemented, this feature would align Better Auth’s JWT plugin with the flexibility of systems like Auth0 “token exchange” or Clerk “custom session tokens,” while keeping the simplicity of `authClient.token()`.
GiteaMirror added the enhancementplugin labels 2026-03-13 09:33:47 -05:00
Author
Owner

@dvanmali commented on GitHub (Oct 29, 2025):

I use scopeExpirations approach in my PR #4163 for scope-based expirations. Additionally I separate token type expirations via accessTokenExpiresIn, m2mAccessTokenExpiresIn, idTokenExpiresIn, and refreshTokenExpiresIn and it supports replay prevention. For multiple audiences, you can use validAudiences. For backend specific lifetimes, you would likely wish to deploy multiple authorization servers (ie at another domain).

Take a look and let me know if you have questions 👍

@dvanmali commented on GitHub (Oct 29, 2025): I use `scopeExpirations` approach in my PR #4163 for scope-based expirations. Additionally I separate token type expirations via `accessTokenExpiresIn`, `m2mAccessTokenExpiresIn`, `idTokenExpiresIn`, and `refreshTokenExpiresIn` and it supports replay prevention. For multiple audiences, you can use `validAudiences`. For `backend` specific lifetimes, you would likely wish to deploy multiple authorization servers (ie at another domain). Take a look and let me know if you have questions 👍
Author
Owner

@dosubot[bot] commented on GitHub (Jan 28, 2026):

Hi, @hasirciogluhq. I'm Dosu, and I'm helping the better-auth team manage their backlog and am marking this issue as stale.

Issue Summary:

  • You proposed enhancing the JWT plugin to support multiple token templates selectable by clients, allowing varied payloads, audiences, and TTLs with server-side control per user role.
  • The goal was to maintain backward compatibility and extend token customization via an updated definePayload function.
  • The maintainer referenced PR #4163, which implements scope-based expirations and multiple token type expirations.
  • They suggested using multiple authorization servers for backend-specific token lifetimes as a solution.
  • This PR effectively addresses your enhancement request.

Next Steps:

  • Please confirm if this solution in the latest version of better-auth still meets your needs or if you have further questions.
  • If I don’t hear back within 7 days, I will automatically close this issue.

Thank you for your understanding and contribution!

@dosubot[bot] commented on GitHub (Jan 28, 2026): Hi, @hasirciogluhq. I'm [Dosu](https://dosu.dev), and I'm helping the better-auth team manage their backlog and am marking this issue as stale. **Issue Summary:** - You proposed enhancing the JWT plugin to support multiple token templates selectable by clients, allowing varied payloads, audiences, and TTLs with server-side control per user role. - The goal was to maintain backward compatibility and extend token customization via an updated definePayload function. - The maintainer referenced PR #4163, which implements scope-based expirations and multiple token type expirations. - They suggested using multiple authorization servers for backend-specific token lifetimes as a solution. - This PR effectively addresses your enhancement request. **Next Steps:** - Please confirm if this solution in the latest version of better-auth still meets your needs or if you have further questions. - If I don’t hear back within 7 days, I will automatically close this issue. Thank you for your understanding and contribution!
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: github-starred/better-auth#2197