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