mirror of
https://github.com/LukeHagar/better-auth.git
synced 2025-12-10 04:19:32 +00:00
fix(bearer): certain sign-in endpoints won't give bearer token v2 (#4330)
This commit is contained in:
@@ -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
|
## How to Use Bearer Tokens
|
||||||
|
|
||||||
### 1. Obtain the Bearer Token
|
### 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:
|
You may want to clear the token based on the response status code or other conditions:
|
||||||
|
|
||||||
### 2. Configure the Auth Client
|
### 2. Configure the Auth Client
|
||||||
@@ -137,3 +145,10 @@ export async function handler(req, res) {
|
|||||||
## Options
|
## Options
|
||||||
|
|
||||||
**requireSignature** (boolean): Require the token to be signed. Default: `false`.
|
**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>
|
||||||
@@ -20,5 +20,6 @@ export * from "../../plugins/api-key/client";
|
|||||||
export * from "../../plugins/one-time-token/client";
|
export * from "../../plugins/one-time-token/client";
|
||||||
export * from "../../plugins/siwe/client";
|
export * from "../../plugins/siwe/client";
|
||||||
export * from "../../plugins/device-authorization/client";
|
export * from "../../plugins/device-authorization/client";
|
||||||
|
export * from "../../plugins/bearer/client";
|
||||||
export type * from "@simplewebauthn/server";
|
export type * from "@simplewebauthn/server";
|
||||||
export * from "../../plugins/last-login-method/client";
|
export * from "../../plugins/last-login-method/client";
|
||||||
|
|||||||
@@ -75,4 +75,15 @@ describe("bearer", async () => {
|
|||||||
});
|
});
|
||||||
expect(session.data?.session).toBeDefined();
|
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}`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
27
packages/better-auth/src/plugins/bearer/client.ts
Normal file
27
packages/better-auth/src/plugins/bearer/client.ts
Normal 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;
|
||||||
|
};
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import { serializeSignedCookie } from "better-call";
|
import { serializeSignedCookie } from "better-call";
|
||||||
import type { BetterAuthPlugin } from "../../types/plugins";
|
import type { BetterAuthPlugin } from "../../types/plugins";
|
||||||
import { parseSetCookieHeader } from "../../cookies";
|
import { parseSetCookieHeader } from "../../cookies";
|
||||||
import { createAuthMiddleware } from "../../api";
|
import { createAuthEndpoint, createAuthMiddleware } from "../../api";
|
||||||
import { createHMAC } from "@better-auth/utils/hmac";
|
import { createHMAC } from "@better-auth/utils/hmac";
|
||||||
|
|
||||||
interface BearerOptions {
|
interface BearerOptions {
|
||||||
@@ -13,14 +13,79 @@ interface BearerOptions {
|
|||||||
* @default false
|
* @default false
|
||||||
*/
|
*/
|
||||||
requireSignature?: boolean;
|
requireSignature?: boolean;
|
||||||
|
/**
|
||||||
|
* Custom cookie name for the temporary bearer token confirmation cookie.
|
||||||
|
*
|
||||||
|
* @default "bearer-token-confirmation"
|
||||||
|
*/
|
||||||
|
cookieName?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Converts bearer token to session cookie
|
* Converts bearer token to session cookie
|
||||||
*/
|
*/
|
||||||
export const bearer = (options?: BearerOptions) => {
|
export const bearer = (options?: BearerOptions) => {
|
||||||
|
const bearerConfirmationCookieName =
|
||||||
|
options?.cookieName || "bearer-token-confirmation";
|
||||||
return {
|
return {
|
||||||
id: "bearer",
|
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: {
|
hooks: {
|
||||||
before: [
|
before: [
|
||||||
{
|
{
|
||||||
@@ -114,6 +179,20 @@ export const bearer = (options?: BearerOptions) => {
|
|||||||
.filter(Boolean),
|
.filter(Boolean),
|
||||||
);
|
);
|
||||||
headersSet.add("set-auth-token");
|
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("set-auth-token", token);
|
||||||
ctx.setHeader(
|
ctx.setHeader(
|
||||||
"Access-Control-Expose-Headers",
|
"Access-Control-Expose-Headers",
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import { MongoClient } from "mongodb";
|
|||||||
import { mongodbAdapter } from "../adapters/mongodb-adapter";
|
import { mongodbAdapter } from "../adapters/mongodb-adapter";
|
||||||
import { createPool } from "mysql2/promise";
|
import { createPool } from "mysql2/promise";
|
||||||
import { bearer } from "../plugins";
|
import { bearer } from "../plugins";
|
||||||
|
import { bearerClient } from "../client/plugins";
|
||||||
|
|
||||||
const cleanupSet = new Set<Function>();
|
const cleanupSet = new Set<Function>();
|
||||||
|
|
||||||
@@ -247,6 +248,10 @@ export async function getTestInstance<
|
|||||||
|
|
||||||
const client = createAuthClient({
|
const client = createAuthClient({
|
||||||
...(config?.clientOptions as C extends undefined ? {} : C),
|
...(config?.clientOptions as C extends undefined ? {} : C),
|
||||||
|
plugins: [
|
||||||
|
bearerClient(),
|
||||||
|
...((config?.clientOptions?.plugins as C["plugins"]) || []),
|
||||||
|
],
|
||||||
baseURL: getBaseURL(
|
baseURL: getBaseURL(
|
||||||
options?.baseURL || "http://localhost:" + (config?.port || 3000),
|
options?.baseURL || "http://localhost:" + (config?.port || 3000),
|
||||||
options?.basePath || "/api/auth",
|
options?.basePath || "/api/auth",
|
||||||
|
|||||||
Reference in New Issue
Block a user