File tree Expand file tree Collapse file tree 5 files changed +134
-2
lines changed
docs/content/docs/plugins
packages/better-auth/src/plugins/jwt Expand file tree Collapse file tree 5 files changed +134
-2
lines changed Original file line number Diff line number Diff line change @@ -239,6 +239,57 @@ jwt({
239239})
240240```
241241
242+ ### Custom JWKS Path
243+
244+ By default, the JWKS endpoint is available at ` /jwks ` . You can customize this path using the ` jwksPath ` option.
245+
246+ This is useful when you need to:
247+ - Follow OAuth 2.0/OIDC conventions (e.g., ` /.well-known/jwks.json ` )
248+ - Match existing API conventions in your application
249+ - Avoid path conflicts with other endpoints
250+
251+ ** Server Configuration:**
252+
253+ ``` ts title="auth.ts"
254+ jwt ({
255+ jwks: {
256+ jwksPath: " /.well-known/jwks.json"
257+ }
258+ })
259+ ```
260+
261+ ** Client Configuration:**
262+
263+ When using a custom ` jwksPath ` on the server, you ** MUST** configure the client with the same path:
264+
265+ ``` ts title="auth-client.ts"
266+ import { createAuthClient } from " better-auth/client"
267+ import { jwtClient } from " better-auth/client/plugins"
268+
269+ export const authClient = createAuthClient ({
270+ plugins: [
271+ jwtClient ({
272+ jwks: {
273+ jwksPath: " /.well-known/jwks.json" // Must match server configuration
274+ }
275+ })
276+ ]
277+ })
278+ ```
279+
280+ Then you can use the ` jwks() ` method as usual:
281+
282+ ``` ts
283+ const { data, error } = await authClient .jwks ()
284+ if (data ) {
285+ // Use data.keys to verify JWT tokens
286+ }
287+ ```
288+
289+ <Callout type = " warning" >
290+ The ` jwksPath ` configured on the client ** MUST** match the server configuration. If they don't match, the client will not be able to fetch the JWKS.
291+ </Callout >
292+
242293### Custom Signing
243294
244295This is an advanced feature. Configuration outside of this plugin ** MUST** be provided.
Original file line number Diff line number Diff line change 11import type { BetterAuthClientPlugin } from "@better-auth/core" ;
2+ import type { JSONWebKeySet } from "jose" ;
23import type { jwt } from "./index" ;
34
4- export const jwtClient = ( ) => {
5+ interface JwtClientOptions {
6+ jwks ?: {
7+ /**
8+ * The path of the endpoint exposing the JWKS.
9+ * Must match the server configuration.
10+ *
11+ * @default /jwks
12+ */
13+ jwksPath ?: string ;
14+ } ;
15+ }
16+
17+ export const jwtClient = ( options ?: JwtClientOptions ) => {
18+ const jwksPath = options ?. jwks ?. jwksPath ?? "/jwks" ;
19+
520 return {
621 id : "better-auth-client" ,
722 $InferServerPlugin : { } as ReturnType < typeof jwt > ,
23+ pathMethods : {
24+ [ jwksPath ] : "GET" ,
25+ } ,
26+ getActions : ( $fetch ) => ( {
27+ jwks : async ( fetchOptions ?: any ) => {
28+ return await $fetch < JSONWebKeySet > ( jwksPath , {
29+ method : "GET" ,
30+ ...fetchOptions ,
31+ } ) ;
32+ } ,
33+ } ) ,
834 } satisfies BetterAuthClientPlugin ;
935} ;
Original file line number Diff line number Diff line change @@ -36,12 +36,25 @@ export const jwt = (options?: JwtOptions | undefined) => {
3636 ) ;
3737 }
3838
39+ const jwksPath = options ?. jwks ?. jwksPath ?? "/jwks" ;
40+ if (
41+ typeof jwksPath !== "string" ||
42+ jwksPath . length === 0 ||
43+ ! jwksPath . startsWith ( "/" ) ||
44+ jwksPath . includes ( ".." )
45+ ) {
46+ throw new BetterAuthError (
47+ "jwks_config" ,
48+ "jwksPath must be a non-empty string starting with '/' and not contain '..'" ,
49+ ) ;
50+ }
51+
3952 return {
4053 id : "jwt" ,
4154 options,
4255 endpoints : {
4356 getJwks : createAuthEndpoint (
44- "/jwks" ,
57+ jwksPath ,
4558 {
4659 method : "GET" ,
4760 metadata : {
Original file line number Diff line number Diff line change @@ -743,3 +743,36 @@ describe("jwt - custom adapter", async () => {
743743 expect ( storage . length ) . toBe ( 1 ) ;
744744 } ) ;
745745} ) ;
746+
747+ describe ( "jwt - custom jwksPath" , async ( ) => {
748+ it ( "should use custom jwksPath when specified" , async ( ) => {
749+ const { auth } = await getTestInstance ( {
750+ plugins : [
751+ jwt ( {
752+ jwks : {
753+ jwksPath : "/.well-known/jwks.json" ,
754+ } ,
755+ } ) ,
756+ ] ,
757+ } ) ;
758+
759+ const client = createAuthClient ( {
760+ plugins : [ jwtClient ( { jwks : { jwksPath : "/.well-known/jwks.json" } } ) ] ,
761+ baseURL : "http://localhost:3000/api/auth" ,
762+ fetchOptions : {
763+ customFetchImpl : async ( url , init ) => {
764+ return auth . handler ( new Request ( url , init ) ) ;
765+ } ,
766+ } ,
767+ } ) ;
768+
769+ const jwks = await client . jwks ( ) ;
770+ expect ( jwks . error ) . toBeNull ( ) ;
771+ expect ( jwks . data ?. keys ) . toBeDefined ( ) ;
772+ expect ( jwks . data ?. keys . length ) . toBeGreaterThan ( 0 ) ;
773+
774+ // Verify old /jwks endpoint is not found
775+ const oldJwks = await client . $fetch < JSONWebKeySet > ( "/jwks" ) ;
776+ expect ( oldJwks . error ?. status ) . toBe ( 404 ) ;
777+ } ) ;
778+ } ) ;
Original file line number Diff line number Diff line change @@ -42,6 +42,15 @@ export interface JwtOptions {
4242 * @default 2592000 (30 days)
4343 */
4444 gracePeriod ?: number ;
45+ /**
46+ * The path of the endpoint exposing the JWKS.
47+ * When set, this replaces the default /jwks endpoint.
48+ * The old endpoint will return 404.
49+ *
50+ * @default /jwks
51+ * @example "/.well-known/jwks.json"
52+ */
53+ jwksPath ?: string ;
4554 }
4655 | undefined ;
4756
You can’t perform that action at this time.
0 commit comments