From 9f15d23f77be570723090142ee51a55596d91595 Mon Sep 17 00:00:00 2001
From: Maxwell <145994855+ping-maxwell@users.noreply.github.com>
Date: Wed, 3 Sep 2025 04:19:34 +1000
Subject: [PATCH] fix(bearer): certain sign-in endpoints won't give bearer
token v2 (#4330)
---
docs/content/docs/plugins/bearer.mdx | 17 +++-
.../better-auth/src/client/plugins/index.ts | 1 +
.../src/plugins/bearer/bearer.test.ts | 11 +++
.../better-auth/src/plugins/bearer/client.ts | 27 +++++++
.../better-auth/src/plugins/bearer/index.ts | 81 ++++++++++++++++++-
.../src/test-utils/test-instance.ts | 5 ++
6 files changed, 140 insertions(+), 2 deletions(-)
create mode 100644 packages/better-auth/src/plugins/bearer/client.ts
diff --git a/docs/content/docs/plugins/bearer.mdx b/docs/content/docs/plugins/bearer.mdx
index 2446c62d..f6036f2c 100644
--- a/docs/content/docs/plugins/bearer.mdx
+++ b/docs/content/docs/plugins/bearer.mdx
@@ -22,6 +22,15 @@ export const auth = betterAuth({
});
```
+And in your auth client as well:
+
+```ts title="auth-client.ts"
+import { bearerClient } from "better-auth/client/plugins";
+export const authClient = createAuthClient({
+ plugins: [bearerClient()]
+});
+```
+
## How to Use Bearer Tokens
### 1. Obtain the Bearer Token
@@ -57,7 +66,6 @@ export const authClient = createAuthClient({
});
```
-
You may want to clear the token based on the response status code or other conditions:
### 2. Configure the Auth Client
@@ -137,3 +145,10 @@ export async function handler(req, res) {
## Options
**requireSignature** (boolean): Require the token to be signed. Default: `false`.
+
+**cookieName** (string): Custom cookie name for the temporary bearer token confirmation cookie. Default: `"bearer-token-confirmation"`.
+(For more information, [read here](https://github.com/better-auth/better-auth/pull/4330))
+
+
+ If this value is changed, make sure to update the client plugin's `cookieName` option.
+
\ No newline at end of file
diff --git a/packages/better-auth/src/client/plugins/index.ts b/packages/better-auth/src/client/plugins/index.ts
index 9c625be6..2399a94e 100644
--- a/packages/better-auth/src/client/plugins/index.ts
+++ b/packages/better-auth/src/client/plugins/index.ts
@@ -20,5 +20,6 @@ export * from "../../plugins/api-key/client";
export * from "../../plugins/one-time-token/client";
export * from "../../plugins/siwe/client";
export * from "../../plugins/device-authorization/client";
+export * from "../../plugins/bearer/client";
export type * from "@simplewebauthn/server";
export * from "../../plugins/last-login-method/client";
diff --git a/packages/better-auth/src/plugins/bearer/bearer.test.ts b/packages/better-auth/src/plugins/bearer/bearer.test.ts
index b82fd09d..df8bb9ff 100644
--- a/packages/better-auth/src/plugins/bearer/bearer.test.ts
+++ b/packages/better-auth/src/plugins/bearer/bearer.test.ts
@@ -75,4 +75,15 @@ describe("bearer", async () => {
});
expect(session.data?.session).toBeDefined();
});
+
+ it("should work with social sign-in", async () => {
+ const session = await client.signIn.social({
+ provider: "google",
+ fetchOptions: {
+ headers: {
+ Authorization: `Bearer ${token}`,
+ },
+ },
+ });
+ });
});
diff --git a/packages/better-auth/src/plugins/bearer/client.ts b/packages/better-auth/src/plugins/bearer/client.ts
new file mode 100644
index 00000000..54f3a3bb
--- /dev/null
+++ b/packages/better-auth/src/plugins/bearer/client.ts
@@ -0,0 +1,27 @@
+import type { BetterAuthClientPlugin } from "../../types";
+
+export type BearerClientOptions = {
+ /**
+ * Custom cookie name for the temporary bearer token confirmation cookie.
+ *
+ * @default "bearer-token-confirmation"
+ */
+ cookieName?: string;
+};
+
+export const bearerClient = (options?: BearerClientOptions) => {
+ return {
+ id: "bearer",
+ getActions($fetch) {
+ if (typeof document === "undefined") return {};
+ const cookie = document.cookie;
+ const cookieName = options?.cookieName || "bearer-token-confirmation";
+ if (cookie.includes(`${cookieName}=true`)) {
+ // This will hit the endpoint which would grab the bearer token cookie if it exists, then delete said cookie
+ // It will then return the bearer token in the response which should be caught on the authClient's `onSuccess` hook
+ $fetch("/get-bearer-token");
+ }
+ return {};
+ },
+ } satisfies BetterAuthClientPlugin;
+};
diff --git a/packages/better-auth/src/plugins/bearer/index.ts b/packages/better-auth/src/plugins/bearer/index.ts
index add11d87..abac230d 100644
--- a/packages/better-auth/src/plugins/bearer/index.ts
+++ b/packages/better-auth/src/plugins/bearer/index.ts
@@ -1,7 +1,7 @@
import { serializeSignedCookie } from "better-call";
import type { BetterAuthPlugin } from "../../types/plugins";
import { parseSetCookieHeader } from "../../cookies";
-import { createAuthMiddleware } from "../../api";
+import { createAuthEndpoint, createAuthMiddleware } from "../../api";
import { createHMAC } from "@better-auth/utils/hmac";
interface BearerOptions {
@@ -13,14 +13,79 @@ interface BearerOptions {
* @default false
*/
requireSignature?: boolean;
+ /**
+ * Custom cookie name for the temporary bearer token confirmation cookie.
+ *
+ * @default "bearer-token-confirmation"
+ */
+ cookieName?: string;
}
/**
* Converts bearer token to session cookie
*/
export const bearer = (options?: BearerOptions) => {
+ const bearerConfirmationCookieName =
+ options?.cookieName || "bearer-token-confirmation";
return {
id: "bearer",
+ endpoints: {
+ getBearerToken: createAuthEndpoint(
+ "/get-bearer-token",
+ {
+ method: "GET",
+ metadata: {
+ client: false,
+ },
+ requireHeaders: true,
+ },
+ async (ctx) => {
+ const cookieString = ctx.headers.get("cookie");
+ if (!cookieString) {
+ return ctx.json({
+ success: false,
+ });
+ }
+ const cookies = cookieString
+ .split(";")
+ .map((cookie) => cookie.trim());
+ const foundBearerToken = cookies.find((cookie) =>
+ cookie.startsWith(`${bearerConfirmationCookieName}=`),
+ );
+ const foundSessionToken = cookies.find((cookie) =>
+ cookie.startsWith(ctx.context.authCookies.sessionToken.name),
+ );
+ if (foundBearerToken && foundSessionToken) {
+ const setCookie = foundSessionToken.split("=")[1];
+ const parsedCookies = parseSetCookieHeader(setCookie);
+ const cookieName = ctx.context.authCookies.sessionToken.name;
+ const sessionCookie = parsedCookies.get(cookieName);
+ if (
+ !sessionCookie ||
+ !sessionCookie.value ||
+ sessionCookie["max-age"] === 0
+ ) {
+ return;
+ }
+ const token = sessionCookie.value;
+ ctx.setHeader("set-auth-token", token);
+ // Delete the confirmation cookie
+ ctx.setCookie(bearerConfirmationCookieName, "", {
+ httpOnly: false,
+ sameSite: "strict",
+ maxAge: 0,
+ expires: new Date(0),
+ });
+ return ctx.json({
+ success: true,
+ });
+ }
+ return ctx.json({
+ success: false,
+ });
+ },
+ ),
+ },
hooks: {
before: [
{
@@ -114,6 +179,20 @@ export const bearer = (options?: BearerOptions) => {
.filter(Boolean),
);
headersSet.add("set-auth-token");
+ const location =
+ ctx.context.responseHeaders?.get("location") ||
+ ctx.context.responseHeaders?.get("Location");
+ // If location exists, it likely means the authClient isn't able to pick up the bearer token.
+ // We will store a "bearer-token-confirmation" cookie so that when the authClient loads it can hit the
+ // `/get-bearer-token` endpoint and check for it, then delete it and return the bearer token.
+ if (location) {
+ // set a temporary cookie that will be used to get the bearer token
+ ctx.setCookie(bearerConfirmationCookieName, "true", {
+ httpOnly: false, // Needs to be read on the client side
+ sameSite: "strict",
+ secure: true,
+ });
+ }
ctx.setHeader("set-auth-token", token);
ctx.setHeader(
"Access-Control-Expose-Headers",
diff --git a/packages/better-auth/src/test-utils/test-instance.ts b/packages/better-auth/src/test-utils/test-instance.ts
index 44403779..0ea24d5b 100644
--- a/packages/better-auth/src/test-utils/test-instance.ts
+++ b/packages/better-auth/src/test-utils/test-instance.ts
@@ -16,6 +16,7 @@ import { MongoClient } from "mongodb";
import { mongodbAdapter } from "../adapters/mongodb-adapter";
import { createPool } from "mysql2/promise";
import { bearer } from "../plugins";
+import { bearerClient } from "../client/plugins";
const cleanupSet = new Set();
@@ -247,6 +248,10 @@ export async function getTestInstance<
const client = createAuthClient({
...(config?.clientOptions as C extends undefined ? {} : C),
+ plugins: [
+ bearerClient(),
+ ...((config?.clientOptions?.plugins as C["plugins"]) || []),
+ ],
baseURL: getBaseURL(
options?.baseURL || "http://localhost:" + (config?.port || 3000),
options?.basePath || "/api/auth",