[GH-ISSUE #7819] OAuth Provider plugin assumes Date objects from DB without validation with Drizzle #19547

Closed
opened 2026-04-15 18:47:31 -05:00 by GiteaMirror · 4 comments
Owner

Originally created by @gustavovalverde on GitHub (Feb 6, 2026).
Original GitHub issue: https://github.com/better-auth/better-auth/issues/7819

Originally assigned to: @bytaesu on GitHub.

Is this suited for github?

  • Yes, this is suited for github

To Reproduce

  1. Set up better-auth with the Drizzle adapter + SQLite + oauthProvider plugin
  2. Define the Drizzle schema for oauth_access_token using text("expires_at") or integer("expires_at", { mode: "timestamp" }) for date columns
  3. Complete an OAuth authorization flow (authorization code grant without resource parameter, so opaque tokens are created)
  4. The callback to the relying party calls the /api/auth/oauth2/userinfo endpoint
  5. validateOpaqueAccessToken reads the access token from the DB and calls accessToken.expiresAt.getTime()crashes with TypeError

Both text() and integer(..., { mode: "timestamp" }) are reasonable Drizzle column types for dates — only mode: "timestamp_ms" happens to work because it's the only mode that returns Date objects with millisecond precision, which is what the plugin assumes without checking.

Current vs. Expected behavior

Current behavior:

The oauth-provider plugin calls .getTime() directly on values read from the database (at least 8 call sites across introspect.ts, register.ts, token.ts) without any type guard or coercion. This creates a hard dependency on the Drizzle schema using exactly integer('col', { mode: 'timestamp_ms' }) — any other column type for dates crashes at runtime:

TypeError: accessToken.expiresAt.getTime is not a function

The Drizzle adapter doesn't set supportsDates, so it defaults to true — meaning the adapter framework skips Date↔string conversion and trusts the ORM. But whether Drizzle actually returns Date objects depends entirely on how the user defined their schema. There's no validation that this implicit contract is met.

Expected behavior:

Either:

  • (a) The plugin should defensively coerce values to Date before calling .getTime() (e.g., new Date(value).getTime()), or
  • (b) The Drizzle adapter should explicitly set supportsDates based on the database type (like the Kysely adapter does — it sets false for SQLite), or
  • (c) A dev-mode warning should detect when date fields return non-Date values

What version of Better Auth are you using?

1.5.0-beta.8

System info

{
  "system": {
    "platform": "darwin",
    "arch": "arm64",
    "version": "Darwin Kernel Version 25.2.0",
    "cpuModel": "Apple M4 Pro"
  },
  "node": { "version": "v24.12.0" },
  "frameworks": [
    { "name": "next", "version": "16.1.1" },
    { "name": "react", "version": "19.2.3" }
  ],
  "databases": [
    { "name": "@libsql/client", "version": "0.15.15" },
    { "name": "drizzle", "version": "0.45.1" }
  ],
  "betterAuth": { "version": "1.5.0-beta.8" }
}

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

Backend, Package

Auth config (if applicable)

import { betterAuth } from "better-auth";
import { oauthProvider } from "@better-auth/oauth-provider";

export const auth = betterAuth({
  database: drizzle(db, { schema }), // SQLite via @libsql/client
  plugins: [
    oauthProvider({
      // ... provider config
    }),
  ],
});

Additional context

There are three contributing factors:

1. Drizzle adapter doesn't set supportsDates for SQLite

The Kysely adapter correctly sets supportsDates: false for SQLite:

// kysely-adapter.ts:624-626
supportsDates:
    config?.type === "sqlite" || config?.type === "mssql" || !config?.type
        ? false : true,

The Drizzle adapter omits supportsDates entirely, inheriting the default true. This is technically correct when the schema uses integer + timestamp_ms (Drizzle returns proper Date objects), but creates a brittle implicit contract that nothing validates.

2. Plugin code calls .getTime() without type guards

At least 8 call sites in the oauth-provider plugin assume Date objects without checking:

  • introspect.ts:239accessToken.expiresAt.getTime()
  • introspect.ts:240accessToken.createdAt.getTime()
  • introspect.ts:326refreshToken.expiresAt.getTime()
  • introspect.ts:327refreshToken.createdAt.getTime()
  • register.ts:359expiresAt.getTime()
  • register.ts:362createdAt.getTime()
  • token.ts:134session.createdAt.getTime()
Originally created by @gustavovalverde on GitHub (Feb 6, 2026). Original GitHub issue: https://github.com/better-auth/better-auth/issues/7819 Originally assigned to: @bytaesu on GitHub. ### Is this suited for github? - [x] Yes, this is suited for github ### To Reproduce 1. Set up better-auth with the Drizzle adapter + SQLite + `oauthProvider` plugin 2. Define the Drizzle schema for `oauth_access_token` using `text("expires_at")` or `integer("expires_at", { mode: "timestamp" })` for date columns 3. Complete an OAuth authorization flow (authorization code grant without `resource` parameter, so opaque tokens are created) 4. The callback to the relying party calls the `/api/auth/oauth2/userinfo` endpoint 5. `validateOpaqueAccessToken` reads the access token from the DB and calls `accessToken.expiresAt.getTime()` — **crashes with `TypeError`** Both `text()` and `integer(..., { mode: "timestamp" })` are reasonable Drizzle column types for dates — only `mode: "timestamp_ms"` happens to work because it's the only mode that returns `Date` objects with millisecond precision, which is what the plugin assumes without checking. ### Current vs. Expected behavior **Current behavior:** The oauth-provider plugin calls `.getTime()` directly on values read from the database (at least 8 call sites across `introspect.ts`, `register.ts`, `token.ts`) without any type guard or coercion. This creates a hard dependency on the Drizzle schema using exactly `integer('col', { mode: 'timestamp_ms' })` — any other column type for dates crashes at runtime: ``` TypeError: accessToken.expiresAt.getTime is not a function ``` The Drizzle adapter doesn't set `supportsDates`, so it defaults to `true` — meaning the adapter framework skips Date↔string conversion and trusts the ORM. But whether Drizzle actually returns `Date` objects depends entirely on how the user defined their schema. There's no validation that this implicit contract is met. **Expected behavior:** Either: - (a) The plugin should defensively coerce values to `Date` before calling `.getTime()` (e.g., `new Date(value).getTime()`), or - (b) The Drizzle adapter should explicitly set `supportsDates` based on the database type (like the Kysely adapter does — it sets `false` for SQLite), or - (c) A dev-mode warning should detect when date fields return non-Date values ### What version of Better Auth are you using? 1.5.0-beta.8 ### System info ```bash { "system": { "platform": "darwin", "arch": "arm64", "version": "Darwin Kernel Version 25.2.0", "cpuModel": "Apple M4 Pro" }, "node": { "version": "v24.12.0" }, "frameworks": [ { "name": "next", "version": "16.1.1" }, { "name": "react", "version": "19.2.3" } ], "databases": [ { "name": "@libsql/client", "version": "0.15.15" }, { "name": "drizzle", "version": "0.45.1" } ], "betterAuth": { "version": "1.5.0-beta.8" } } ``` ### Which area(s) are affected? (Select all that apply) Backend, Package ### Auth config (if applicable) ```typescript import { betterAuth } from "better-auth"; import { oauthProvider } from "@better-auth/oauth-provider"; export const auth = betterAuth({ database: drizzle(db, { schema }), // SQLite via @libsql/client plugins: [ oauthProvider({ // ... provider config }), ], }); ``` ### Additional context There are three contributing factors: **1. Drizzle adapter doesn't set `supportsDates` for SQLite** The Kysely adapter correctly sets `supportsDates: false` for SQLite: ```typescript // kysely-adapter.ts:624-626 supportsDates: config?.type === "sqlite" || config?.type === "mssql" || !config?.type ? false : true, ``` The Drizzle adapter omits `supportsDates` entirely, inheriting the default `true`. This is technically correct when the schema uses `integer` + `timestamp_ms` (Drizzle returns proper `Date` objects), but creates a brittle implicit contract that nothing validates. **2. Plugin code calls `.getTime()` without type guards** At least 8 call sites in the oauth-provider plugin assume Date objects without checking: - `introspect.ts:239` — `accessToken.expiresAt.getTime()` - `introspect.ts:240` — `accessToken.createdAt.getTime()` - `introspect.ts:326` — `refreshToken.expiresAt.getTime()` - `introspect.ts:327` — `refreshToken.createdAt.getTime()` - `register.ts:359` — `expiresAt.getTime()` - `register.ts:362` — `createdAt.getTime()` - `token.ts:134` — `session.createdAt.getTime()`
GiteaMirror added the lockedbug labels 2026-04-15 18:47:31 -05:00
Author
Owner

@better-auth-agent[bot] commented on GitHub (Feb 6, 2026):

It appears you're experiencing a crash in the OAuth Provider plugin due to the handling of Date objects when using the Drizzle adapter with SQLite. The underlying issue is that the plugin assumes Date objects are returned from the database but does not validate this assumption. This leads to runtime errors when it tries to call methods on values that may not be Date objects.

Here are possible next steps to troubleshoot or resolve the issue:

  1. Coercion to Dates: As suggested in your description, you can implement a defensive check in the plugin to ensure values are converted to Date objects before calling .getTime() on them. This can be achieved by using something like new Date(value).getTime().

  2. Adjust Drizzle Adapter: It may also be beneficial to add validation in the Drizzle adapter regarding the supportsDates property, particularly setting this to false for SQLite databases, which would help prevent these assumptions.

  3. Open/Review Existing Issues: Since there are existing concerns regarding how Drizzle interacts with Better Auth, you may want to look for or raise a relevant issue on GitHub to ensure this is tracked for future updates. There seems to be a similar issue regarding date handling in various contexts, such as the timestamp and mode settings in Drizzle [1], [5].

  4. Community Feedback: Feel free to ask for advice from the Better Auth team or other community members. Tagging @bekacru or @ping__ in your query can help get insights or a more official response on the matter.

Would you like assistance with setting up a specific workaround or approaching the next steps?

If you need more help, tag @better-auth-agent in a comment so I can respond.

DiagramDiscordGitHub

Diagram Join Star

<!-- gh-comment-id:3858200245 --> @better-auth-agent[bot] commented on GitHub (Feb 6, 2026): It appears you're experiencing a crash in the OAuth Provider plugin due to the handling of `Date` objects when using the Drizzle adapter with SQLite. The underlying issue is that the plugin assumes `Date` objects are returned from the database but does not validate this assumption. This leads to runtime errors when it tries to call methods on values that may not be `Date` objects. Here are possible next steps to troubleshoot or resolve the issue: 1. **Coercion to Dates**: As suggested in your description, you can implement a defensive check in the plugin to ensure values are converted to `Date` objects before calling `.getTime()` on them. This can be achieved by using something like `new Date(value).getTime()`. 2. **Adjust Drizzle Adapter**: It may also be beneficial to add validation in the Drizzle adapter regarding the `supportsDates` property, particularly setting this to `false` for SQLite databases, which would help prevent these assumptions. 3. **Open/Review Existing Issues**: Since there are existing concerns regarding how Drizzle interacts with Better Auth, you may want to look for or raise a relevant issue on GitHub to ensure this is tracked for future updates. There seems to be a similar issue regarding date handling in various contexts, such as the `timestamp` and `mode` settings in Drizzle [[1]](https://github.com/better-auth/better-auth/issues/6995), [[5]](https://github.com/better-auth/better-auth/pull/1563). 4. **Community Feedback**: Feel free to ask for advice from the Better Auth team or other community members. Tagging @bekacru or @ping__ in your query can help get insights or a more official response on the matter. Would you like assistance with setting up a specific workaround or approaching the next steps? _If you need more help, tag @better-auth-agent in a comment so I can respond._ <!-- bot:webhook reply v1 --> [Diagram](https://repodiagrams.s3.eu-north-1.amazonaws.com/better-auth_ultra_detailed_interactive.html) • [Discord](https://discord.gg/better-auth) • [GitHub](https://github.com/better-auth/better-auth) [![Diagram](https://img.shields.io/badge/Diagram-2b3137?style=flat-square)](https://repodiagrams.s3.eu-north-1.amazonaws.com/better-auth_ultra_detailed_interactive.html) [![Join](https://img.shields.io/badge/join-5865F2?logo=discord&logoColor=white&style=flat-square)](https://discord.gg/better-auth) [![Star](https://img.shields.io/badge/star-181717?logo=github&logoColor=white&style=flat-square)](https://github.com/better-auth/better-auth)
Author
Owner

@bytaesu commented on GitHub (Feb 8, 2026):

Hi @gustavovalverde,

When generating the schema with @better-auth/cli generate, it will appear as below and is expected to work correctly 🤔

expiresAt: integer("expires_at", { mode: "timestamp_ms" }),
createdAt: integer("created_at", { mode: "timestamp_ms" }),
<!-- gh-comment-id:3868104594 --> @bytaesu commented on GitHub (Feb 8, 2026): Hi @gustavovalverde, When generating the schema with `@better-auth/cli generate`, it will appear as below and is expected to work correctly 🤔 ``` expiresAt: integer("expires_at", { mode: "timestamp_ms" }), createdAt: integer("created_at", { mode: "timestamp_ms" }), ```
Author
Owner

@gustavovalverde commented on GitHub (Feb 9, 2026):

When generating the schema with @better-auth/cli generate, it will appear as below and is expected to work correctly 🤔

That's correct @bytaesu but if you're using a custom schema, then the problem above arises. It's an edge case when this is done (as stated in the OP):

2. Define the Drizzle schema for oauth_access_token using text("expires_at") or integer("expires_at", { mode: "timestamp" }) for date columns

<!-- gh-comment-id:3870523628 --> @gustavovalverde commented on GitHub (Feb 9, 2026): > When generating the schema with `@better-auth/cli generate`, it will appear as below and is expected to work correctly 🤔 That's correct @bytaesu but if you're using a custom schema, then the problem above arises. It's an edge case when this is done (as stated in the OP): > 2\. Define the Drizzle schema for `oauth_access_token` using `text("expires_at")` or `integer("expires_at", { mode: "timestamp" })` for date columns
Author
Owner

@dvanmali commented on GitHub (Feb 12, 2026):

@himself65 @bytaesu This is an adapter issue not oauth-provider plugin issue

<!-- gh-comment-id:3892128154 --> @dvanmali commented on GitHub (Feb 12, 2026): @himself65 @bytaesu This is an adapter issue not oauth-provider plugin issue
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: github-starred/better-auth#19547