[GH-ISSUE #910] JWT Error... non-extractable CryptoKey cannot be exported as a JWK #8496

Closed
opened 2026-04-13 03:34:32 -05:00 by GiteaMirror · 9 comments
Owner

Originally created by @daveycodez on GitHub (Dec 16, 2024).
Original GitHub issue: https://github.com/better-auth/better-auth/issues/910

Is this suited for github?

  • Yes, this is suited for github

To Reproduce

I set up Better Auth in NextJS and I'm trying to enable the JWT provider. I'm trying to get this bad boy working with Neon RLS.
I added the plugins, ran the migrations, confirmed I have the jwks table, and now when I navigate to /api/auth/token I get this error:

non-extractable CryptoKey cannot be exported as a JWK

/pages/test.tsx

export default function Test() {
    const { data: session } = authClient.useSession()

    useEffect(() => {
        fetch("/api/auth/token", {
            headers: {
                "Authorization": `Bearer ${session?.session.token}`
            },
        })
    }, [session])

    return <div />
}

/pages/api/auth/[...all].ts

import { toNextJsHandler } from "better-auth/next-js"
import { auth } from "@/lib/auth"
import { NextRequest } from "next/server"

export const config = { runtime: "edge" }

const handler = (req: NextRequest) =>
    toNextJsHandler(auth.handler)[req.method as "GET" | "POST"](req)

export default handler

Also why does session live on session? It's weird to see session.session.. The useSession hook should definitely pass session directly so you don't have to cast "data" and it should have session.token and session.user

Current vs. Expected behavior

Need to get JWT and JWKS working

What version of Better Auth are you using?

1.0.21

Provide environment information

NextJS Pages Router
Neon

Which area(s) are affected? (Select all that apply)

Backend, Client, Documentation, Package

Auth config (if applicable)

import { getURL } from "@/lib/utils"
import { betterAuth } from "better-auth"
import { NeonHTTPDialect } from "kysely-neon"
import { jwt, bearer } from "better-auth/plugins"

export const auth = betterAuth({
    baseURL: getURL(),
    database: {
        dialect: new NeonHTTPDialect({
            connectionString: process.env.DATABASE_URL!,
        }),
        type: "postgres"
    },
    emailAndPassword: { enabled: true },
    plugins: [
        jwt(),
        bearer()
    ]
})

Additional context

No response

Originally created by @daveycodez on GitHub (Dec 16, 2024). Original GitHub issue: https://github.com/better-auth/better-auth/issues/910 ### Is this suited for github? - [X] Yes, this is suited for github ### To Reproduce I set up Better Auth in NextJS and I'm trying to enable the JWT provider. I'm trying to get this bad boy working with Neon RLS. I added the plugins, ran the migrations, confirmed I have the jwks table, and now when I navigate to /api/auth/token I get this error: `non-extractable CryptoKey cannot be exported as a JWK` /pages/test.tsx ```tsx export default function Test() { const { data: session } = authClient.useSession() useEffect(() => { fetch("/api/auth/token", { headers: { "Authorization": `Bearer ${session?.session.token}` }, }) }, [session]) return <div /> } ``` /pages/api/auth/[...all].ts ```ts import { toNextJsHandler } from "better-auth/next-js" import { auth } from "@/lib/auth" import { NextRequest } from "next/server" export const config = { runtime: "edge" } const handler = (req: NextRequest) => toNextJsHandler(auth.handler)[req.method as "GET" | "POST"](req) export default handler ``` Also why does session live on session? It's weird to see session.session.. The useSession hook should definitely pass session directly so you don't have to cast "data" and it should have session.token and session.user ### Current vs. Expected behavior Need to get JWT and JWKS working ### What version of Better Auth are you using? 1.0.21 ### Provide environment information ```bash NextJS Pages Router Neon ``` ### Which area(s) are affected? (Select all that apply) Backend, Client, Documentation, Package ### Auth config (if applicable) ```typescript import { getURL } from "@/lib/utils" import { betterAuth } from "better-auth" import { NeonHTTPDialect } from "kysely-neon" import { jwt, bearer } from "better-auth/plugins" export const auth = betterAuth({ baseURL: getURL(), database: { dialect: new NeonHTTPDialect({ connectionString: process.env.DATABASE_URL!, }), type: "postgres" }, emailAndPassword: { enabled: true }, plugins: [ jwt(), bearer() ] }) ``` ### Additional context _No response_
GiteaMirror added the lockedbug labels 2026-04-13 03:34:32 -05:00
Author
Owner

@daveycodez commented on GitHub (Dec 16, 2024):

 await signIn.email({ email, password }, {
                                    onSuccess: (ctx) => {
                                        const authToken = ctx.response.headers.get("set-auth-token") // get the token from the response headers
                                        // Store the token securely (e.g., in localStorage)
                                        localStorage.setItem("bearer_token", authToken!)
                                    }
                                })

Trying this on sign in, it's correctly setting bearer_token in localStorage.


    useEffect(() => {
        const bearerToken = localStorage.getItem("bearer_token")
        fetch("/api/auth/token", {
            headers: {
                "Authorization": `Bearer ${bearerToken}`
            },
        })
    }, [session])

Same error, non-extractable CryptoKey cannot be exported as a JWK

<!-- gh-comment-id:2545413490 --> @daveycodez commented on GitHub (Dec 16, 2024): ```ts await signIn.email({ email, password }, { onSuccess: (ctx) => { const authToken = ctx.response.headers.get("set-auth-token") // get the token from the response headers // Store the token securely (e.g., in localStorage) localStorage.setItem("bearer_token", authToken!) } }) ``` Trying this on sign in, it's correctly setting bearer_token in localStorage. ```ts useEffect(() => { const bearerToken = localStorage.getItem("bearer_token") fetch("/api/auth/token", { headers: { "Authorization": `Bearer ${bearerToken}` }, }) }, [session]) ``` Same error, non-extractable CryptoKey cannot be exported as a JWK
Author
Owner

@daveycodez commented on GitHub (Dec 16, 2024):

npx @better-auth/cli generate
ℹ  Your schema is already up to date.       

I might just have to manually sign JWT's on my requests to Neon and provide my own JWKS but I was hoping to get this to work out of the box

<!-- gh-comment-id:2545416768 --> @daveycodez commented on GitHub (Dec 16, 2024): ``` npx @better-auth/cli generate ℹ Your schema is already up to date. ``` I might just have to manually sign JWT's on my requests to Neon and provide my own JWKS but I was hoping to get this to work out of the box
Author
Owner

@Bekacru commented on GitHub (Dec 16, 2024):

Hey, please check 1.0.22-beta.2 and confirm me if it's working.

<!-- gh-comment-id:2545596672 --> @Bekacru commented on GitHub (Dec 16, 2024): Hey, please check `1.0.22-beta.2` and confirm me if it's working.
Author
Owner

@daveycodez commented on GitHub (Dec 16, 2024):

Will do thanks for the swift response. Loving the library so far.

Also I only receive that bearer token from signIn callback, I tried putting the global fetcher on the createAuth client and it wasnt getting called

<!-- gh-comment-id:2546360916 --> @daveycodez commented on GitHub (Dec 16, 2024): Will do thanks for the swift response. Loving the library so far. Also I only receive that bearer token from signIn callback, I tried putting the global fetcher on the createAuth client and it wasnt getting called
Author
Owner

@daveycodez commented on GitHub (Dec 16, 2024):

@Bekacru

New error..

  TypeError TypeError: "alg" argument is required when "jwk.alg" is not present {  }

This is when I pass the jwt() plugin without options. It is now creating entries in the jwks table which is progress but now I get this error. If I provide options to JWT like below, no entries get created in the table at all and I get the old non-extractable CryptoKey cannot be exported as a JWK

jwt({
  jwks: {
    keyPairConfig: {
      alg: "EdDSA",
      crv: "Ed25519"
    }
  }
})

Is it possible this is failing due to using the edge configuration for my auth API endpoints? I know there can be issues with Crypto there, I've had to use the "jose" library to sign JWT's on edge routes in the past since OG jwt library wouldn't work

<!-- gh-comment-id:2546423334 --> @daveycodez commented on GitHub (Dec 16, 2024): @Bekacru New error.. TypeError TypeError: "alg" argument is required when "jwk.alg" is not present { } This is when I pass the jwt() plugin without options. It is now creating entries in the jwks table which is progress but now I get this error. If I provide options to JWT like below, no entries get created in the table at all and I get the old non-extractable CryptoKey cannot be exported as a JWK ```ts jwt({ jwks: { keyPairConfig: { alg: "EdDSA", crv: "Ed25519" } } }) ``` Is it possible this is failing due to using the edge configuration for my auth API endpoints? I know there can be issues with Crypto there, I've had to use the "jose" library to sign JWT's on edge routes in the past since OG jwt library wouldn't work
Author
Owner

@daveycodez commented on GitHub (Dec 16, 2024):

Changing my NextJS Pages API Route to use the NodeJS handler is working now it is giving me token for /api/auth/token however it breaks everything else

/pages/auth/[...all].ts

import { auth } from "@/lib/auth"
import { toNodeHandler } from "better-auth/node";

export default toNodeHandler(auth)

For other requests like sign up, login log out:
⨯ TypeError: Response body object should not be disturbed or locked

I really don't want to have to use the App Router for one endpoint since it makes builds a lot slower, really need Pages Router support somehow

Is there another way to make a custom handler without a helper function? Or can we get helper functions for Pages Router API Routes and support for Pages Router Edge API Routes?

EDIT looks like using the App Router fixed it.. but yea I really don't want to use the App Router

<!-- gh-comment-id:2546456043 --> @daveycodez commented on GitHub (Dec 16, 2024): Changing my NextJS Pages API Route to use the NodeJS handler is working now it is giving me token for /api/auth/token however it breaks everything else /pages/auth/[...all].ts ```ts import { auth } from "@/lib/auth" import { toNodeHandler } from "better-auth/node"; export default toNodeHandler(auth) ``` For other requests like sign up, login log out: ⨯ TypeError: Response body object should not be disturbed or locked I really don't want to have to use the App Router for one endpoint since it makes builds a lot slower, really need Pages Router support somehow Is there another way to make a custom handler without a helper function? Or can we get helper functions for Pages Router API Routes and support for Pages Router Edge API Routes? **EDIT** looks like using the App Router fixed it.. but yea I really don't want to use the App Router
Author
Owner

@daveycodez commented on GitHub (Dec 17, 2024):

I actually cannot use the App Router API Routes because I use output: "export" for Capacitor, so I won't be able to use JWT feature if Pages API Router doesn't have support sadly

<!-- gh-comment-id:2547648175 --> @daveycodez commented on GitHub (Dec 17, 2024): I actually cannot use the App Router API Routes because I use output: "export" for Capacitor, so I won't be able to use JWT feature if Pages API Router doesn't have support sadly
Author
Owner

@izakfilmalter commented on GitHub (Jun 1, 2025):

If anyone is using bun and runs into this, you can see some insight in this issue: https://github.com/oven-sh/bun/issues/19212

You need to make sure you include extractable: true in your keyPairConfig in the plugin in auth.ts. Example:

export const auth = betterAuth({
  ...
  plugins: [
    jwt({
      jwks: {
        // @ts-expect-error - extractable is a valid option for keyPairConfig
        keyPairConfig: { alg: 'EdDSA', crv: 'Ed25519', extractable: true },
      },
    }),
  ],
  ...
})
<!-- gh-comment-id:2927767968 --> @izakfilmalter commented on GitHub (Jun 1, 2025): If anyone is using bun and runs into this, you can see some insight in this issue: https://github.com/oven-sh/bun/issues/19212 You need to make sure you include `extractable: true` in your `keyPairConfig` in the plugin in auth.ts. Example: ```ts export const auth = betterAuth({ ... plugins: [ jwt({ jwks: { // @ts-expect-error - extractable is a valid option for keyPairConfig keyPairConfig: { alg: 'EdDSA', crv: 'Ed25519', extractable: true }, }, }), ], ... }) ```
Author
Owner

@DiegoGonzalezCruz commented on GitHub (Jul 15, 2025):

this was a random error for me, I solved like you suggested. Thank you!

<!-- gh-comment-id:3073198724 --> @DiegoGonzalezCruz commented on GitHub (Jul 15, 2025): this was a random error for me, I solved like you suggested. Thank you!
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: github-starred/better-auth#8496