chore: misc update

This commit is contained in:
Bereket Engida
2025-09-28 15:12:16 -07:00
committed by Alex Yang
parent ba916a6669
commit 9818fb85ec
28 changed files with 1423 additions and 1299 deletions

View File

@@ -103,19 +103,20 @@ export default function SignIn() {
toast.success("Successfully signed in");
router.push(getCallbackURL(params));
},
onError(context) {
toast.error(context.error.message);
},
},
);
});
}}
>
<div className="flex items-center justify-between w-full">
<span className="flex-1">
<div className="flex items-center justify-center w-full relative">
{loading ? (
<Loader2 size={16} className="animate-spin" />
) : (
"Login"
)}
</span>
{client.isLastUsedLoginMethod("email") && <LastUsedIndicator />}
</div>
</Button>

View File

@@ -1,7 +1,6 @@
"use client";
import * as React from "react";
import { ChevronLeftIcon, ChevronRightIcon } from "@radix-ui/react-icons";
import { DayPicker } from "react-day-picker";
import { cn } from "@/lib/utils";
@@ -59,10 +58,6 @@ function Calendar({
day_hidden: "invisible",
...classNames,
}}
components={{
IconLeft: ({ ...props }) => <ChevronLeftIcon className="h-4 w-4" />,
IconRight: ({ ...props }) => <ChevronRightIcon className="h-4 w-4" />,
}}
{...props}
/>
);

View File

@@ -19,11 +19,9 @@ import {
SelectValue,
} from "@/components/ui/select";
import { Textarea } from "@/components/ui/textarea";
import { Callout } from "@/components/ui/callout";
import * as React from "react";
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,

View File

@@ -1,7 +1,6 @@
"use client";
import * as React from "react";
import { ChevronLeft, ChevronRight } from "lucide-react";
import { DayPicker } from "react-day-picker";
import { cn } from "@/lib/utils";
@@ -59,14 +58,6 @@ function Calendar({
day_hidden: "invisible",
...classNames,
}}
components={{
IconLeft: ({ className, ...props }) => (
<ChevronLeft className={cn("size-4", className)} {...props} />
),
IconRight: ({ className, ...props }) => (
<ChevronRight className={cn("size-4", className)} {...props} />
),
}}
{...props}
/>
);

View File

@@ -2,12 +2,12 @@
"extends": "../../tsconfig.json",
"compilerOptions": {
"target": "esnext",
"module": "esnext",
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"strict": true,
"skipLibCheck": true,
"moduleResolution": "node16"
"moduleResolution": "node16",
"module": "Node16"
},
"include": ["./test/**/*"]
}

View File

