[GH-ISSUE #5663] Prevent duplicate JWKs caused by race conditions during creation and signing #27640

Closed
opened 2026-04-17 18:45:23 -05:00 by GiteaMirror · 1 comment
Owner

Originally created by @luist18 on GitHub (Oct 29, 2025).
Original GitHub issue: https://github.com/better-auth/better-auth/issues/5663

Is this suited for github?

  • Yes, this is suited for github

To Reproduce

curl https://better-auth-endpoint/jwks & \
> curl https://better-auth-endpoint/jwks & \
> wait

With enough luck, you will get into a data race; otherwise, keep trying until you get two entries in the database 😃

Current vs. Expected behavior

The JWK creation logic in the JWT plugin appears to be prone to data races. The following code, part of the handler for the /jwks endpoint, illustrates the issue:

cd567659b8/packages/better-auth/src/plugins/jwt/index.ts (L134-L141)

The current implementation first queries all keys in the database, and if none are found, it proceeds to create a new one. This introduces a race condition: two (or more) concurrent requests may both determine that no keys exist before either has inserted one, resulting in multiple keys being created.
While this doesn’t cause a critical failure (you just end up with two keys), it is still an unintended and inefficient behavior that can be improved.

A similar race condition can occur when signing a token:

cd567659b8/packages/better-auth/src/plugins/jwt/sign.ts (L54-L61)

JWKs are created lazily (just like the /jwks endpoint) if a token is requested but no JWK exists, a new one is generated. However, multiple concurrent requests might all evaluate the last known key as undefined, again leading to duplicate key creation.

Addressing this issue is non-trivial and will likely depend on the database dialect. Ideally, the existence check should be optimized to query specifically for one valid key rather than fetching all keys (getting all keys in the database with all fields is not very optimized just to check if one key exists). The operation should also be wrapped in a transaction or similar concurrency-safe mechanism to ensure that only one JWK can be inserted at a time. Depending on the database, this could be achieved with a row-level or table-level lock, or by leveraging unique constraints and catching duplicate-insert errors to enforce atomicity without locking the entire table.

What version of Better Auth are you using?

1.3.34

System info

{
  "system": {
    "platform": "darwin",
    "arch": "arm64",
    "version": "Darwin Kernel Version 24.6.0: Mon Aug 11 21:15:09 PDT 2025; root:xnu-11417.140.69.701.11~1/RELEASE_ARM64_T6041",
    "release": "24.6.0",
    "cpuCount": 16,
    "cpuModel": "Apple M4 Max",
    "totalMemory": "64.00 GB",
    "freeMemory": "0.16 GB"
  },
  "node": {
    "version": "v20.15.1",
    "env": "development"
  },
  "packageManager": {
    "name": "npm",
    "version": "10.8.2"
  },
  "frameworks": [
    {
      "name": "react",
      "version": "^19.1.0"
    }
  ],
  "databases": null,
  "betterAuth": {
    "version": "Unknown",
    "config": null
  }
}

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

Backend

Auth config (if applicable)


Additional context

No response

Originally created by @luist18 on GitHub (Oct 29, 2025). Original GitHub issue: https://github.com/better-auth/better-auth/issues/5663 ### Is this suited for github? - [x] Yes, this is suited for github ### To Reproduce ```bash curl https://better-auth-endpoint/jwks & \ > curl https://better-auth-endpoint/jwks & \ > wait ``` With enough luck, you will get into a data race; otherwise, keep trying until you get two entries in the database 😃 ### Current vs. Expected behavior The JWK creation logic in the JWT plugin appears to be prone to data races. The following code, part of the handler for the [/jwks](https://github.com/better-auth/better-auth/blob/cd567659b8b6c78bf90843fd7218446a4ca6aa9b/packages/better-auth/src/plugins/jwt/index.ts#L42) endpoint, illustrates the issue: https://github.com/better-auth/better-auth/blob/cd567659b8b6c78bf90843fd7218446a4ca6aa9b/packages/better-auth/src/plugins/jwt/index.ts#L134-L141 The current implementation first queries all keys in the database, and if none are found, it proceeds to create a new one. This introduces a race condition: two (or more) concurrent requests may both determine that no keys exist before either has inserted one, resulting in multiple keys being created. While this doesn’t cause a critical failure (you just end up with two keys), it is still an unintended and inefficient behavior that can be improved. A similar race condition can occur when signing a token: https://github.com/better-auth/better-auth/blob/cd567659b8b6c78bf90843fd7218446a4ca6aa9b/packages/better-auth/src/plugins/jwt/sign.ts#L54-L61 JWKs are created lazily (just like the `/jwks` endpoint) if a token is requested but no JWK exists, a new one is generated. However, multiple concurrent requests might all evaluate the last known key as undefined, again leading to duplicate key creation. Addressing this issue is non-trivial and will likely depend on the database dialect. Ideally, the existence check should be optimized to query specifically for one valid key rather than fetching all keys (getting all keys in the database with all fields is not very optimized just to check if one key exists). The operation should also be wrapped in a transaction or similar concurrency-safe mechanism to ensure that only one JWK can be inserted at a time. Depending on the database, this could be achieved with a row-level or table-level lock, or by leveraging unique constraints and catching duplicate-insert errors to enforce atomicity without locking the entire table. ### What version of Better Auth are you using? 1.3.34 ### System info ```bash { "system": { "platform": "darwin", "arch": "arm64", "version": "Darwin Kernel Version 24.6.0: Mon Aug 11 21:15:09 PDT 2025; root:xnu-11417.140.69.701.11~1/RELEASE_ARM64_T6041", "release": "24.6.0", "cpuCount": 16, "cpuModel": "Apple M4 Max", "totalMemory": "64.00 GB", "freeMemory": "0.16 GB" }, "node": { "version": "v20.15.1", "env": "development" }, "packageManager": { "name": "npm", "version": "10.8.2" }, "frameworks": [ { "name": "react", "version": "^19.1.0" } ], "databases": null, "betterAuth": { "version": "Unknown", "config": null } } ``` ### Which area(s) are affected? (Select all that apply) Backend ### Auth config (if applicable) ```typescript ``` ### Additional context _No response_
GiteaMirror added the lockedbug labels 2026-04-17 18:45:23 -05:00
Author
Owner

@dosubot[bot] commented on GitHub (Oct 29, 2025):

Hi @luist18! I'm Dosu and I’m helping the better-auth team.

You're right—there's a real race condition in the current JWK creation logic. Both the /jwks endpoint and token signing flow check for existing keys and create a new one if none are found, but this check-then-create pattern isn't atomic. If multiple requests hit at the same time, you can end up with duplicate keys in the database because there's no transaction, lock, or unique constraint to prevent it. This is confirmed in the code for both the endpoint and the signing logic, and the adapter's createJwk just inserts a new row without any concurrency control or deduplication logic (see code).

To prevent this, you should wrap the existence check and creation in a transaction if your database supports it, or enforce a unique constraint on a field that identifies the "active" JWK (for example, a boolean flag or a timestamp). Then, handle duplicate key errors gracefully—if a duplicate is detected, fetch the existing key instead of creating a new one. This pattern is used elsewhere in Better Auth for deduplication under concurrency (like user sign-up with a unique index on email) and is the recommended approach for this kind of problem (see discussion).

If you're using a database that supports transactions or upserts, you can implement this logic in your adapter. If not, you may need to add a retry loop or use a distributed lock. The key is to ensure that only one JWK can be created when none exist, even under concurrent requests.

To reply, just mention @dosu.


How did I do? Good | Irrelevant | Incorrect | Verbose | Hallucination | Report 🐛 | Other  Chat with Dosu Join Discord Share on X

<!-- gh-comment-id:3462638269 --> @dosubot[bot] commented on GitHub (Oct 29, 2025): <!-- Greeting --> Hi @luist18! I'm [Dosu](https://go.dosu.dev/dosubot) and I’m helping the better-auth team. <!-- Answer --> You're right—there's a real race condition in the current JWK creation logic. Both the `/jwks` endpoint and token signing flow check for existing keys and create a new one if none are found, but this check-then-create pattern isn't atomic. If multiple requests hit at the same time, you can end up with duplicate keys in the database because there's no transaction, lock, or unique constraint to prevent it. This is confirmed in the code for both the endpoint and the signing logic, and the adapter's `createJwk` just inserts a new row without any concurrency control or deduplication logic [(see code)](https://github.com/better-auth/better-auth/blob/e665c4c50829c4add1b40c531109dc6897df5e6d/packages/better-auth/src/plugins/jwt/index.ts). To prevent this, you should wrap the existence check and creation in a transaction if your database supports it, or enforce a unique constraint on a field that identifies the "active" JWK (for example, a boolean flag or a timestamp). Then, handle duplicate key errors gracefully—if a duplicate is detected, fetch the existing key instead of creating a new one. This pattern is used elsewhere in Better Auth for deduplication under concurrency (like user sign-up with a unique index on email) and is the recommended approach for this kind of problem [(see discussion)](https://github.com/better-auth/better-auth/issues/4039#issuecomment-3193450348). If you're using a database that supports transactions or upserts, you can implement this logic in your adapter. If not, you may need to add a retry loop or use a distributed lock. The key is to ensure that only one JWK can be created when none exist, even under concurrent requests. <!-- Dosu Comment Footer --> *To reply, just mention [@dosu](https://go.dosu.dev/dosubot).* --- <sup>How did I do? [Good](https://app.dosu.dev/response-feedback/40427f93-e9d2-40c3-9dac-6a8241871b92?feedback_type=great_response) | [Irrelevant](https://app.dosu.dev/response-feedback/40427f93-e9d2-40c3-9dac-6a8241871b92?feedback_type=irrelevant_answer) | [Incorrect](https://app.dosu.dev/response-feedback/40427f93-e9d2-40c3-9dac-6a8241871b92?feedback_type=incorrect_sources) | [Verbose](https://app.dosu.dev/response-feedback/40427f93-e9d2-40c3-9dac-6a8241871b92?feedback_type=too_verbose) | [Hallucination](https://app.dosu.dev/response-feedback/40427f93-e9d2-40c3-9dac-6a8241871b92?feedback_type=hallucination) | [Report 🐛](https://app.dosu.dev/response-feedback/40427f93-e9d2-40c3-9dac-6a8241871b92?feedback_type=bug_report) | [Other](https://app.dosu.dev/response-feedback/40427f93-e9d2-40c3-9dac-6a8241871b92?feedback_type=other)</sup>&nbsp;&nbsp;[![Chat with Dosu](https://dosu.dev/dosu-chat-badge.svg)](https://app.dosu.dev/cdda13d9-dd27-4d31-b09a-5d8bec92de21/ask?utm_source=github)&nbsp;[![Join Discord](https://img.shields.io/badge/join-5865F2?logo=discord&logoColor=white&label=)](https://go.dosu.dev/discord-bot)&nbsp;[![Share on X](https://img.shields.io/badge/X-share-black)](https://twitter.com/intent/tweet?text=%40dosu_ai%20helped%20me%20solve%20this%20issue!&url=https%3A//github.com/better-auth/better-auth/issues/5663)
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: github-starred/better-auth#27640