# JWT (/docs/plugins/jwt)

Authenticate users with JWT tokens in services that can't use the session



The JWT plugin provides endpoints to retrieve a JWT token and a JWKS endpoint to verify the token.

<Callout type="info">
  This plugin is not meant as a replacement for the session. It's meant to be used for services that require JWT tokens. If you're looking to use JWT tokens for authentication, check out the [Bearer Plugin](/docs/plugins/bearer).
</Callout>

<Callout type="info">
  The JWT plugin's `/token` endpoint and `set-auth-jwt` header produce JWKS-verifiable JWTs for external services. They are separate from Better Auth's primary `session_token` cookie.

  If you enable `session.cookieCache.strategy = "jwt"`, the `session_data` cookie still uses HS256 with your Better Auth secret by default. To make that cookie-cache JWT verifiable with the JWT plugin's JWKS too, set `sessionCookieCache: true` on `jwt()`.
</Callout>

## Installation [#installation]

<Steps>
  <Step>
    ### Add the plugin to your **auth** config [#add-the-plugin-to-your-auth-config]

    ```ts title="auth.ts"
    import { betterAuth } from "better-auth"
    import { jwt } from "better-auth/plugins" // [!code highlight]

    export const auth = betterAuth({
        plugins: [
            jwt(), // [!code highlight]
        ]
    })
    ```
  </Step>

  <Step>
    ### Migrate the database [#migrate-the-database]

    Run the migration or generate the schema to add the necessary fields and tables to the database.

    <Tabs items="[&#x22;migrate&#x22;, &#x22;generate&#x22;]">
      <Tab value="migrate">
        <CodeBlockTabs defaultValue="npm" groupId="persist-install">
          <CodeBlockTabsList>
            <CodeBlockTabsTrigger value="npm">
              npm
            </CodeBlockTabsTrigger>

            <CodeBlockTabsTrigger value="pnpm">
              pnpm
            </CodeBlockTabsTrigger>

            <CodeBlockTabsTrigger value="yarn">
              yarn
            </CodeBlockTabsTrigger>

            <CodeBlockTabsTrigger value="bun">
              bun
            </CodeBlockTabsTrigger>
          </CodeBlockTabsList>

          <CodeBlockTab value="npm">
            ```bash
            npx auth migrate
            ```
          </CodeBlockTab>

          <CodeBlockTab value="pnpm">
            ```bash
            pnpm dlx auth migrate
            ```
          </CodeBlockTab>

          <CodeBlockTab value="yarn">
            ```bash
            yarn dlx auth migrate
            ```
          </CodeBlockTab>

          <CodeBlockTab value="bun">
            ```bash
            bun x auth migrate
            ```
          </CodeBlockTab>
        </CodeBlockTabs>
      </Tab>

      <Tab value="generate">
        <CodeBlockTabs defaultValue="npm" groupId="persist-install">
          <CodeBlockTabsList>
            <CodeBlockTabsTrigger value="npm">
              npm
            </CodeBlockTabsTrigger>

            <CodeBlockTabsTrigger value="pnpm">
              pnpm
            </CodeBlockTabsTrigger>

            <CodeBlockTabsTrigger value="yarn">
              yarn
            </CodeBlockTabsTrigger>

            <CodeBlockTabsTrigger value="bun">
              bun
            </CodeBlockTabsTrigger>
          </CodeBlockTabsList>

          <CodeBlockTab value="npm">
            ```bash
            npx auth generate
            ```
          </CodeBlockTab>

          <CodeBlockTab value="pnpm">
            ```bash
            pnpm dlx auth generate
            ```
          </CodeBlockTab>

          <CodeBlockTab value="yarn">
            ```bash
            yarn dlx auth generate
            ```
          </CodeBlockTab>

          <CodeBlockTab value="bun">
            ```bash
            bun x auth generate
            ```
          </CodeBlockTab>
        </CodeBlockTabs>
      </Tab>
    </Tabs>

    See the [Schema](#schema) section to add the fields manually.
  </Step>

  <Step>
    ### Add the client plugin to your auth client [#add-the-client-plugin-to-your-auth-client]

    ```ts title="auth-client.ts"
    import { createAuthClient } from "better-auth/client"
    import { jwtClient } from "better-auth/client/plugins" // [!code highlight]

    export const authClient = createAuthClient({
      plugins: [
        jwtClient() // [!code highlight]
      ]
    })
    ```
  </Step>
</Steps>

## Usage [#usage]

Once you've installed the plugin, you can start using the JWT & JWKS plugin to get the token and the JWKS through their respective endpoints.

## JWT [#jwt]

### Retrieve the token [#retrieve-the-token]

There are multiple ways to retrieve JWT tokens:

1. **Using the client plugin (recommended)**

```ts
import { authClient } from "@/lib/auth-client"

const { data, error } = await authClient.token()
if (error) {
  // handle error
}
if (data) {
  const jwtToken = data.token
  // Use this token for authenticated requests to external services
}
```

This is the recommended approach for client applications that need JWT tokens for external API authentication.

2. **Using your session token**

To get the token, call the `/token` endpoint. This will return the following:

```json
  { 
    "token": "ey..."
  }
```

Make sure to include the token in the `Authorization` header of your requests if the `bearer` plugin is added in your auth configuration.

```ts
await fetch("/api/auth/token", {
  headers: {
    "Authorization": `Bearer ${token}`
  },
})
```

3. **From `set-auth-jwt` header**

When you call `getSession` method, a JWT is returned in the `set-auth-jwt` header, which you can use to send to your services directly.

```ts
import { authClient } from "@/lib/auth-client"

await authClient.getSession({
  fetchOptions: {
    onSuccess: (ctx)=>{
      const jwt = ctx.response.headers.get("set-auth-jwt")
    }
  }
})
```

### Verifying the token [#verifying-the-token]

The token can be verified in your own service, without the need for an additional verify call or database check.
For this JWKS is used. The public key can be fetched from the `/api/auth/jwks` endpoint.

Since this key is not subject to frequent changes, it can be cached indefinitely.
The key ID (`kid`) that was used to sign a JWT is included in the header of the token.
In case a JWT with a different `kid` is received, it is recommended to fetch the JWKS again.

```json
  {
    "keys": [
        {
            "crv": "Ed25519",
            "x": "bDHiLTt7u-VIU7rfmcltcFhaHKLVvWFy-_csKZARUEU",
            "kty": "OKP",
            "kid": "c5c7995d-0037-4553-8aee-b5b620b89b23"
        }
    ]
  }
```

#### Example using jose with remote JWKS [#example-using-jose-with-remote-jwks]

```ts
import { jwtVerify, createRemoteJWKSet } from 'jose'

async function validateToken(token: string) {
  try {
    const JWKS = createRemoteJWKSet(
      new URL('http://localhost:3000/api/auth/jwks')
    )
    const { payload } = await jwtVerify(token, JWKS, {
      issuer: 'http://localhost:3000', // Should match your JWT issuer, which is the BASE_URL
      audience: 'http://localhost:3000', // Should match your JWT audience, which is the BASE_URL by default
    })
    return payload
  } catch (error) {
    console.error('Token validation failed:', error)
    throw error
  }
}

// Usage example
const token = 'your.jwt.token' // this is the token you get from the /api/auth/token endpoint
const payload = await validateToken(token)
```

#### Example with local JWKS [#example-with-local-jwks]

```ts
import { jwtVerify, createLocalJWKSet } from 'jose'


async function validateToken(token: string) {
  try {
    /**
     * This is the JWKS that you get from the /api/auth/
     * jwks endpoint
     */
    const storedJWKS = {
      keys: [{
        //...
      }]
    };
    const JWKS = createLocalJWKSet({
      keys: storedJWKS.data?.keys!,
    })
    const { payload } = await jwtVerify(token, JWKS, {
      issuer: 'http://localhost:3000', // Should match your JWT issuer, which is the BASE_URL
      audience: 'http://localhost:3000', // Should match your JWT audience, which is the BASE_URL by default
    })
    return payload
  } catch (error) {
    console.error('Token validation failed:', error)
    throw error
  }
}

// Usage example
const token = 'your.jwt.token' // this is the token you get from the /api/auth/token endpoint
const payload = await validateToken(token)
```

### OAuth Provider Mode [#oauth-provider-mode]

If you are making your system oAuth compliant (such as when utilizing the OIDC or MCP plugins), you **MUST** disable the `/token` endpoint (oAuth equivalent `/oauth2/token`) and disable setting the jwt header (oAuth equivalent `/oauth2/userinfo`).

```ts title="auth.ts"
import { betterAuth } from "better-auth";

betterAuth({
  disabledPaths: [
    "/token",
  ],
  plugins: [jwt({
    disableSettingJwtHeader: true,
  })]
})
```

### Remote JWKS Url [#remote-jwks-url]

Disables the `/jwks` endpoint and uses this endpoint in any discovery such as OIDC.

Useful if your JWKS are not managed at `/jwks` or if your jwks are signed with a certificate and placed on your CDN.

NOTE: you **MUST** specify which asymmetric algorithm is used for signing.

```ts title="auth.ts"
jwt({
  jwks: {
    remoteUrl: "https://example.com/.well-known/jwks.json",
    keyPairConfig: {
      alg: 'ES256',
    },
  }
})
```

### Custom JWKS Path [#custom-jwks-path]

By default, the JWKS endpoint is available at `/jwks`. You can customize this path using the `jwksPath` option.

This is useful when you need to:

* Follow OAuth 2.0/OIDC conventions (e.g., `/.well-known/jwks.json`)
* Match existing API conventions in your application
* Avoid path conflicts with other endpoints

**Server Configuration:**

```ts title="auth.ts"
jwt({
  jwks: {
    jwksPath: "/.well-known/jwks.json"
  }
})
```

**Client Configuration:**

When using a custom `jwksPath` on the server, you **MUST** configure the client with the same path:

```ts title="auth-client.ts"
import { createAuthClient } from "better-auth/client"
import { jwtClient } from "better-auth/client/plugins"

export const authClient = createAuthClient({
  plugins: [
    jwtClient({
      jwks: {
        jwksPath: "/.well-known/jwks.json" // Must match server configuration
      }
    })
  ]
})
```

Then you can use the `jwks()` method as usual:

```ts
const { data, error } = await authClient.jwks()
if (data) {
  // Use data.keys to verify JWT tokens
}
```

<Callout type="warning">
  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.
</Callout>

### Custom Signing [#custom-signing]

This is an advanced feature. Configuration outside of this plugin **MUST** be provided.

Implementers:

* `remoteUrl` must be defined if using the `sign` function. This shall store all active keys, not just the current one.
* If using localized approach, ensure server uses the latest private key when rotated. Depending on deployment, the server may need to be restarted.
* When using remote approach, verify the payload is unchanged after transit. Use integrity validation like CRC32 or SHA256 checks if available.

#### Localized Signing [#localized-signing]

```ts title="auth.ts"
jwt({
  jwks: {
    remoteUrl: "https://example.com/.well-known/jwks.json",
    keyPairConfig: {
      alg: 'EdDSA',
    },
  },
  jwt: {
    sign: async (jwtPayload: JWTPayload) => {
      // this is pseudocode
      return await new SignJWT(jwtPayload)
        .setProtectedHeader({
          alg: "EdDSA",
          kid: process.env.currentKid,
          typ: "JWT",
        })
        .sign(process.env.clientPrivateKey);
    },
  },
})
```

#### Remote Signing [#remote-signing]

Useful if you are using a remote Key Management Service such as [Google KMS](https://cloud.google.com/kms/docs/encrypt-decrypt-rsa#kms-encrypt-asymmetric-nodejs), [Amazon KMS](https://docs.aws.amazon.com/kms/latest/APIReference/API_Sign.html), or [Azure Key Vault](https://learn.microsoft.com/en-us/rest/api/keyvault/keys/sign/sign?view=rest-keyvault-keys-7.4\&tabs=HTTP).

```ts title="auth.ts"
jwt({
  jwks: {
    remoteUrl: "https://example.com/.well-known/jwks.json",
    keyPairConfig: {
      alg: 'ES256',
    },
  },
  jwt: {
    sign: async (jwtPayload: JWTPayload) => {
      // this is pseudocode
      const headers = JSON.stringify({ kid: '123', alg: 'ES256', typ: 'JWT' })
      const payload = JSON.stringify(jwtPayload)
      const encodedHeaders = Buffer.from(headers).toString('base64url')
      const encodedPayload = Buffer.from(payload).toString('base64url')
      const hash = createHash('sha256')
      const data = `${encodedHeaders}.${encodedPayload}`
      hash.update(Buffer.from(data))
      const digest = hash.digest()
      const sig = await remoteSign(digest)
      // integrityCheck(sig)
      const jwt = `${data}.${sig}`
      // verifyJwt(jwt)
      return jwt
    },
  },
})
```

## Schema [#schema]

The JWT plugin adds the following tables to the database:

### JWKS [#jwks]

Table Name: `jwks`



<DatabaseTable name="jwks" fields="jwksTableFields" />

<Callout>
  You can customize the table name and fields for the `jwks` table. See the [Database concept documentation](/docs/concepts/database#custom-table-names) for more information on how to customize plugin schema.
</Callout>

## Options [#options]

### Session cookie cache [#session-cookie-cache]

Set `sessionCookieCache` to `true` to sign JWT session cookie cache values with the plugin's locally managed keys. This requires `session.cookieCache.strategy` to be set to `"jwt"`.

```ts title="auth.ts"
jwt({
  sessionCookieCache: true
})
```

### Algorithm of the Key Pair [#algorithm-of-the-key-pair]

The algorithm used for the generation of the key pair. The default is **EdDSA** with the **Ed25519** curve. Below are the available options:

```ts title="auth.ts"
jwt({
  jwks: {
    keyPairConfig: {
      alg: "EdDSA",
      crv: "Ed25519"
    }
  }
})
```

#### EdDSA [#eddsa]

* **Default Curve**: `Ed25519`
* **Optional Property**: `crv`
  * Available options: `Ed25519`, `Ed448`
  * Default: `Ed25519`

#### ES256 [#es256]

* No additional properties

#### RSA256 [#rsa256]

* **Optional Property**: `modulusLength`
  * Expects a number
  * Default: `2048`

#### PS256 [#ps256]

* **Optional Property**: `modulusLength`
  * Expects a number
  * Default: `2048`

#### ECDH-ES [#ecdh-es]

* **Optional Property**: `crv`
  * Available options: `P-256`, `P-384`, `P-521`
  * Default: `P-256`

#### ES512 [#es512]

* No additional properties

### Disable private key encryption [#disable-private-key-encryption]

By default, the private key is encrypted using AES256 GCM. You can disable this by setting the `disablePrivateKeyEncryption` option to `true`.

For security reasons, it's recommended to keep the private key encrypted.

```ts title="auth.ts"
jwt({
  jwks: {
    disablePrivateKeyEncryption: true
  }
})
```

### Key Rotation [#key-rotation]

You can enable key rotation by setting the `rotationInterval` option. This will automatically rotate the key pair at the specified interval.

The default value is `undefined` (disabled).

```ts title="auth.ts"
jwt({
  jwks: {
    rotationInterval: 60 * 60 * 24 * 30, // 30 days
    gracePeriod: 60 * 60 * 24 * 30 // 30 days
  }
})
```

* `rotationInterval`: The interval in seconds to rotate the key pair.
* `gracePeriod`: The period in seconds to keep the old key pair valid after rotation. This is useful to allow clients to verify tokens signed by the old key pair. The default value is 30 days.

### Modify JWT payload [#modify-jwt-payload]

By default the entire user object is added to the JWT payload. You can modify the payload by providing a function to the `definePayload` option.

```ts title="auth.ts"
jwt({
  jwt: {
    definePayload: ({user}) => {
      return {
        id: user.id,
        email: user.email,
        role: user.role
      }
    }
  }
})
```

### Modify Issuer, Audience, Subject or Expiration time [#modify-issuer-audience-subject-or-expiration-time]

If none is given, the `BASE_URL` is used as the issuer and the audience is set to the `BASE_URL`. The expiration time is set to 15 minutes.

```ts title="auth.ts"
jwt({
  jwt: {
    issuer: "https://example.com",
    audience: "https://example.com",
    expirationTime: "1h",
    getSubject: (session) => {
      // by default the subject is the user id
      return session.user.email
    }
  }
})
```

### Custom Adapter [#custom-adapter]

By default, the JWT plugin stores and retrieves JWKS from your database. You can provide a custom adapter to override this behavior, allowing you to store JWKS in alternative locations such as Redis, external services, or in-memory storage.

```ts title="auth.ts"
jwt({
  adapter: {
    getJwks: async (ctx) => {
      // Custom implementation to get all JWKS
      // This overrides the default database query
      return await yourCustomStorage.getAllKeys()
    },
    createJwk: async (ctx, webKey) => {
      // Custom implementation to create a new key
      // This overrides the default database insert
      return await yourCustomStorage.createKey(webKey)
    }
  }
})
```

