fix(bearer): certain sign-in endpoints won't give bearer token v2 (#4330)

This commit is contained in:
Maxwell
2025-09-03 04:19:34 +10:00
committed by GitHub
parent b4f0c1a98a
commit 9f15d23f77
6 changed files with 140 additions and 2 deletions

View File

@@ -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))
<Callout type="warn">
If this value is changed, make sure to update the client plugin's `cookieName` option.
</Callout>

View File

@@ -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";

View File

@@ -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}`,
},
},
});
});
});

View File

@@ -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;
};

View File

@@ -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",

View File

@@ -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<Function>();
@@ -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",