@@ -19,15 +19,17 @@
"test": "turbo --filter \"./packages/*\" test",
"e2e:smoke": "turbo --filter \"./e2e/*\" e2e:smoke",
"e2e:integration": "turbo --filter \"./e2e/*\" e2e:integration",
"typecheck": "tsc -b --verbose --diagnostics"
"typecheck": "turbo --filter \"./packages/*\" typecheck"
},
"devDependencies": {
"@biomejs/biome": "2.2.4",
"@types/bun": "^1.2.20",
"@types/node": "^24.4.0",
"bumpp": "^10.2.3",
"tinyglobby": "^0.2.15",
"turbo": "^2.5.6",
"typescript": "catalog:",
"unbuild": "catalog:",
"vitest": "catalog:"
},
"resolutions": {

View File

@@ -1,17 +0,0 @@
# better-auth
## 1.3.4
### Patch Changes
- 2bd2fa9: Added support for listing organization members with pagination, sorting, and filtering, and improved client inference for additional organization fields. Also fixed date handling in rate limits and tokens, improved Notion OAuth user extraction, and ensured session is always set in context.
Organization
- Added listMembers API with pagination, sorting, and filtering.
- Added membersLimit param to getFullOrganization.
- Improved client inference for additional fields in organization schemas.
- Bug Fixes
- Fixed date handling by casting DB values to Date objects before using date methods.
- Fixed Notion OAuth to extract user info correctly.
- Ensured session is set in context when reading from cookie cach

View File

@@ -69,42 +69,7 @@ export const createInternalAdapter = (
session: parsed.session,
user,
}),
sessionTTL,
);
}),
);
}
async function refreshUserSessions(user: User) {
if (!secondaryStorage) return;
const listRaw = await secondaryStorage.get(`active-sessions-${user.id}`);
if (!listRaw) return;
const now = Date.now();
const list =
safeJSONParse<{ token: string; expiresAt: number }[]>(listRaw) || [];
const validSessions = list.filter((s) => s.expiresAt > now);
await Promise.all(
validSessions.map(async ({ token }) => {
const cached = await secondaryStorage.get(token);
if (!cached) return;
const parsed = safeJSONParse<{ session: Session; user: User }>(cached);
if (!parsed) return;
const sessionTTL = Math.max(
Math.floor(new Date(parsed.session.expiresAt).getTime() - now) / 1000,
0,
);
await secondaryStorage.set(
token,
JSON.stringify({
session: parsed.session,
user,
}),
sessionTTL,
Math.floor(sessionTTL),
);
}),
);

View File

@@ -1,6 +1,5 @@
import { describe, expect, it, vi } from "vitest";
import { getTestInstance } from "../test-utils/test-instance";
import { parseSetCookieHeader } from "../cookies";
import type { GoogleProfile } from "../social-providers";
import { DEFAULT_SECRET } from "../utils/constants";
import { getOAuth2Tokens } from "../oauth2";
@@ -43,7 +42,7 @@ vi.mock("../oauth2", async (importOriginal) => {
});
describe("oauth2 - email verification on link", async () => {
const { auth, client } = await getTestInstance({
const { auth, client, cookieSetter } = await getTestInstance({
socialProviders: {
google: {
clientId: "test",
@@ -66,25 +65,19 @@ describe("oauth2 - email verification on link", async () => {
const ctx = await auth.$context;
async function linkGoogleAccount() {
const oAuthHeaders = new Headers();
const signInRes = await client.signIn.social({
provider: "google",
callbackURL: "/",
fetchOptions: {
onSuccess: cookieSetter(oAuthHeaders),
},
});
const state = new URL(signInRes.data!.url!).searchParams.get("state") || "";
const headers = new Headers();
const cookies = parseSetCookieHeader(
(signInRes as any).headers?.["set-cookie"] || "",
);
headers.set(
"cookie",
`better-auth.state=${cookies.get("better-auth.state")?.value}`,
);
await client.$fetch("/callback/google", {
query: { state, code: "test_code" },
method: "GET",
headers,
headers: oAuthHeaders,
onError(context) {
expect(context.response.status).toBe(302);
},

View File

@@ -461,12 +461,14 @@ describe("Admin plugin", async () => {
});
it("should not allow banned user to sign in with social provider", async () => {
const headers = new Headers();
const res = await client.signIn.social(
{
provider: "google",
},
{
throw: true,
onSuccess: cookieSetter(headers),
},
);
const state = new URL(res.url!).searchParams.get("state");
@@ -476,6 +478,7 @@ describe("Admin plugin", async () => {
state,
code: "test",
},
headers,
method: "GET",
onError(context) {
expect(context.response.status).toBe(302);

View File

@@ -45,7 +45,7 @@ vi.mock("../../oauth2", async (importOriginal) => {
describe("anonymous", async () => {
const linkAccountFn = vi.fn();
const { customFetchImpl, auth, sessionSetter, testUser } =
const { customFetchImpl, auth, sessionSetter, testUser, cookieSetter } =
await getTestInstance({
plugins: [
anonymous({
@@ -114,9 +114,13 @@ describe("anonymous", async () => {
headers,
},
});
const singInRes = await client.signIn.social({
provider: "google",
callbackURL: "/dashboard",
fetchOptions: {
onSuccess: cookieSetter(headers),
},
});
const state = new URL(singInRes.data?.url || "").searchParams.get("state");
await client.$fetch("/callback/google", {

View File

@@ -225,7 +225,7 @@ describe("email-otp", async () => {
type: "email-verification",
});
vi.useFakeTimers();
await vi.advanceTimersByTimeAsync(1000 * 60 * 5);
await vi.advanceTimersByTimeAsync(1000 * 60 * 6);
const res = await client.emailOtp.verifyEmail({
email: testUser.email,
otp,

View File

@@ -20,7 +20,7 @@ describe("oauth2", async () => {
await server.stop();
});
const { customFetchImpl, auth } = await getTestInstance({
const { customFetchImpl, auth, cookieSetter } = await getTestInstance({
plugins: [
genericOAuth({
config: [
@@ -88,10 +88,7 @@ describe("oauth2", async () => {
headers,
onError(context) {
callbackURL = context.response.headers.get("location") || "";
newHeaders.set(
"cookie",
context.response.headers.get("Set-Cookie") || "",
);
cookieSetter(newHeaders)(context);
},
});
@@ -104,6 +101,9 @@ describe("oauth2", async () => {
providerId: "test",
callbackURL: "http://localhost:3000/dashboard",
newUserCallbackURL: "http://localhost:3000/new_user",
fetchOptions: {
onSuccess: cookieSetter(headers),
},
});
expect(signInRes.data).toMatchObject({
url: expect.stringContaining(`http://localhost:${port}/authorize`),
@@ -133,6 +133,9 @@ describe("oauth2", async () => {
providerId: "test",
callbackURL: "http://localhost:3000/dashboard",
newUserCallbackURL: "http://localhost:3000/new_user",
fetchOptions: {
onSuccess: cookieSetter(headers),
},
});
expect(signInRes.data).toMatchObject({
url: expect.stringContaining(`http://localhost:${port}/authorize`),
@@ -148,7 +151,7 @@ describe("oauth2", async () => {
headers: newHeaders,
},
});
console.log(session.data, newHeaders);
const ctx = await auth.$context;
const accounts = await ctx.internalAdapter.findAccounts(
session.data?.user.id!,
@@ -173,6 +176,9 @@ describe("oauth2", async () => {
providerId: "test",
callbackURL: "http://localhost:3000/dashboard",
newUserCallbackURL: "http://localhost:3000/new_user",
fetchOptions: {
onSuccess: cookieSetter(headers),
},
});
const { callbackURL } = await simulateOAuthFlow(
res.data?.url || "",
@@ -250,12 +256,13 @@ describe("oauth2", async () => {
}),
],
});
const headers = new Headers();
const authClient = createAuthClient({
plugins: [genericOAuthClient()],
baseURL: "http://localhost:3000",
fetchOptions: {
customFetchImpl,
onSuccess: cookieSetter(headers),
},
});
@@ -263,9 +270,11 @@ describe("oauth2", async () => {
providerId: "test2",
callbackURL: "http://localhost:3000/dashboard",
newUserCallbackURL: "http://localhost:3000/new_user",
fetchOptions: {
onSuccess: cookieSetter(headers),
},
});
expect(res.data?.url).toContain(`http://localhost:${port}/authorize`);
const headers = new Headers();
const { callbackURL } = await simulateOAuthFlow(
res.data?.url || "",
headers,
@@ -286,7 +295,7 @@ describe("oauth2", async () => {
userInfoResponse.statusCode = 200;
});
const { customFetchImpl } = await getTestInstance({
const { customFetchImpl, cookieSetter } = await getTestInstance({
plugins: [
genericOAuth({
config: [
@@ -302,7 +311,6 @@ describe("oauth2", async () => {
}),
],
});
const authClient = createAuthClient({
plugins: [genericOAuthClient()],
baseURL: "http://localhost:3000",
@@ -310,14 +318,16 @@ describe("oauth2", async () => {
customFetchImpl,
},
});
const headers = new Headers();
const res = await authClient.signIn.oauth2({
providerId: "test2",
callbackURL: "http://localhost:3000/dashboard",
errorCallbackURL: "http://localhost:3000/error",
fetchOptions: {
onSuccess: cookieSetter(headers),
},
});
expect(res.data?.url).toContain(`http://localhost:${port}/authorize`);
const headers = new Headers();
const { callbackURL } = await simulateOAuthFlow(
res.data?.url || "",
headers,
@@ -340,7 +350,7 @@ describe("oauth2", async () => {
userInfoResponse.statusCode = 200;
});
const { customFetchImpl } = await getTestInstance({
const { customFetchImpl, cookieSetter } = await getTestInstance({
plugins: [
genericOAuth({
config: [
@@ -364,15 +374,17 @@ describe("oauth2", async () => {
customFetchImpl,
},
});
const headers = new Headers();
const res = await authClient.signIn.oauth2({
providerId: "test2",
callbackURL: "http://localhost:3000/dashboard",
errorCallbackURL: "http://localhost:3000/error",
requestSignUp: true,
fetchOptions: {
onSuccess: cookieSetter(headers),
},
});
expect(res.data?.url).toContain(`http://localhost:${port}/authorize`);
const headers = new Headers();
const { callbackURL } = await simulateOAuthFlow(
res.data?.url || "",
headers,
@@ -391,7 +403,7 @@ describe("oauth2", async () => {
receivedHeaders = req.headers as Record<string, string>;
});
const { customFetchImpl } = await getTestInstance({
const { customFetchImpl, cookieSetter } = await getTestInstance({
plugins: [
genericOAuth({
config: [
@@ -407,12 +419,13 @@ describe("oauth2", async () => {
}),
],
});
const headers = new Headers();
const authClient = createAuthClient({
plugins: [genericOAuthClient()],
baseURL: "http://localhost:3000",
fetchOptions: {
customFetchImpl,
onSuccess: cookieSetter(headers),
},
});
@@ -420,10 +433,12 @@ describe("oauth2", async () => {
providerId: "test3",
callbackURL: "http://localhost:3000/dashboard",
newUserCallbackURL: "http://localhost:3000/new_user",
fetchOptions: {
onSuccess: cookieSetter(headers),
},
});
expect(res.data?.url).toContain(`http://localhost:${port}/authorize`);
const headers = new Headers();
await simulateOAuthFlow(res.data?.url || "", headers, customFetchImpl);
expect(receivedHeaders).toHaveProperty("x-custom-header");
@@ -432,7 +447,7 @@ describe("oauth2", async () => {
it("should delete oauth user with verification flow without password", async () => {
let token = "";
const { customFetchImpl } = await getTestInstance({
const { customFetchImpl, cookieSetter } = await getTestInstance({
user: {
deleteUser: {
enabled: true,
@@ -454,18 +469,22 @@ describe("oauth2", async () => {
}),
],
});
const headers = new Headers();
const client = createAuthClient({
plugins: [genericOAuthClient()],
baseURL: "http://localhost:3000",
fetchOptions: {
customFetchImpl,
onSuccess: cookieSetter(headers),
},
});
const signInRes = await client.signIn.oauth2({
providerId: "test",
callbackURL: "http://localhost:3000/dashboard",
newUserCallbackURL: "http://localhost:3000/new_user",
fetchOptions: {
onSuccess: cookieSetter(headers),
},
});
expect(signInRes.data).toMatchObject({
@@ -473,22 +492,22 @@ describe("oauth2", async () => {
redirect: true,
});
const { headers } = await simulateOAuthFlow(
const { headers: newHeaders } = await simulateOAuthFlow(
signInRes.data?.url || "",
new Headers(),
headers,
customFetchImpl,
);
const session = await client.getSession({
fetchOptions: {
headers,
headers: newHeaders,
},
});
expect(session.data).not.toBeNull();
const deleteRes = await client.deleteUser({
fetchOptions: {
headers,
headers: newHeaders,
},
});
@@ -501,7 +520,7 @@ describe("oauth2", async () => {
const deleteCallbackRes = await client.deleteUser({
token,
fetchOptions: {
headers,
headers: newHeaders,
},
});
expect(deleteCallbackRes.data).toMatchObject({
@@ -530,7 +549,7 @@ describe("oauth2", async () => {
userInfoResponse.statusCode = 200;
});
const { customFetchImpl, auth } = await getTestInstance({
const { customFetchImpl, auth, cookieSetter } = await getTestInstance({
plugins: [
genericOAuth({
config: [
@@ -545,12 +564,13 @@ describe("oauth2", async () => {
}),
],
});
const headers = new Headers();
const authClient = createAuthClient({
plugins: [genericOAuthClient()],
baseURL: "http://localhost:3000",
fetchOptions: {
customFetchImpl,
onSuccess: cookieSetter(headers),
},
});
@@ -563,7 +583,7 @@ describe("oauth2", async () => {
const { callbackURL: firstCallbackURL, headers: firstHeaders } =
await simulateOAuthFlow(
firstSignIn.data?.url || "",
new Headers(),
headers,
customFetchImpl,
);
@@ -606,7 +626,7 @@ describe("oauth2", async () => {
const { callbackURL: secondCallbackURL, headers: secondHeaders } =
await simulateOAuthFlow(
secondSignIn.data?.url || "",
new Headers(),
headers,
customFetchImpl,
);
@@ -629,7 +649,7 @@ describe("oauth2", async () => {
it("should handle custom getUserInfo returning numeric ID", async () => {
const numericId = 987654321;
const { customFetchImpl, auth } = await getTestInstance({
const { customFetchImpl, auth, cookieSetter } = await getTestInstance({
plugins: [
genericOAuth({
config: [
@@ -654,12 +674,13 @@ describe("oauth2", async () => {
}),
],
});
const headers = new Headers();
const authClient = createAuthClient({
plugins: [genericOAuthClient()],
baseURL: "http://localhost:3000",
fetchOptions: {
customFetchImpl,
onSuccess: cookieSetter(headers),
},
});
@@ -669,9 +690,9 @@ describe("oauth2", async () => {
newUserCallbackURL: "http://localhost:3000/new_user",
});
const { callbackURL, headers } = await simulateOAuthFlow(
const { callbackURL, headers: newHeaders } = await simulateOAuthFlow(
signInRes.data?.url || "",
new Headers(),
headers,
customFetchImpl,
);
@@ -679,7 +700,7 @@ describe("oauth2", async () => {
const session = await authClient.getSession({
fetchOptions: {
headers,
headers: newHeaders,
},
});
@@ -706,7 +727,7 @@ describe("oauth2", async () => {
userInfoResponse.statusCode = 200;
});
const { customFetchImpl, auth } = await getTestInstance({
const { customFetchImpl, auth, cookieSetter } = await getTestInstance({
plugins: [
genericOAuth({
config: [
@@ -729,12 +750,13 @@ describe("oauth2", async () => {
}),
],
});
const headers = new Headers();
const authClient = createAuthClient({
plugins: [genericOAuthClient()],
baseURL: "http://localhost:3000",
fetchOptions: {
customFetchImpl,
onSuccess: cookieSetter(headers),
},
});
@@ -744,9 +766,9 @@ describe("oauth2", async () => {
newUserCallbackURL: "http://localhost:3000/new_user",
});
const { callbackURL, headers } = await simulateOAuthFlow(
const { callbackURL, headers: newHeaders } = await simulateOAuthFlow(
signInRes.data?.url || "",
new Headers(),
headers,
customFetchImpl,
);
@@ -754,7 +776,7 @@ describe("oauth2", async () => {
const session = await authClient.getSession({
fetchOptions: {
headers,
headers: newHeaders,
},
});
@@ -781,7 +803,7 @@ describe("oauth2", async () => {
userInfoResponse.statusCode = 200;
});
const { customFetchImpl, auth } = await getTestInstance({
const { customFetchImpl, auth, cookieSetter } = await getTestInstance({
plugins: [
genericOAuth({
config: [
@@ -809,12 +831,13 @@ describe("oauth2", async () => {
}),
],
});
const headers = new Headers();
const authClient = createAuthClient({
plugins: [genericOAuthClient()],
baseURL: "http://localhost:3000",
fetchOptions: {
customFetchImpl,
onSuccess: cookieSetter(headers),
},
});
@@ -828,9 +851,9 @@ describe("oauth2", async () => {
// we missed the `activity:read_all`
expect(signInRes.data?.url).toContain("scope=read+activity");
const { callbackURL, headers } = await simulateOAuthFlow(
const { callbackURL, headers: newHeaders } = await simulateOAuthFlow(
signInRes.data?.url || "",
new Headers(),
headers,
customFetchImpl,
);
@@ -838,7 +861,7 @@ describe("oauth2", async () => {
const session = await authClient.getSession({
fetchOptions: {
headers,
headers: newHeaders,
},
});

View File

@@ -179,7 +179,7 @@ describe("lastLoginMethod", async () => {
});
it("should update the last login method in the database on subsequent logins with email and OAuth", async () => {
const { client, auth } = await getTestInstance({
const { client, auth, cookieSetter } = await getTestInstance({
plugins: [lastLoginMethod({ storeInDatabase: true })],
account: {
accountLinking: {
@@ -216,9 +216,13 @@ describe("lastLoginMethod", async () => {
await client.signOut();
const oAuthHeaders = new Headers();
const signInRes = await client.signIn.social({
provider: "google",
callbackURL: "/callback",
fetchOptions: {
onSuccess: cookieSetter(oAuthHeaders),
},
});
expect(signInRes.data).toMatchObject({
url: expect.stringContaining("google.com"),
@@ -232,6 +236,7 @@ describe("lastLoginMethod", async () => {
state,
code: "test",
},
headers: oAuthHeaders,
method: "GET",
onError(context) {
expect(context.response.status).toBe(302);

View File

@@ -21,7 +21,7 @@ describe("mcp", async () => {
const baseURL = `http://localhost:${port}`;
await tempServer.close();
const { auth, signInWithTestUser, customFetchImpl, testUser } =
const { auth, signInWithTestUser, customFetchImpl, testUser, cookieSetter } =
await getTestInstance({
baseURL,
plugins: [
@@ -161,7 +161,8 @@ describe("mcp", async () => {
});
it("should authenticate public client with PKCE only", async ({ expect }) => {
const { customFetchImpl: customFetchImplRP } = await getTestInstance({
const { customFetchImpl: customFetchImplRP, cookieSetter } =
await getTestInstance({
account: {
accountLinking: {
trustedProviders: ["test-public"],
@@ -191,7 +192,7 @@ describe("mcp", async () => {
customFetchImpl: customFetchImplRP,
},
});
const oAuthHeaders = new Headers();
const data = await client.signIn.oauth2(
{
providerId: "test-public",
@@ -199,6 +200,7 @@ describe("mcp", async () => {
},
{
throw: true,
onSuccess: cookieSetter(oAuthHeaders),
},
);
@@ -220,6 +222,7 @@ describe("mcp", async () => {
let callbackURL = "";
await client.$fetch(redirectURI, {
headers: oAuthHeaders,
onError(context: any) {
callbackURL = context.response.headers.get("Location") || "";
},
@@ -254,7 +257,8 @@ describe("mcp", async () => {
it("should still support confidential clients in MCP context", async ({
expect,
}) => {
const { customFetchImpl: customFetchImplRP } = await getTestInstance({
const { customFetchImpl: customFetchImplRP, cookieSetter } =
await getTestInstance({
account: {
accountLinking: {
trustedProviders: ["test-confidential"],
@@ -276,7 +280,7 @@ describe("mcp", async () => {
}),
],
});
const oAuthHeaders = new Headers();
const client = createAuthClient({
plugins: [genericOAuthClient()],
baseURL: "http://localhost:5001",
@@ -292,6 +296,7 @@ describe("mcp", async () => {
},
{
throw: true,
onSuccess: cookieSetter(oAuthHeaders),
},
);
@@ -311,6 +316,7 @@ describe("mcp", async () => {
let callbackURL = "";
await client.$fetch(redirectURI, {
headers: oAuthHeaders,
onError(context: any) {
callbackURL = context.response.headers.get("Location") || "";
},

View File

@@ -42,7 +42,7 @@ vi.mock("../../oauth2", async (importOriginal) => {
});
describe("oauth-proxy", async () => {
const { client } = await getTestInstance({
const { client, cookieSetter } = await getTestInstance({
plugins: [
oAuthProxy({
currentURL: "http://preview-localhost:3000",
@@ -57,6 +57,7 @@ describe("oauth-proxy", async () => {
});
it("should redirect to proxy url", async () => {
const headers = new Headers();
const res = await client.signIn.social(
{
provider: "google",
@@ -64,10 +65,12 @@ describe("oauth-proxy", async () => {
},
{
throw: true,
onSuccess: cookieSetter(headers),
},
);
const state = new URL(res.url!).searchParams.get("state");
await client.$fetch(`/callback/google?code=test&state=${state}`, {
headers,
onError(context) {
const location = context.response.headers.get("location") ?? "";
if (!location) {
@@ -83,7 +86,7 @@ describe("oauth-proxy", async () => {
});
it("shouldn't redirect to proxy url on same origin", async () => {
const { client } = await getTestInstance({
const { client, cookieSetter } = await getTestInstance({
plugins: [oAuthProxy()],
socialProviders: {
google: {
@@ -92,6 +95,7 @@ describe("oauth-proxy", async () => {
},
},
});
const headers = new Headers();
const res = await client.signIn.social(
{
provider: "google",
@@ -99,10 +103,12 @@ describe("oauth-proxy", async () => {
},
{
throw: true,
onSuccess: cookieSetter(headers),
},
);
const state = new URL(res.url!).searchParams.get("state");
await client.$fetch(`/callback/google?code=test&state=${state}`, {
headers,
onError(context) {
const location = context.response.headers.get("location");
if (!location) {
@@ -115,7 +121,7 @@ describe("oauth-proxy", async () => {
});
it("should proxy to the original request url", async () => {
const { client } = await getTestInstance({
const { client, cookieSetter } = await getTestInstance({
baseURL: "https://myapp.com",
plugins: [
oAuthProxy({
@@ -129,6 +135,7 @@ describe("oauth-proxy", async () => {
},
},
});
const headers = new Headers();
const res = await client.signIn.social(
{
provider: "google",
@@ -136,10 +143,12 @@ describe("oauth-proxy", async () => {
},
{
throw: true,
onSuccess: cookieSetter(headers),
},
);
const state = new URL(res.url!).searchParams.get("state");
await client.$fetch(`/callback/google?code=test&state=${state}`, {
headers,
onError(context) {
const location = context.response.headers.get("location");
if (!location) {
@@ -155,7 +164,7 @@ describe("oauth-proxy", async () => {
});
it("shouldn't redirect to proxy url on same origin", async () => {
const { client } = await getTestInstance({
const { client, cookieSetter } = await getTestInstance({
baseURL: "https://myapp.com",
plugins: [oAuthProxy()],
socialProviders: {
@@ -165,6 +174,7 @@ describe("oauth-proxy", async () => {
},
},
});
const headers = new Headers();
const res = await client.signIn.social(
{
provider: "google",
@@ -172,10 +182,12 @@ describe("oauth-proxy", async () => {
},
{
throw: true,
onSuccess: cookieSetter(headers),
},
);
const state = new URL(res.url!).searchParams.get("state");
await client.$fetch(`/callback/google?code=test&state=${state}`, {
headers,
onError(context) {
const location = context.response.headers.get("location");
if (!location) {

View File

@@ -24,6 +24,7 @@ describe("oidc", async () => {
auth: authorizationServer,
signInWithTestUser,
customFetchImpl,
cookieSetter,
testUser,
} = await getTestInstance({
baseURL: "http://localhost:3000",
@@ -140,6 +141,7 @@ describe("oidc", async () => {
customFetchImpl: customFetchImplRP,
},
});
const headers = new Headers();
const data = await client.signIn.oauth2(
{
providerId: "test",
@@ -147,6 +149,7 @@ describe("oidc", async () => {
},
{
throw: true,
onSuccess: cookieSetter(headers),
},
);
expect(data.url).toContain(
@@ -167,6 +170,7 @@ describe("oidc", async () => {
let callbackURL = "";
await client.$fetch(redirectURI, {
headers,
onError(context) {
callbackURL = context.response.headers.get("Location") || "";
},
@@ -209,6 +213,7 @@ describe("oidc", async () => {
customFetchImpl: customFetchImplRP,
},
});
const oAuthHeaders = new Headers();
const data = await client.signIn.oauth2(
{
providerId: "test",
@@ -216,6 +221,7 @@ describe("oidc", async () => {
},
{
throw: true,
onSuccess: cookieSetter(oAuthHeaders),
},
);
expect(data.url).toContain(
@@ -236,6 +242,8 @@ describe("oidc", async () => {
expect(redirectURI).toContain("/oauth2/authorize?");
expect(redirectURI).toContain("consent_code=");
expect(redirectURI).toContain("client_id=");
console.log({ newHeaders });
const res = await serverClient.oauth2.consent(
{
accept: true,
@@ -251,6 +259,7 @@ describe("oidc", async () => {
let callbackURL = "";
await client.$fetch(res.redirectURI, {
headers: oAuthHeaders,
onError(context) {
callbackURL = context.response.headers.get("Location") || "";
},
@@ -293,6 +302,7 @@ describe("oidc", async () => {
customFetchImpl: customFetchImplRP,
},
});
const oAuthHeaders = new Headers();
const data = await client.signIn.oauth2(
{
providerId: "test",
@@ -300,6 +310,7 @@ describe("oidc", async () => {
},
{
throw: true,
onSuccess: cookieSetter(oAuthHeaders),
},
);
expect(data.url).toContain(
@@ -338,6 +349,7 @@ describe("oidc", async () => {
);
let callbackURL = "";
await client.$fetch(redirectURI, {
headers: oAuthHeaders,
onError(context) {
callbackURL = context.response.headers.get("Location") || "";
},
@@ -443,7 +455,8 @@ describe("oidc storage", async () => {
};
}
// The RP (Relying Party) - the client application
const { customFetchImpl: customFetchImplRP } = await getTestInstance({
const { customFetchImpl: customFetchImplRP, cookieSetter } =
await getTestInstance({
account: {
accountLinking: {
trustedProviders: ["test"],
@@ -474,6 +487,7 @@ describe("oidc storage", async () => {
customFetchImpl: customFetchImplRP,
},
});
const oAuthHeaders = new Headers();
const data = await client.signIn.oauth2(
{
providerId: "test",
@@ -481,6 +495,7 @@ describe("oidc storage", async () => {
},
{
throw: true,
onSuccess: cookieSetter(oAuthHeaders),
},
);
expect(data.url).toContain(
@@ -501,6 +516,7 @@ describe("oidc storage", async () => {
let callbackURL = "";
await client.$fetch(redirectURI, {
headers: oAuthHeaders,
onError(context) {
callbackURL = context.response.headers.get("Location") || "";
},
@@ -601,7 +617,8 @@ describe("oidc-jwt", async () => {
}
// The RP (Relying Party) - the client application
const { customFetchImpl: customFetchImplRP } = await getTestInstance({
const { customFetchImpl: customFetchImplRP, cookieSetter } =
await getTestInstance({
account: {
accountLinking: {
trustedProviders: ["test"],
@@ -632,6 +649,7 @@ describe("oidc-jwt", async () => {
customFetchImpl: customFetchImplRP,
},
});
const oAuthHeaders = new Headers();
const data = await client.signIn.oauth2(
{
providerId: "test",
@@ -639,6 +657,7 @@ describe("oidc-jwt", async () => {
},
{
throw: true,
onSuccess: cookieSetter(oAuthHeaders),
},
);
expect(data.url).toContain(
@@ -659,6 +678,7 @@ describe("oidc-jwt", async () => {
let authToken = undefined;
let callbackURL = "";
await client.$fetch(redirectURI, {
headers: oAuthHeaders,
onError(context) {
callbackURL = context.response.headers.get("Location") || "";
authToken = context.response.headers.get("set-auth-token")!;

View File

@@ -4,14 +4,25 @@ import { sso } from ".";
import { OAuth2Server } from "oauth2-mock-server";
import { betterFetch } from "@better-fetch/fetch";
import { organization } from "../organization";
import { createAuthClient } from "../../client";
import { ssoClient } from "./client";
let server = new OAuth2Server();
describe("SSO", async () => {
const { auth, signInWithTestUser, customFetchImpl } = await getTestInstance({
const { auth, signInWithTestUser, customFetchImpl, cookieSetter } =
await getTestInstance({
plugins: [sso(), organization()],
});
const authClient = createAuthClient({
plugins: [ssoClient()],
baseURL: "http://localhost:3000",
fetchOptions: {
customFetchImpl,
},
});
beforeAll(async () => {
await server.issuer.keys.generate("RS256");
server.issuer.on;
@@ -56,7 +67,7 @@ describe("SSO", async () => {
});
if (!location) throw new Error("No redirect location found");
const newHeaders = new Headers();
let callbackURL = "";
await betterFetch(location, {
method: "GET",
@@ -64,10 +75,11 @@ describe("SSO", async () => {
headers,
onError(context) {
callbackURL = context.response.headers.get("location") || "";
cookieSetter(newHeaders)(context);
},
});
return callbackURL;
return { callbackURL, headers: newHeaders };
}
it("should register a new SSO provider", async () => {
@@ -141,60 +153,76 @@ describe("SSO", async () => {
});
it("should sign in with SSO provider with email matching", async () => {
const res = await auth.api.signInSSO({
body: {
const headers = new Headers();
const res = await authClient.signIn.sso({
email: "my-email@localhost.com",
callbackURL: "/dashboard",
fetchOptions: {
throw: true,
onSuccess: cookieSetter(headers),
},
});
expect(res.url).toContain("http://localhost:8080/authorize");
expect(res.url).toContain(
"redirect_uri=http%3A%2F%2Flocalhost%3A3000%2Fapi%2Fauth%2Fsso%2Fcallback%2Ftest",
);
const headers = new Headers();
const callbackURL = await simulateOAuthFlow(res.url, headers);
const { callbackURL } = await simulateOAuthFlow(res.url, headers);
expect(callbackURL).toContain("/dashboard");
});
it("should sign in with SSO provider with domain", async () => {
const res = await auth.api.signInSSO({
body: {
const headers = new Headers();
const res = await authClient.signIn.sso({
email: "my-email@test.com",
domain: "localhost.com",
callbackURL: "/dashboard",
fetchOptions: {
throw: true,
onSuccess: cookieSetter(headers),
},
});
expect(res.url).toContain("http://localhost:8080/authorize");
expect(res.url).toContain(
"redirect_uri=http%3A%2F%2Flocalhost%3A3000%2Fapi%2Fauth%2Fsso%2Fcallback%2Ftest",
);
const headers = new Headers();
const callbackURL = await simulateOAuthFlow(res.url, headers);
const { callbackURL } = await simulateOAuthFlow(res.url, headers);
expect(callbackURL).toContain("/dashboard");
});
it("should sign in with SSO provider with providerId", async () => {
const res = await auth.api.signInSSO({
body: {
const headers = new Headers();
const res = await authClient.signIn.sso({
providerId: "test",
callbackURL: "/dashboard",
fetchOptions: {
throw: true,
onSuccess: cookieSetter(headers),
},
});
expect(res.url).toContain("http://localhost:8080/authorize");
expect(res.url).toContain(
"redirect_uri=http%3A%2F%2Flocalhost%3A3000%2Fapi%2Fauth%2Fsso%2Fcallback%2Ftest",
);
const headers = new Headers();
const callbackURL = await simulateOAuthFlow(res.url, headers);
const { callbackURL } = await simulateOAuthFlow(res.url, headers);
expect(callbackURL).toContain("/dashboard");
});
});
describe("SSO disable implicit sign in", async () => {
const { auth, signInWithTestUser, customFetchImpl } = await getTestInstance({
const { auth, signInWithTestUser, customFetchImpl, cookieSetter } =
await getTestInstance({
plugins: [sso({ disableImplicitSignUp: true }), organization()],
});
const authClient = createAuthClient({
plugins: [ssoClient()],
baseURL: "http://localhost:3000",
fetchOptions: {
customFetchImpl,
},
});
beforeAll(async () => {
await server.issuer.keys.generate("RS256");
server.issuer.on;
@@ -239,7 +267,7 @@ describe("SSO disable implicit sign in", async () => {
});
if (!location) throw new Error("No redirect location found");
const newHeaders = new Headers(headers);
let callbackURL = "";
await betterFetch(location, {
method: "GET",
@@ -247,10 +275,11 @@ describe("SSO disable implicit sign in", async () => {
headers,
onError(context) {
callbackURL = context.response.headers.get("location") || "";
cookieSetter(newHeaders)(context);
},
});
return callbackURL;
return { callbackURL, headers: newHeaders };
}
it("should register a new SSO provider", async () => {
@@ -300,46 +329,59 @@ describe("SSO disable implicit sign in", async () => {
});
it("should not create user with SSO provider when sign ups are disabled", async () => {
const res = await auth.api.signInSSO({
body: {
const headers = new Headers();
const res = await authClient.signIn.sso({
email: "my-email@localhost.com",
callbackURL: "/dashboard",
fetchOptions: {
throw: true,
onSuccess: cookieSetter(headers),
},
});
expect(res.url).toContain("http://localhost:8080/authorize");
expect(res.url).toContain(
"redirect_uri=http%3A%2F%2Flocalhost%3A3000%2Fapi%2Fauth%2Fsso%2Fcallback%2Ftest",
);
const headers = new Headers();
const callbackURL = await simulateOAuthFlow(res.url, headers);
const { callbackURL } = await simulateOAuthFlow(res.url, headers);
expect(callbackURL).toContain(
"/api/auth/error/error?error=signup disabled",
);
});
it("should create user with SSO provider when sign ups are disabled but sign up is requested", async () => {
const res = await auth.api.signInSSO({
body: {
const headers = new Headers();
const res = await authClient.signIn.sso({
email: "my-email@localhost.com",
callbackURL: "/dashboard",
requestSignUp: true,
fetchOptions: {
throw: true,
onSuccess: cookieSetter(headers),
},
});
expect(res.url).toContain("http://localhost:8080/authorize");
expect(res.url).toContain(
"redirect_uri=http%3A%2F%2Flocalhost%3A3000%2Fapi%2Fauth%2Fsso%2Fcallback%2Ftest",
);
const headers = new Headers();
const callbackURL = await simulateOAuthFlow(res.url, headers);
const { callbackURL } = await simulateOAuthFlow(res.url, headers);
expect(callbackURL).toContain("/dashboard");
});
});
describe("provisioning", async (ctx) => {
const { auth, signInWithTestUser, customFetchImpl } = await getTestInstance({
const { auth, signInWithTestUser, customFetchImpl, cookieSetter } =
await getTestInstance({
plugins: [sso(), organization()],
});
const authClient = createAuthClient({
plugins: [ssoClient()],
baseURL: "http://localhost:3000",
fetchOptions: {
customFetchImpl,
},
});
beforeAll(async () => {
await server.issuer.keys.generate("RS256");
server.issuer.on;
@@ -367,12 +409,14 @@ describe("provisioning", async (ctx) => {
if (!location) throw new Error("No redirect location found");
let callbackURL = "";
const newHeaders = new Headers();
await betterFetch(location, {
method: "GET",
customFetchImpl: fetchImpl || customFetchImpl,
headers,
onError(context) {
callbackURL = context.response.headers.get("location") || "";
cookieSetter(newHeaders)(context);
},
});
@@ -429,18 +473,20 @@ describe("provisioning", async (ctx) => {
expect(provider).toMatchObject({
organizationId: organization?.id,
});
const res = await auth.api.signInSSO({
body: {
const newHeaders = new Headers();
const res = await authClient.signIn.sso({
email: "my-email@localhost.com",
callbackURL: "/dashboard",
fetchOptions: {
onSuccess: cookieSetter(newHeaders),
throw: true,
},
});
expect(res.url).toContain("http://localhost:8080/authorize");
expect(res.url).toContain(
"redirect_uri=http%3A%2F%2Flocalhost%3A3000%2Fapi%2Fauth%2Fsso%2Fcallback%2Ftest",
);
const newHeaders = new Headers();
const callbackURL = await simulateOAuthFlow(res.url, newHeaders);
expect(callbackURL).toContain("/dashboard");
const org = await auth.api.getFullOrganization({

View File

@@ -181,10 +181,8 @@ describe("Social Providers", async (c) => {
token.payload.name = "Test User";
token.payload.picture = "https://test.com/picture.png";
});
let state = "";
const headers = new Headers();
describe("signin", async () => {
async function simulateOAuthFlowRefresh(
authUrl: string,
headers: Headers,
@@ -221,15 +219,25 @@ describe("Social Providers", async (c) => {
url: expect.stringContaining("google.com"),
redirect: true,
});
state = new URL(signInRes.data!.url!).searchParams.get("state") || "";
});
it("should be able to sign in with social providers", async () => {
const headers = new Headers();
const signInRes = await client.signIn.social({
provider: "google",
callbackURL: "/callback",
newUserCallbackURL: "/welcome",
fetchOptions: {
onSuccess: cookieSetter(headers),
},
});
const state = new URL(signInRes.data!.url!).searchParams.get("state") || "";
await client.$fetch("/callback/google", {
query: {
state,
code: "test",
},
headers,
method: "GET",
onError(context) {
expect(context.response.status).toBe(302);
@@ -245,22 +253,27 @@ describe("Social Providers", async (c) => {
});
it("Should use callback URL if the user is already registered", async () => {
const headers = new Headers();
const signInRes = await client.signIn.social({
provider: "google",
callbackURL: "/callback",
newUserCallbackURL: "/welcome",
fetchOptions: {
onSuccess: cookieSetter(headers),
},
});
const state = new URL(signInRes.data!.url!).searchParams.get("state") || "";
expect(signInRes.data).toMatchObject({
url: expect.stringContaining("google.com"),
redirect: true,
});
state = new URL(signInRes.data!.url!).searchParams.get("state") || "";
await client.$fetch("/callback/google", {
query: {
state,
code: "test",
},
headers,
method: "GET",
onError(context) {
expect(context.response.status).toBe(302);
@@ -276,23 +289,25 @@ describe("Social Providers", async (c) => {
});
it("should be able to map profile to user", async () => {
const headers = new Headers();
const signInRes = await client.signIn.social({
provider: "google",
callbackURL: "/callback",
fetchOptions: {
onSuccess: cookieSetter(headers),
},
});
expect(signInRes.data).toMatchObject({
url: expect.stringContaining("google.com"),
redirect: true,
});
state = new URL(signInRes.data!.url!).searchParams.get("state") || "";
const headers = new Headers();
const profile = await client.$fetch("/callback/google", {
const state = new URL(signInRes.data!.url!).searchParams.get("state") || "";
await client.$fetch("/callback/google", {
query: {
state,
code: "test",
},
headers,
method: "GET",
onError: (c) => {
//TODO: fix this
@@ -335,22 +350,27 @@ describe("Social Providers", async (c) => {
});
it("should refresh the access token", async () => {
const headers = new Headers();
const signInRes = await client.signIn.social({
provider: "google",
callbackURL: "/callback",
newUserCallbackURL: "/welcome",
fetchOptions: {
onSuccess: cookieSetter(headers),
},
});
const headers = new Headers();
expect(signInRes.data).toMatchObject({
url: expect.stringContaining("google.com"),
redirect: true,
});
state = new URL(signInRes.data!.url!).searchParams.get("state") || "";
const state = new URL(signInRes.data!.url!).searchParams.get("state") || "";
await client.$fetch("/callback/google", {
query: {
state,
code: "test",
},
headers,
method: "GET",
onError(context) {
expect(context.response.status).toBe(302);
@@ -393,7 +413,6 @@ describe("Social Providers", async (c) => {
});
});
});
});
describe("Redirect URI", async () => {
it("should infer redirect uri", async () => {
const { client } = await getTestInstance({
@@ -454,7 +473,7 @@ describe("Redirect URI", async () => {
describe("Disable implicit signup", async () => {
it("Should not create user when implicit sign up is disabled", async () => {
const { client } = await getTestInstance({
const { client, cookieSetter } = await getTestInstance({
socialProviders: {
google: {
clientId: "test",
@@ -464,11 +483,14 @@ describe("Disable implicit signup", async () => {
},
},
});
const headers = new Headers();
const signInRes = await client.signIn.social({
provider: "google",
callbackURL: "/callback",
newUserCallbackURL: "/welcome",
fetchOptions: {
onSuccess: cookieSetter(headers),
},
});
expect(signInRes.data).toMatchObject({
url: expect.stringContaining("google.com"),
@@ -481,6 +503,7 @@ describe("Disable implicit signup", async () => {
state,
code: "test",
},
headers,
method: "GET",
onError(context) {
expect(context.response.status).toBe(302);
@@ -494,7 +517,7 @@ describe("Disable implicit signup", async () => {
});
it("Should create user when implicit sign up is disabled but it is requested", async () => {
const { client } = await getTestInstance({
const { client, cookieSetter } = await getTestInstance({
socialProviders: {
google: {
clientId: "test",
@@ -505,11 +528,15 @@ describe("Disable implicit signup", async () => {
},
});
const headers = new Headers();
const signInRes = await client.signIn.social({
provider: "google",
callbackURL: "/callback",
newUserCallbackURL: "/welcome",
requestSignUp: true,
fetchOptions: {
onSuccess: cookieSetter(headers),
},
});
expect(signInRes.data).toMatchObject({
url: expect.stringContaining("google.com"),
@@ -522,6 +549,7 @@ describe("Disable implicit signup", async () => {
state,
code: "test",
},
headers,
method: "GET",
onError(context) {
expect(context.response.status).toBe(302);
@@ -539,7 +567,8 @@ describe("Disable implicit signup", async () => {
describe("Disable signup", async () => {
it("Should not create user when sign up is disabled", async () => {
const { client } = await getTestInstance({
const headers = new Headers();
const { client, cookieSetter } = await getTestInstance({
socialProviders: {
google: {
clientId: "test",
@@ -554,6 +583,9 @@ describe("Disable signup", async () => {
provider: "google",
callbackURL: "/callback",
newUserCallbackURL: "/welcome",
fetchOptions: {
onSuccess: cookieSetter(headers),
},
});
expect(signInRes.data).toMatchObject({
url: expect.stringContaining("google.com"),
@@ -566,6 +598,7 @@ describe("Disable signup", async () => {
state,
code: "test",
},
headers,
method: "GET",
onError(context) {
expect(context.response.status).toBe(302);
@@ -590,6 +623,7 @@ describe("signin", async () => {
});
it("should allow user info override during sign in", async () => {
let state = "";
const headers = new Headers();
const { client, cookieSetter } = await getTestInstance({
database,
socialProviders: {
@@ -603,6 +637,9 @@ describe("signin", async () => {
const signInRes = await client.signIn.social({
provider: "google",
callbackURL: "/callback",
fetchOptions: {
onSuccess: cookieSetter(headers),
},
});
expect(signInRes.data).toMatchObject({
url: expect.stringContaining("google.com"),
@@ -610,13 +647,12 @@ describe("signin", async () => {
});
state = new URL(signInRes.data!.url!).searchParams.get("state") || "";
const headers = new Headers();
await client.$fetch("/callback/google", {
query: {
state,
code: "test",
},
headers,
method: "GET",
onError: (c) => {
cookieSetter(headers)(c as any);
@@ -634,6 +670,7 @@ describe("signin", async () => {
});
it("should allow user info override during sign in", async () => {
const headers = new Headers();
let state = "";
const { client, cookieSetter } = await getTestInstance(
{
@@ -654,6 +691,9 @@ describe("signin", async () => {
const signInRes = await client.signIn.social({
provider: "google",
callbackURL: "/callback",
fetchOptions: {
onSuccess: cookieSetter(headers),
},
});
expect(signInRes.data).toMatchObject({
url: expect.stringContaining("google.com"),
@@ -661,13 +701,12 @@ describe("signin", async () => {
});
state = new URL(signInRes.data!.url!).searchParams.get("state") || "";
const headers = new Headers();
await client.$fetch("/callback/google", {
query: {
state,
code: "test",
},
headers,
method: "GET",
onError: (c) => {
cookieSetter(headers)(c as any);
@@ -697,6 +736,9 @@ describe("updateAccountOnSignIn", async () => {
const signInRes = await client.signIn.social({
provider: "google",
callbackURL: "/callback",
fetchOptions: {
onSuccess: cookieSetter(headers),
},
});
expect(signInRes.data).toMatchObject({
url: expect.stringContaining("google.com"),
@@ -710,6 +752,7 @@ describe("updateAccountOnSignIn", async () => {
code: "test",
},
method: "GET",
headers,
onError(context) {
cookieSetter(headers)(context as any);
},
@@ -730,6 +773,9 @@ describe("updateAccountOnSignIn", async () => {
const signInRes2 = await client.signIn.social({
provider: "google",
callbackURL: "/callback",
fetchOptions: {
onSuccess: cookieSetter(headers),
},
});
expect(signInRes2.data).toMatchObject({
url: expect.stringContaining("google.com"),
@@ -743,6 +789,7 @@ describe("updateAccountOnSignIn", async () => {
state: state2,
code: "test",
},
headers,
method: "GET",
onError(context) {
cookieSetter(headers)(context as any);

View File

@@ -1,5 +1,4 @@
{
"extends": "../../tsconfig.json",
"extends": "../../tsconfig.json",
"compilerOptions": {
"rootDir": "./src",
@@ -7,6 +6,5 @@
"lib": ["esnext", "dom", "dom.iterable"],
"types": ["node", "bun"]
},
"include": ["src"],
"references": [{ "path": "../core" }]
"include": ["src"]
}

View File

@@ -5,10 +5,5 @@
"outDir": "./dist",
"lib": ["esnext", "dom", "dom.iterable"]
},
"references": [
{
"path": "../better-auth/tsconfig.json"
}
],
"include": ["src"]
}

View File

@@ -5,10 +5,5 @@
"outDir": "./dist",
"lib": ["esnext", "dom", "dom.iterable"]
},
"references": [
{
"path": "../better-auth/tsconfig.json"
}
],
"include": ["src"]
}

View File

@@ -1,18 +1,28 @@
import { afterAll, beforeAll, describe, expect, it } from "vitest";
import { getTestInstanceMemory as getTestInstance } from "better-auth/test";
import { sso } from ".";
import { OAuth2Server } from "oauth2-mock-server";
import { betterFetch } from "@better-fetch/fetch";
import { organization } from "better-auth/plugins/organization";
import { getTestInstanceMemory } from "better-auth/test";
import { organization } from "better-auth/plugins";
import { createAuthClient } from "better-auth/client";
import { ssoClient } from "./client";
let server = new OAuth2Server();
describe("SSO", async () => {
const { auth, signInWithTestUser, customFetchImpl } =
await getTestInstanceMemory({
const { auth, signInWithTestUser, customFetchImpl, cookieSetter } =
await getTestInstance({
plugins: [sso(), organization()],
});
const authClient = createAuthClient({
plugins: [ssoClient()],
baseURL: "http://localhost:3000",
fetchOptions: {
customFetchImpl,
},
});
beforeAll(async () => {
await server.issuer.keys.generate("RS256");
server.issuer.on;
@@ -57,7 +67,7 @@ describe("SSO", async () => {
});
if (!location) throw new Error("No redirect location found");
const newHeaders = new Headers();
let callbackURL = "";
await betterFetch(location, {
method: "GET",
@@ -65,10 +75,11 @@ describe("SSO", async () => {
headers,
onError(context) {
callbackURL = context.response.headers.get("location") || "";
cookieSetter(newHeaders)(context);
},
});
return callbackURL;
return { callbackURL, headers: newHeaders };
}
it("should register a new SSO provider", async () => {
@@ -147,122 +158,76 @@ describe("SSO", async () => {
});
it("should sign in with SSO provider with email matching", async () => {
const res = await auth.api.signInSSO({
body: {
const headers = new Headers();
const res = await authClient.signIn.sso({
email: "my-email@localhost.com",
callbackURL: "/dashboard",
fetchOptions: {
throw: true,
onSuccess: cookieSetter(headers),
},
});
expect(res.url).toContain("http://localhost:8080/authorize");
expect(res.url).toContain(
"redirect_uri=http%3A%2F%2Flocalhost%3A3000%2Fapi%2Fauth%2Fsso%2Fcallback%2Ftest",
);
const headers = new Headers();
const callbackURL = await simulateOAuthFlow(res.url, headers);
const { callbackURL } = await simulateOAuthFlow(res.url, headers);
expect(callbackURL).toContain("/dashboard");
});
it("should sign in with SSO provider with domain", async () => {
const res = await auth.api.signInSSO({
body: {
const headers = new Headers();
const res = await authClient.signIn.sso({
email: "my-email@test.com",
domain: "localhost.com",
callbackURL: "/dashboard",
fetchOptions: {
throw: true,
onSuccess: cookieSetter(headers),
},
});
expect(res.url).toContain("http://localhost:8080/authorize");
expect(res.url).toContain(
"redirect_uri=http%3A%2F%2Flocalhost%3A3000%2Fapi%2Fauth%2Fsso%2Fcallback%2Ftest",
);
const headers = new Headers();
const callbackURL = await simulateOAuthFlow(res.url, headers);
const { callbackURL } = await simulateOAuthFlow(res.url, headers);
expect(callbackURL).toContain("/dashboard");
});
it("should sign in with SSO provider with providerId", async () => {
const res = await auth.api.signInSSO({
body: {
const headers = new Headers();
const res = await authClient.signIn.sso({
providerId: "test",
callbackURL: "/dashboard",
fetchOptions: {
throw: true,
onSuccess: cookieSetter(headers),
},
});
expect(res.url).toContain("http://localhost:8080/authorize");
expect(res.url).toContain(
"redirect_uri=http%3A%2F%2Flocalhost%3A3000%2Fapi%2Fauth%2Fsso%2Fcallback%2Ftest",
);
const headers = new Headers();
const callbackURL = await simulateOAuthFlow(res.url, headers);
const { callbackURL } = await simulateOAuthFlow(res.url, headers);
expect(callbackURL).toContain("/dashboard");
});
});
describe("SSO with defaultSSO array", async () => {
const { auth, signInWithTestUser, customFetchImpl } =
await getTestInstanceMemory({
plugins: [
sso({
defaultSSO: [
{
domain: "localhost.com",
providerId: "default-test",
oidcConfig: {
issuer: "http://localhost:8080",
clientId: "test",
clientSecret: "test",
authorizationEndpoint: "http://localhost:8080/authorize",
tokenEndpoint: "http://localhost:8080/token",
jwksEndpoint: "http://localhost:8080/jwks",
discoveryEndpoint:
"http://localhost:8080/.well-known/openid-configuration",
pkce: true,
mapping: {
id: "sub",
email: "email",
emailVerified: "email_verified",
name: "name",
image: "picture",
},
},
},
],
}),
organization(),
],
});
it("should use default SSO provider from array when no provider found in database using providerId", async () => {
const res = await auth.api.signInSSO({
body: {
providerId: "default-test",
callbackURL: "/dashboard",
},
});
expect(res.url).toContain("http://localhost:8080/authorize");
expect(res.url).toContain(
"redirect_uri=http%3A%2F%2Flocalhost%3A3000%2Fapi%2Fauth%2Fsso%2Fcallback%2Fdefault-test",
);
});
it("should use default SSO provider from array when no provider found in database using domain fallback", async () => {
const res = await auth.api.signInSSO({
body: {
email: "test@localhost.com",
callbackURL: "/dashboard",
},
});
expect(res.url).toContain("http://localhost:8080/authorize");
expect(res.url).toContain(
"redirect_uri=http%3A%2F%2Flocalhost%3A3000%2Fapi%2Fauth%2Fsso%2Fcallback%2Fdefault-test",
);
});
});
describe("SSO disable implicit sign in", async () => {
const { auth, signInWithTestUser, customFetchImpl } =
await getTestInstanceMemory({
const { auth, signInWithTestUser, customFetchImpl, cookieSetter } =
await getTestInstance({
plugins: [sso({ disableImplicitSignUp: true }), organization()],
});
const authClient = createAuthClient({
plugins: [ssoClient()],
baseURL: "http://localhost:3000",
fetchOptions: {
customFetchImpl,
},
});
beforeAll(async () => {
await server.issuer.keys.generate("RS256");
server.issuer.on;
@@ -307,7 +272,7 @@ describe("SSO disable implicit sign in", async () => {
});
if (!location) throw new Error("No redirect location found");
const newHeaders = new Headers(headers);
let callbackURL = "";
await betterFetch(location, {
method: "GET",
@@ -315,10 +280,11 @@ describe("SSO disable implicit sign in", async () => {
headers,
onError(context) {
callbackURL = context.response.headers.get("location") || "";
cookieSetter(newHeaders)(context);
},
});
return callbackURL;
return { callbackURL, headers: newHeaders };
}
it("should register a new SSO provider", async () => {
@@ -369,150 +335,61 @@ describe("SSO disable implicit sign in", async () => {
userId: expect.any(String),
});
});
it("should not allow creating a provider if limit is set to 0", async () => {
const { auth, signInWithTestUser } = await getTestInstanceMemory({
plugins: [sso({ providersLimit: 0 })],
});
const { headers } = await signInWithTestUser();
await expect(
auth.api.registerSSOProvider({
body: {
issuer: server.issuer.url!,
domain: "localhost.com",
oidcConfig: {
clientId: "test",
clientSecret: "test",
},
providerId: "test",
},
headers,
}),
).rejects.toMatchObject({
status: "FORBIDDEN",
body: { message: "SSO provider registration is disabled" },
});
});
it("should not allow creating a provider if limit is reached", async () => {
const { auth, signInWithTestUser } = await getTestInstanceMemory({
plugins: [sso({ providersLimit: 1 })],
});
const { headers } = await signInWithTestUser();
await auth.api.registerSSOProvider({
body: {
issuer: server.issuer.url!,
domain: "localhost.com",
oidcConfig: {
clientId: "test",
clientSecret: "test",
},
providerId: "test-1",
},
headers,
});
await expect(
auth.api.registerSSOProvider({
body: {
issuer: server.issuer.url!,
domain: "localhost.com",
oidcConfig: {
clientId: "test",
clientSecret: "test",
},
providerId: "test-2",
},
headers,
}),
).rejects.toMatchObject({
status: "FORBIDDEN",
body: {
message: "You have reached the maximum number of SSO providers",
},
});
});
it("should not allow creating a provider if limit from function is reached", async () => {
const { auth, signInWithTestUser } = await getTestInstanceMemory({
plugins: [sso({ providersLimit: async () => 1 })],
});
const { headers } = await signInWithTestUser();
await auth.api.registerSSOProvider({
body: {
issuer: server.issuer.url!,
domain: "localhost.com",
oidcConfig: {
clientId: "test",
clientSecret: "test",
},
providerId: "test-1",
},
headers,
});
await expect(
auth.api.registerSSOProvider({
body: {
issuer: server.issuer.url!,
domain: "localhost.com",
oidcConfig: {
clientId: "test",
clientSecret: "test",
},
providerId: "test-2",
},
headers,
}),
).rejects.toMatchObject({
status: "FORBIDDEN",
body: {
message: "You have reached the maximum number of SSO providers",
},
});
});
it("should not create user with SSO provider when sign ups are disabled", async () => {
const res = await auth.api.signInSSO({
body: {
const headers = new Headers();
const res = await authClient.signIn.sso({
email: "my-email@localhost.com",
callbackURL: "/dashboard",
fetchOptions: {
throw: true,
onSuccess: cookieSetter(headers),
},
});
expect(res.url).toContain("http://localhost:8080/authorize");
expect(res.url).toContain(
"redirect_uri=http%3A%2F%2Flocalhost%3A3000%2Fapi%2Fauth%2Fsso%2Fcallback%2Ftest",
);
const headers = new Headers();
const callbackURL = await simulateOAuthFlow(res.url, headers);
const { callbackURL } = await simulateOAuthFlow(res.url, headers);
expect(callbackURL).toContain(
"/api/auth/error/error?error=signup disabled",
);
});
it("should create user with SSO provider when sign ups are disabled but sign up is requested", async () => {
const res = await auth.api.signInSSO({
body: {
const headers = new Headers();
const res = await authClient.signIn.sso({
email: "my-email@localhost.com",
callbackURL: "/dashboard",
requestSignUp: true,
fetchOptions: {
throw: true,
onSuccess: cookieSetter(headers),
},
});
expect(res.url).toContain("http://localhost:8080/authorize");
expect(res.url).toContain(
"redirect_uri=http%3A%2F%2Flocalhost%3A3000%2Fapi%2Fauth%2Fsso%2Fcallback%2Ftest",
);
const headers = new Headers();
const callbackURL = await simulateOAuthFlow(res.url, headers);
const { callbackURL } = await simulateOAuthFlow(res.url, headers);
expect(callbackURL).toContain("/dashboard");
});
});
describe("provisioning", async (ctx) => {
const { auth, signInWithTestUser, customFetchImpl } =
await getTestInstanceMemory({
const { auth, signInWithTestUser, customFetchImpl, cookieSetter } =
await getTestInstance({
plugins: [sso(), organization()],
});
const authClient = createAuthClient({
plugins: [ssoClient()],
baseURL: "http://localhost:3000",
fetchOptions: {
customFetchImpl,
},
});
beforeAll(async () => {
await server.issuer.keys.generate("RS256");
server.issuer.on;
@@ -540,12 +417,14 @@ describe("provisioning", async (ctx) => {
if (!location) throw new Error("No redirect location found");
let callbackURL = "";
const newHeaders = new Headers();
await betterFetch(location, {
method: "GET",
customFetchImpl: fetchImpl || customFetchImpl,
headers,
onError(context) {
callbackURL = context.response.headers.get("location") || "";
cookieSetter(newHeaders)(context);
},
});
@@ -605,18 +484,20 @@ describe("provisioning", async (ctx) => {
expect(provider).toMatchObject({
organizationId: organization?.id,
});
const res = await auth.api.signInSSO({
body: {
const newHeaders = new Headers();
const res = await authClient.signIn.sso({
email: "my-email@localhost.com",
callbackURL: "/dashboard",
fetchOptions: {
onSuccess: cookieSetter(newHeaders),
throw: true,
},
});
expect(res.url).toContain("http://localhost:8080/authorize");
expect(res.url).toContain(
"redirect_uri=http%3A%2F%2Flocalhost%3A3000%2Fapi%2Fauth%2Fsso%2Fcallback%2Ftest",
);
const newHeaders = new Headers();
const callbackURL = await simulateOAuthFlow(res.url, newHeaders);
expect(callbackURL).toContain("/dashboard");
const org = await auth.api.getFullOrganization({

View File

@@ -5,10 +5,5 @@
"outDir": "./dist",
"lib": ["esnext", "dom", "dom.iterable"]
},
"references": [
{
"path": "../better-auth/tsconfig.json"
}
],
"include": ["src"]
}

View File

@@ -5,10 +5,5 @@
"outDir": "./dist",
"lib": ["esnext", "dom", "dom.iterable"]
},
"references": [
{
"path": "../better-auth/tsconfig.json"
}
],
"include": ["src"]
}

1244
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -2,83 +2,18 @@
"compilerOptions": {
"strict": true,
"target": "esnext",
"module": "esnext",
"moduleResolution": "bundler",
"downlevelIteration": true,
"baseUrl": ".",
"strict": true,
"target": "esnext",
"downlevelIteration": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"module": "esnext",
"moduleResolution": "bundler",
"skipLibCheck": true,
"verbatimModuleSyntax": true,
"noUnusedLocals": false,
"noUnusedParameters": false,
"noUnusedLocals": false,
"noUnusedParameters": false,
"noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": false,
"declaration": true,
"emitDeclarationOnly": true,
"composite": true,
"incremental": true,
"noErrorTruncation": true,
"types": ["node"]
"types": ["node", "bun"]
},
"references": [
{
"path": "./packages/better-auth"
},
{
"path": "./packages/cli"
},
{
"path": "./packages/expo"
},
{
"path": "./packages/sso"
},
{
"path": "./packages/stripe"
}
],
"files": [],
"include": [],
"exactOptionalPropertyTypes": false,
"declaration": true,
"emitDeclarationOnly": true,
"composite": true,
"incremental": true,
"noErrorTruncation": true,
"types": ["node"],
"paths": {
"better-auth": ["./packages/better-auth/src"],
"better-auth/*": ["./packages/better-auth/src/*"]
}
},
"references": [
{
"path": "./packages/better-auth"
},
{
"path": "./packages/cli"
},
{
"path": "./packages/cli/tsconfig.test.json"
},
{
"path": "./packages/expo"
},
{
"path": "./packages/sso"
},
{
"path": "./packages/stripe"
}
],
"files": [],
"include": [],
"exclude": ["**/dist/**", "**/node_modules/**", "**/examples/**"]
"exclude": ["**/dist/**", "**/node_modules/**"]
}