diff --git a/dev/next-app/package.json b/dev/next-app/package.json
index 0b2e705a10..efd64c2bbf 100644
--- a/dev/next-app/package.json
+++ b/dev/next-app/package.json
@@ -32,6 +32,7 @@
"react": "^18",
"react-dom": "^18",
"react-hook-form": "^7.52.2",
+ "react-qr-code": "^2.0.15",
"sonner": "^1.5.0",
"tailwind-merge": "^2.5.0",
"tailwindcss-animate": "^1.0.7",
diff --git a/dev/next-app/prisma/db.sqlite b/dev/next-app/prisma/db.sqlite
index 5f659c0a8c..4bfc5a953d 100644
Binary files a/dev/next-app/prisma/db.sqlite and b/dev/next-app/prisma/db.sqlite differ
diff --git a/dev/next-app/src/app/(auth)/sign-in/page.tsx b/dev/next-app/src/app/(auth)/sign-in/page.tsx
index e97846a4df..051eff804c 100644
--- a/dev/next-app/src/app/(auth)/sign-in/page.tsx
+++ b/dev/next-app/src/app/(auth)/sign-in/page.tsx
@@ -11,8 +11,9 @@ import {
} from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
-import { authClient } from "@/lib/client";
+import { authClient } from "@/lib/auth-client";
import { useState } from "react";
+import { Key } from "lucide-react";
export default function Page() {
const [email, setEmail] = useState("");
@@ -59,7 +60,7 @@ export default function Page() {
/>
+
Don't have an account?{" "}
diff --git a/dev/next-app/src/app/(auth)/sign-up/page.tsx b/dev/next-app/src/app/(auth)/sign-up/page.tsx
index 360f1e3fce..ac9ee8b36e 100644
--- a/dev/next-app/src/app/(auth)/sign-up/page.tsx
+++ b/dev/next-app/src/app/(auth)/sign-up/page.tsx
@@ -13,7 +13,7 @@ import {
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { useState } from "react";
-import { authClient } from "@/lib/client";
+import { authClient } from "@/lib/auth-client";
export default function SignUpForm() {
const [firstName, setFirstName] = useState("");
diff --git a/dev/next-app/src/app/(auth)/two-factor/otp/page.tsx b/dev/next-app/src/app/(auth)/two-factor/otp/page.tsx
new file mode 100644
index 0000000000..0a2815454f
--- /dev/null
+++ b/dev/next-app/src/app/(auth)/two-factor/otp/page.tsx
@@ -0,0 +1,77 @@
+'use client'
+
+import { useState } from 'react'
+import { Button } from "@/components/ui/button"
+import { Input } from "@/components/ui/input"
+import { Label } from "@/components/ui/label"
+import { Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter } from "@/components/ui/card"
+import { AlertCircle, CheckCircle2, Mail } from "lucide-react"
+import { authClient } from '@/lib/auth-client'
+
+export default function Component() {
+ const [otp, setOtp] = useState('')
+ const [isOtpSent, setIsOtpSent] = useState(false)
+ const [message, setMessage] = useState('')
+ const [isError, setIsError] = useState(false)
+ const [isValidated, setIsValidated] = useState(false)
+
+ // In a real app, this email would come from your authentication context
+ const userEmail = "user@example.com"
+
+ const requestOTP = async () => {
+ await authClient.twoFactor.sendOtp();
+ // In a real app, this would call your backend API to send the OTP
+ setMessage('OTP sent to your email')
+ setIsError(false)
+ setIsOtpSent(true)
+ }
+
+ const validateOTP = async () => {
+ await authClient.twoFactor.verifyOtp({
+ body: {
+ code: otp,
+ }
+ })
+ }
+ return (
+
+
+
+ Two-Factor Authentication
+ Verify your identity with a one-time password
+
+
+
+ {!isOtpSent ? (
+
+ ) : (
+ <>
+
+
+ setOtp(e.target.value)}
+ maxLength={6}
+ />
+
+
+ >
+ )}
+
+ {message && (
+
+ {isError ?
:
}
+
{message}
+
+ )}
+
+
+
+ )
+}
\ No newline at end of file
diff --git a/dev/next-app/src/app/(auth)/two-factor/page.tsx b/dev/next-app/src/app/(auth)/two-factor/page.tsx
index ef5705a06c..0486cc93f6 100644
--- a/dev/next-app/src/app/(auth)/two-factor/page.tsx
+++ b/dev/next-app/src/app/(auth)/two-factor/page.tsx
@@ -6,7 +6,8 @@ import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"
import { AlertCircle, CheckCircle2 } from "lucide-react"
-import { authClient } from "@/lib/client"
+import { authClient } from "@/lib/auth-client"
+import Link from "next/link"
export default function Component() {
const [totpCode, setTotpCode] = useState("")
@@ -19,10 +20,10 @@ export default function Component() {
setError("TOTP code must be 6 digits")
return
}
- authClient.verifyTotp({
+ authClient.twoFactor.verify({
body: {
code: totpCode,
- callbackURL: "/"
+ with: "totp",
}
}).then((res) => {
console.log(res)
@@ -78,8 +79,12 @@ export default function Component() {
)}
-
- Protect your account with TOTP-based authentication
+
+
+
+
diff --git a/dev/next-app/src/app/page.tsx b/dev/next-app/src/app/page.tsx
index ec9a66e418..a9514f198c 100644
--- a/dev/next-app/src/app/page.tsx
+++ b/dev/next-app/src/app/page.tsx
@@ -28,7 +28,7 @@ export default async function Home() {
)}
-
+ {/* */}
);
diff --git a/dev/next-app/src/components/add-count.tsx b/dev/next-app/src/components/add-count.tsx
index dc36a18060..1862519155 100644
--- a/dev/next-app/src/components/add-count.tsx
+++ b/dev/next-app/src/components/add-count.tsx
@@ -1,7 +1,7 @@
"use client";
import { setCounter } from "@/server/counter";
import { Button } from "./ui/button";
-import { client } from "@/lib/client";
+import { client } from "@/lib/auth-client";
export async function AddCount() {
return (
diff --git a/dev/next-app/src/components/client.tsx b/dev/next-app/src/components/client.tsx
index 5692265b72..1c42185310 100644
--- a/dev/next-app/src/components/client.tsx
+++ b/dev/next-app/src/components/client.tsx
@@ -1,10 +1,15 @@
"use client";
-import { authClient } from "@/lib/client";
+import { authClient } from "@/lib/auth-client";
import { useAuthStore } from "better-auth/react"
import { Button } from "./ui/button";
+import QRCode from "react-qr-code";
+import { useEffect, useState } from "react";
+import { Card, CardContent, CardHeader } from "./ui/card";
+import { Dialog, DialogContent, DialogTrigger } from "./ui/dialog";
export function Client() {
- const session = useAuthStore(authClient.$session)
+ const [uri, setUri] = useState()
+ const session = authClient.useSession()
type S = NonNullable
const a: S['user'] = {
id: "1",
@@ -14,23 +19,59 @@ export function Client() {
createdAt: new Date(),
updatedAt: new Date(),
}
+
+ useEffect(() => {
+ if (session?.user?.twoFactorEnabled) {
+ authClient.twoFactor.getTotpUri().then((res) => {
+ if (res.data) {
+ setUri(res.data.totpURI)
+ }
+ })
+ }
+ }, [session])
+
return (
-
- {
- session ?
-
-
: null
- }
-
+
+
+
+
+
+
+ {
+ session ?
+
+
: null
+ }
+ {
+ uri ? : null
+ }
+
+
)
}
\ No newline at end of file
diff --git a/dev/next-app/src/components/org.tsx b/dev/next-app/src/components/org.tsx
index 7bdfaaa060..557e7432b6 100644
--- a/dev/next-app/src/components/org.tsx
+++ b/dev/next-app/src/components/org.tsx
@@ -26,7 +26,7 @@ import {
TableHeader,
TableRow,
} from "./ui/table";
-import { authClient } from "@/lib/client";
+import { authClient } from "@/lib/auth-client";
import { useAuthStore } from "better-auth/react";
export const Organization = () => {
diff --git a/dev/next-app/src/components/signout.tsx b/dev/next-app/src/components/signout.tsx
index 5f73191e69..9c5a576f24 100644
--- a/dev/next-app/src/components/signout.tsx
+++ b/dev/next-app/src/components/signout.tsx
@@ -1,6 +1,6 @@
"use client";
-import { authClient } from "@/lib/client";
+import { authClient } from "@/lib/auth-client";
import { Button } from "./ui/button";
export const SignOut = () => {
diff --git a/dev/next-app/src/lib/client.ts b/dev/next-app/src/lib/auth-client.ts
similarity index 71%
rename from dev/next-app/src/lib/client.ts
rename to dev/next-app/src/lib/auth-client.ts
index ee1f84fe3b..e111bb5ea3 100644
--- a/dev/next-app/src/lib/client.ts
+++ b/dev/next-app/src/lib/auth-client.ts
@@ -1,4 +1,4 @@
-import { createAuthClient } from "better-auth/client";
+import { createAuthClient } from "better-auth/react";
import { auth } from "./auth";
export const authClient = createAuthClient({
diff --git a/dev/next-app/src/lib/auth.ts b/dev/next-app/src/lib/auth.ts
index 7fe349049a..f59c674cd4 100644
--- a/dev/next-app/src/lib/auth.ts
+++ b/dev/next-app/src/lib/auth.ts
@@ -1,6 +1,6 @@
import { betterAuth } from "better-auth";
import { github, passkey } from "better-auth/provider";
-import { twoFactor } from "better-auth/plugins";
+import { organization, twoFactor } from "better-auth/plugins";
export const auth = betterAuth({
basePath: "/api/auth",
@@ -9,10 +9,6 @@ export const auth = betterAuth({
clientId: process.env.GITHUB_CLIENT_ID as string,
clientSecret: process.env.GITHUB_CLIENT_SECRET as string,
}),
- passkey({
- rpID: "localhost",
- rpName: "Better Auth",
- }),
],
database: {
provider: "sqlite",
@@ -23,9 +19,15 @@ export const auth = betterAuth({
enabled: true,
},
plugins: [
+ organization(),
twoFactor({
issuer: "BetterAuth",
twoFactorURL: "/two-factor",
+ otpOptions: {
+ async sendOTP(user, otp) {
+ console.log({ user, otp });
+ },
+ },
}),
],
});
diff --git a/packages/better-auth/package.json b/packages/better-auth/package.json
index 3a8381cf9c..1660ae3f10 100644
--- a/packages/better-auth/package.json
+++ b/packages/better-auth/package.json
@@ -52,12 +52,13 @@
"@simplewebauthn/browser": "^10.0.0",
"@simplewebauthn/server": "^10.0.1",
"arctic": "^1.9.2",
- "better-call": "^0.1.28",
+ "better-call": "^0.1.33",
"chalk": "^5.3.0",
"commander": "^12.1.0",
"consola": "^3.2.3",
"dotenv": "^16.4.5",
"jiti": "^1.21.6",
+ "jose": "^5.7.0",
"kysely": "^0.27.4",
"mysql2": "^3.11.0",
"nanostores": "^0.11.2",
diff --git a/packages/better-auth/src/adapters/schema.ts b/packages/better-auth/src/adapters/schema.ts
index b9f8657f2b..ca025a6739 100644
--- a/packages/better-auth/src/adapters/schema.ts
+++ b/packages/better-auth/src/adapters/schema.ts
@@ -1,4 +1,6 @@
import { z } from "zod";
+import { BetterAuthOptions } from "../types";
+import { FieldAttribute } from "../db";
export const accountSchema = z.object({
id: z.string(),
@@ -39,3 +41,53 @@ export interface MigrationTable {
name: string;
timestamp: string;
}
+
+export function parseData>(
+ data: T,
+ schema: {
+ fields: Record;
+ },
+) {
+ const fields = schema.fields;
+ const parsedData: Record = {};
+ for (const key in data) {
+ const field = fields[key];
+ if (!field) {
+ parsedData[key] = data[key];
+ continue;
+ }
+ if (field.returned === false) {
+ continue;
+ }
+ parsedData[key] = data[key];
+ }
+ return parsedData as T;
+}
+
+export function getAllFields(options: BetterAuthOptions, table: string) {
+ let schema: Record = {};
+ for (const plugin of options.plugins || []) {
+ if (plugin.schema && plugin.schema[table]) {
+ schema = {
+ ...schema,
+ ...plugin.schema[table].fields,
+ };
+ }
+ }
+ return schema;
+}
+
+export function parseUser(options: BetterAuthOptions, user: User) {
+ const schema = getAllFields(options, "user");
+ return parseData(user, { fields: schema });
+}
+
+export function parseAccount(options: BetterAuthOptions, account: Account) {
+ const schema = getAllFields(options, "account");
+ return parseData(account, { fields: schema });
+}
+
+export function parseSession(options: BetterAuthOptions, session: Session) {
+ const schema = getAllFields(options, "session");
+ return parseData(session, { fields: schema });
+}
diff --git a/packages/better-auth/src/api/index.ts b/packages/better-auth/src/api/index.ts
index c835612762..a191dcd8f2 100644
--- a/packages/better-auth/src/api/index.ts
+++ b/packages/better-auth/src/api/index.ts
@@ -10,6 +10,7 @@ import { AuthContext } from "../init";
import { csrfMiddleware } from "./middlewares/csrf";
import { getCSRFToken } from "./routes/csrf";
import { signUpCredential } from "./routes/signup";
+import { parseAccount, parseSession, parseUser } from "../adapters/schema";
export const router = (ctx: C) => {
const pluginEndpoints = ctx.options.plugins?.reduce(
@@ -101,8 +102,25 @@ export const router = (ctx: C) => {
},
...middlewares,
],
- onError(e) {
- ctx.logger.error(e);
+ /**
+ * this is to remove any sensitive data from the response
+ */
+ async transformResponse(res) {
+ const body = await res.json();
+ if (body?.user) {
+ body.user = parseUser(ctx.options, body.user);
+ }
+ if (body?.session) {
+ body.session = parseSession(ctx.options, body.session);
+ }
+ if (body?.account) {
+ body.account = parseAccount(ctx.options, body.account);
+ }
+ return new Response(body ? JSON.stringify(body) : null, {
+ headers: res.headers,
+ status: res.status,
+ statusText: res.statusText,
+ });
},
});
};
diff --git a/packages/better-auth/src/api/routes/confirm-email.ts b/packages/better-auth/src/api/routes/confirm-email.ts
new file mode 100644
index 0000000000..8f6b208fbe
--- /dev/null
+++ b/packages/better-auth/src/api/routes/confirm-email.ts
@@ -0,0 +1,14 @@
+import { z } from "zod";
+import { createAuthEndpoint } from "../call";
+
+export const confirmEmail = createAuthEndpoint(
+ "/confirm-email",
+ {
+ method: "POST",
+ body: z.object({
+ token: z.string(),
+ email: z.string().email(),
+ }),
+ },
+ async (ctx) => {},
+);
diff --git a/packages/better-auth/src/api/routes/forget-password.ts b/packages/better-auth/src/api/routes/forget-password.ts
new file mode 100644
index 0000000000..ffd2a3cd7b
--- /dev/null
+++ b/packages/better-auth/src/api/routes/forget-password.ts
@@ -0,0 +1,46 @@
+import { z } from "zod";
+import { createAuthEndpoint } from "../call";
+import { createJWT } from "oslo/jwt";
+import { TimeSpan } from "oslo";
+
+export const forgetPassword = createAuthEndpoint(
+ "/send-forget-password",
+ {
+ method: "POST",
+ body: z.object({
+ email: z.string().email(),
+ }),
+ },
+ async (ctx) => {
+ const { email } = ctx.body;
+ const user = await ctx.context.internalAdapter.findUserByEmail(email);
+ if (!user) {
+ return ctx.json(
+ {
+ error: "User not found",
+ },
+ {
+ status: 400,
+ statusText: "USER_NOT_FOUND",
+ body: {
+ message: "User not found",
+ },
+ },
+ );
+ }
+ const token = await createJWT(
+ "HS256",
+ Buffer.from(ctx.context.secret),
+ {
+ email: user.user.email,
+ },
+ {
+ expiresIn: new TimeSpan(1, "h"),
+ issuer: "better-auth",
+ subject: "forget-password",
+ audiences: [user.user.email],
+ includeIssuedTimestamp: true,
+ },
+ );
+ },
+);
diff --git a/packages/better-auth/src/api/routes/signin.ts b/packages/better-auth/src/api/routes/signin.ts
index b36964a9d0..5c37135557 100644
--- a/packages/better-auth/src/api/routes/signin.ts
+++ b/packages/better-auth/src/api/routes/signin.ts
@@ -7,7 +7,7 @@ import { oAuthProviderList } from "../../providers";
import { Argon2id } from "oslo/password";
export const signInOAuth = createAuthEndpoint(
- "/signin/oauth",
+ "/sign-in/oauth",
{
method: "POST",
query: z
diff --git a/packages/better-auth/src/client/base.ts b/packages/better-auth/src/client/base.ts
index d41f176e40..03970509e7 100644
--- a/packages/better-auth/src/client/base.ts
+++ b/packages/better-auth/src/client/base.ts
@@ -1,125 +1,44 @@
-import { Endpoint, Prettify } from "better-call";
import { BetterAuth } from "../auth";
-import { HasRequiredKeys, UnionToIntersection } from "type-fest";
-import {
- BetterFetchOption,
- BetterFetchPlugin,
- BetterFetchResponse,
- createFetch,
-} from "@better-fetch/fetch";
-import { BetterAuthError } from "../error/better-auth-error";
+import { createDynamicPathProxy } from "./proxy";
+import { createClient } from "better-call/client";
+import { BetterFetch, createFetch } from "@better-fetch/fetch";
+import { getSessionAtom } from "./session-atom";
+import { getOrganizationAtoms } from "./org-atoms";
+import { getPasskeyActions } from "./passkey-actions";
+import { addCurrentURL, csrfPlugin, redirectPlugin } from "./client-plugins";
+import { InferRoutes } from "./path-to-object";
+import { ClientOptions } from "./type";
+import { getBaseURL } from "./client-utils";
-type InferContext = T extends (ctx: infer Ctx) => any
- ? Ctx extends
- | {
- body: infer Body;
- }
- | {
- params: infer Param;
- }
- ? (Body extends undefined
- ? {}
- : {
- body: Body;
- }) &
- (Param extends undefined
- ? {}
- : {
- params: Param;
- })
- : never
- : never;
-
-export interface ClientOptions extends BetterFetchOption {}
-
-const redirectPlugin = {
- id: "redirect",
- name: "Redirect",
- hooks: {
- onSuccess(context) {
- if (context.data.url && context.data.redirect) {
- console.log("redirecting to", context.data.url);
- }
- },
- },
-} satisfies BetterFetchPlugin;
-
-function inferBaeURL() {
- const url =
- process.env.AUTH_URL ||
- process.env.NEXT_PUBLIC_AUTH_URL ||
- process.env.BETTER_AUTH_URL ||
- process.env.NEXT_PUBLIC_BETTER_AUTH_URL ||
- process.env.VERCEL_URL ||
- process.env.NEXT_PUBLIC_VERCEL_URL;
- if (url) {
- return url;
- }
- if (
- !url &&
- (process.env.NODE_ENV === "development" || process.env.NODE_ENV === "test")
- ) {
- return "http://localhost:3000";
- }
- throw new BetterAuthError(
- "Could not infer baseURL from environment variables. Please pass it as an option to the createClient function.",
- );
-}
-
-export const createBaseClient = (
+export const createVanillaClient = (
options?: ClientOptions,
) => {
- const fetch = createFetch({
- baseURL: options?.baseURL || inferBaeURL(),
+ type API = Auth extends never ? BetterAuth["api"] : Auth["api"];
+ const $fetch = createFetch({
...options,
- plugins: [...(options?.plugins || []), redirectPlugin],
+ baseURL: getBaseURL(options?.baseURL),
+ plugins: [redirectPlugin, addCurrentURL, csrfPlugin],
});
-
- type API = Auth["api"];
- type Options = API extends {
- [key: string]: infer T;
- }
- ? T extends Endpoint
- ? {
- [key in T["path"]]: T;
- }
- : {}
- : {};
-
- type O = Prettify>;
- return async (
- path: K,
- ...options: HasRequiredKeys> extends true
- ? [
- BetterFetchOption<
- InferContext["body"],
- any,
- InferContext["params"]
- >,
- ]
- : [
- BetterFetchOption<
- InferContext["body"],
- InferContext["params"]
- >?,
- ]
- ): Promise<
- BetterFetchResponse<
- Awaited>
- >
- > => {
- const opts = options[0] as {
- params?: Record;
- body?: Record;
- };
- return (await fetch(path as string, {
- ...options[0],
- body: opts.body,
- params: opts.params,
- method: opts.body ? "POST" : "GET",
- onRequest(context) {
- console.log("request", context.url);
- },
- })) as any;
+ const { $session, $sessionSignal } = getSessionAtom($fetch);
+ const { signInPasskey, signUpPasskey } = getPasskeyActions($fetch);
+ const { $activeOrganization, $listOrganizations, activeOrgId, $listOrg } =
+ getOrganizationAtoms($fetch, $session);
+ const actions = {
+ setActiveOrg: (orgId: string | null) => {
+ activeOrgId.set(orgId);
+ },
+ signInPasskey,
+ signUpPasskey,
+ $atoms: {
+ $session,
+ $activeOrganization,
+ $listOrganizations,
+ },
};
+ const proxy = createDynamicPathProxy(actions, $fetch, {
+ "/create/organization": $listOrg,
+ "/two-factor/enable": $sessionSignal,
+ "/two-factor/disable": $sessionSignal,
+ }) as unknown as InferRoutes & typeof actions;
+ return proxy;
};
diff --git a/packages/better-auth/src/client/client-utils.ts b/packages/better-auth/src/client/client-utils.ts
index b45856d5ea..d439bd8173 100644
--- a/packages/better-auth/src/client/client-utils.ts
+++ b/packages/better-auth/src/client/client-utils.ts
@@ -1,23 +1,44 @@
-import { BetterAuthError } from "../error/better-auth-error";
+export const HIDE_ON_CLIENT_METADATA = {
+ onClient: "hide" as const,
+};
-export function inferBaeURL() {
- const url =
+function checkHasPath(url: string): boolean {
+ try {
+ const parsedUrl = new URL(url);
+ return parsedUrl.pathname !== "/";
+ } catch (error) {
+ console.error("Invalid URL:", error);
+ return false;
+ }
+}
+
+function withPath(url: string) {
+ const hasPath = checkHasPath(url);
+ if (hasPath) {
+ return url;
+ }
+ return `${url}/api/auth`;
+}
+
+export function getBaseURL(url?: string) {
+ if (url) {
+ return withPath(url);
+ }
+ const fromEnv =
process.env.AUTH_URL ||
process.env.NEXT_PUBLIC_AUTH_URL ||
process.env.BETTER_AUTH_URL ||
- process.env.NEXT_PUBLIC_BETTER_AUTH_URL ||
- process.env.VERCEL_URL ||
- process.env.NEXT_PUBLIC_VERCEL_URL;
- if (url) {
- return url;
+ process.env.NEXT_PUBLIC_BETTER_AUTH_URL;
+ if (fromEnv) {
+ return withPath(fromEnv);
}
if (
- !url &&
+ !fromEnv &&
(process.env.NODE_ENV === "development" || process.env.NODE_ENV === "test")
) {
- return "http://localhost:3000";
+ return "http://localhost:3000/api/auth";
}
- throw new BetterAuthError(
+ throw new Error(
"Could not infer baseURL from environment variables. Please pass it as an option to the createClient function.",
);
}
diff --git a/packages/better-auth/src/client/client.test.ts b/packages/better-auth/src/client/client.test.ts
new file mode 100644
index 0000000000..5b346b7b47
--- /dev/null
+++ b/packages/better-auth/src/client/client.test.ts
@@ -0,0 +1,27 @@
+import { describe, it } from "vitest";
+import { getTestInstance } from "../test-utils/test-instance";
+import { createAuthClient } from "./react";
+import { passkey } from "../providers";
+import { createClient } from "better-call/client";
+
+describe("client path to object", async () => {
+ const auth = await getTestInstance({
+ providers: [
+ passkey({
+ rpID: "test",
+ rpName: "test",
+ }),
+ ],
+ });
+
+ it("should return a path to object", async () => {
+ const client = createAuthClient({
+ baseURL: "http://localhost:3000/api/auth",
+ customFetchImpl: async (url, options) => {
+ console.log({ url, options });
+ return new Response();
+ },
+ });
+ console.log(client.$atoms.$session.get());
+ });
+});
diff --git a/packages/better-auth/src/client/index.ts b/packages/better-auth/src/client/index.ts
index 056a3b9afd..955fdd1439 100644
--- a/packages/better-auth/src/client/index.ts
+++ b/packages/better-auth/src/client/index.ts
@@ -1,81 +1 @@
-import { ClientOptions } from "./base";
-import { BetterAuth } from "../auth";
-import {
- InferredActions,
- PickDefaultPaths,
- PickOrganizationPaths,
- PickProvidePaths,
-} from "./type";
-import { getProxy } from "./proxy";
-import { createClient } from "better-call/client";
-import { BetterFetch, createFetch } from "@better-fetch/fetch";
-import { OAuthProvider, OAuthProviderList } from "../types/provider";
-import { getSessionAtom } from "./session-atom";
-import { getOrganizationAtoms } from "./org-atoms";
-import { getPasskeyActions } from "./passkey-actions";
-import { inferBaeURL } from "./client-utils";
-import { addCurrentURL, csrfPlugin, redirectPlugin } from "./client-plugins";
-
-export const createAuthClient = (
- options?: ClientOptions,
-) => {
- type API = BetterAuth["api"];
-
- const client = createClient({
- ...options,
- baseURL: options?.baseURL || inferBaeURL(),
- plugins: [redirectPlugin, addCurrentURL, csrfPlugin],
- });
-
- const $fetch = createFetch({
- ...options,
- baseURL: options?.baseURL || inferBaeURL(),
- plugins: [redirectPlugin, addCurrentURL, csrfPlugin],
- });
-
- const signInOAuth = async (data: {
- provider: Auth["options"]["providers"] extends Array
- ? T extends OAuthProvider
- ? T["id"]
- : never
- : OAuthProviderList[number];
- callbackURL: string;
- }) => {
- const res = await client("@post/signin/oauth", {
- body: data,
- });
- if (res.data?.redirect) {
- window.location.href = res.data.url;
- }
- return res;
- };
-
- const { $session } = getSessionAtom($fetch);
- const { signInPasskey, signUpPasskey } = getPasskeyActions($fetch);
- const { $activeOrganization, $listOrganizations, activeOrgId, $listOrg } =
- getOrganizationAtoms($fetch, $session);
-
- const actions = {
- signInOAuth,
- $session,
- $activeOrganization,
- $listOrganizations,
- setActiveOrg: (orgId: string | null) => {
- activeOrgId.set(orgId);
- },
- signInPasskey,
- signUpPasskey,
- };
-
- type PickedActions = Pick<
- typeof actions,
- | PickOrganizationPaths
- | PickDefaultPaths
- | PickProvidePaths<"passkey", "signInPasskey" | "signUpPasskey", Auth>
- >;
-
- const proxy = getProxy(actions, client as BetterFetch, {
- "create/organization": $listOrg,
- }) as InferredActions & PickedActions;
- return proxy;
-};
+export * from "./base";
diff --git a/packages/better-auth/src/client/path-to-object.ts b/packages/better-auth/src/client/path-to-object.ts
new file mode 100644
index 0000000000..2f311dc680
--- /dev/null
+++ b/packages/better-auth/src/client/path-to-object.ts
@@ -0,0 +1,46 @@
+import { BetterFetchResponse } from "@better-fetch/fetch";
+import { Context, Endpoint } from "better-call";
+import {
+ HasRequiredKeys,
+ Prettify,
+ UnionToIntersection,
+} from "../types/helper";
+
+type CamelCase =
+ S extends `${infer P1}-${infer P2}${infer P3}`
+ ? `${Lowercase}${Uppercase}${CamelCase}`
+ : Lowercase;
+
+export type PathToObject<
+ T extends string,
+ Fn extends (...args: any[]) => any,
+> = T extends `/${infer Segment}/${infer Rest}`
+ ? { [K in CamelCase]: PathToObject<`/${Rest}`, Fn> }
+ : T extends `/${infer Segment}`
+ ? { [K in CamelCase]: Fn }
+ : never;
+
+type MergeRoutes = UnionToIntersection;
+type InferRoute = API extends {
+ [key: string]: infer T;
+}
+ ? T extends Endpoint
+ ? T["options"]["metadata"] extends {
+ onClient: "hide";
+ }
+ ? {}
+ : PathToObject<
+ T["path"],
+ T extends (ctx: infer C) => infer R
+ ? C extends Context
+ ? (
+ ...data: HasRequiredKeys extends true
+ ? [Prettify]
+ : [Prettify?]
+ ) => Promise>>
+ : never
+ : never
+ >
+ : never
+ : never;
+export type InferRoutes = MergeRoutes>;
diff --git a/packages/better-auth/src/client/preact.ts b/packages/better-auth/src/client/preact.ts
index 00ce895ff7..8f992098e6 100644
--- a/packages/better-auth/src/client/preact.ts
+++ b/packages/better-auth/src/client/preact.ts
@@ -1,2 +1,23 @@
import { useStore } from "@nanostores/react";
export const useAuthStore = useStore;
+
+import { createVanillaClient } from "./base";
+import { BetterFetchOption } from "@better-fetch/fetch";
+
+export const createAuthClient = (options?: BetterFetchOption) => {
+ const client = createVanillaClient(options);
+ function useSession() {
+ return useStore(client.$atoms.$session);
+ }
+ function useActiveOrganization() {
+ return useStore(client.$atoms.$activeOrganization);
+ }
+ function useListOrganization() {
+ return useStore(client.$atoms.$listOrganizations);
+ }
+ return Object.assign(client, {
+ useSession,
+ useActiveOrganization,
+ useListOrganization,
+ });
+};
diff --git a/packages/better-auth/src/client/proxy.ts b/packages/better-auth/src/client/proxy.ts
index 9a5720b911..5f853dbe07 100644
--- a/packages/better-auth/src/client/proxy.ts
+++ b/packages/better-auth/src/client/proxy.ts
@@ -1,53 +1,12 @@
import { BetterFetch, BetterFetchOption } from "@better-fetch/fetch";
-import { createBaseClient } from "./base";
-import { Atom, PreinitializedWritableAtom } from "nanostores";
-
-function fromCamelCase(str: string) {
- const path = str
- .split(/(?=[A-Z])/)
- .join("/")
- .toLowerCase();
- return `/${path}`;
-}
-
-const knownCases = [
- ["sign", "in"],
- ["sign", "up"],
- ["sign", "out"],
- ["invite", "member"],
- ["update", "member"],
- ["delete", "member"],
- ["accept", "invitation"],
- ["reject", "invitation"],
- ["cancel", "invitation"],
- ["has", "permission"],
-];
-
-/**
- * Handles edge cases like signInCredential and
- * signUpCredential
- */
-function handleEdgeCases(str: string) {
- const splits = str.split("/").filter((s) => s);
- let index = 0;
- for (const path of splits) {
- const secondPath = splits[index + 1]?.trim();
- if (secondPath) {
- const isKnownCase = knownCases.some(
- ([a, b]) => a === path && b === secondPath,
- );
- if (isKnownCase) {
- splits[index] = `${path}-${secondPath}`;
- splits.splice(1, index + 1);
- }
- }
- }
- return splits.join("/");
-}
+import { PreinitializedWritableAtom } from "nanostores";
const knownPathMethods: Record = {
"/sign-out": "POST",
"enable/totp": "POST",
+ "/two-factor/disable": "POST",
+ "/two-factor/enable": "POST",
+ "/two-factor/send-otp": "POST",
};
function getMethod(path: string, args?: BetterFetchOption) {
@@ -61,40 +20,48 @@ function getMethod(path: string, args?: BetterFetchOption) {
return "GET";
}
-export function getProxy(
- actions: Record,
+export function createDynamicPathProxy>(
+ routes: T,
client: BetterFetch,
$signal?: {
[key: string]: PreinitializedWritableAtom;
},
-) {
- return new Proxy(actions, {
- get(target, key) {
- if (key in target) {
- return target[key as keyof typeof actions];
+): T {
+ const handler: ProxyHandler = {
+ get(target, prop: string) {
+ // If the property exists in the initial object, return it directly
+ if (prop in routes) {
+ return routes[prop as string];
}
- return (args?: BetterFetchOption) => {
- key = fromCamelCase(key as string);
- key = handleEdgeCases(key);
- if (args?.params) {
- const paramPlaceholder = Object.keys(args?.params)
- .map((key) => `:${key}`)
+ return new Proxy(() => {}, {
+ get: (_, nestedProp: string) => {
+ //@ts-expect-error
+ return handler.get(target, `${prop}.${nestedProp}`);
+ },
+ apply: async (_, __, args) => {
+ if (prop in target) {
+ return target[prop](...args);
+ }
+ const path = prop
+ .split(".")
+ .map((segment) =>
+ segment.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`),
+ )
.join("/");
- key = paramPlaceholder.length
- ? `${key as string}/${paramPlaceholder}`
- : key;
- }
- return client(key as "/signin/oauth", {
- ...(args || {}),
- method: getMethod(key, args),
- onSuccess() {
- const signal = $signal?.[key as string];
- if (signal) {
- signal.set(!signal.get());
- }
- },
- });
- };
+ const routePath = `/${path}`;
+ return await client(routePath, {
+ ...args[0],
+ method: getMethod(routePath, args[0]),
+ onSuccess() {
+ const signal = $signal?.[routePath as string];
+ if (signal) {
+ signal.set(!signal.get());
+ }
+ },
+ });
+ },
+ });
},
- });
+ };
+ return new Proxy(routes, handler);
}
diff --git a/packages/better-auth/src/client/react.ts b/packages/better-auth/src/client/react.ts
index 00ce895ff7..18a96f9001 100644
--- a/packages/better-auth/src/client/react.ts
+++ b/packages/better-auth/src/client/react.ts
@@ -1,2 +1,26 @@
import { useStore } from "@nanostores/react";
+import { createVanillaClient } from "./base";
+import { BetterFetchOption } from "@better-fetch/fetch";
+import { BetterAuth } from "../auth";
+
+export const createAuthClient = (
+ options?: BetterFetchOption,
+) => {
+ const client = createVanillaClient(options);
+ function useSession() {
+ return useStore(client.$atoms.$session);
+ }
+ function useActiveOrganization() {
+ return useStore(client.$atoms.$activeOrganization);
+ }
+ function useListOrganization() {
+ return useStore(client.$atoms.$listOrganizations);
+ }
+ return Object.assign(client, {
+ useSession,
+ useActiveOrganization,
+ useListOrganization,
+ });
+};
+
export const useAuthStore = useStore;
diff --git a/packages/better-auth/src/client/session-atom.ts b/packages/better-auth/src/client/session-atom.ts
index ceb99cb9cf..3a9a27e5bb 100644
--- a/packages/better-auth/src/client/session-atom.ts
+++ b/packages/better-auth/src/client/session-atom.ts
@@ -1,6 +1,6 @@
import { atom, computed, task } from "nanostores";
import { Session, User } from "../adapters/schema";
-import { Prettify } from "../types/helper";
+import { Prettify, UnionToIntersection } from "../types/helper";
import { BetterAuth } from "../auth";
import { FieldAttribute, InferFieldOutput } from "../db";
import { BetterFetch } from "@better-fetch/fetch";
@@ -54,8 +54,10 @@ export function getSessionAtom(client: BetterFetch) {
: {}
: {};
- type UserWithAdditionalFields = User & AdditionalUserFields;
- type SessionWithAdditionalFields = Session & AdditionalSessionFields;
+ type UserWithAdditionalFields = User &
+ UnionToIntersection;
+ type SessionWithAdditionalFields = Session &
+ UnionToIntersection;
const $signal = atom(false);
const $session = computed($signal, () =>
@@ -70,5 +72,5 @@ export function getSessionAtom(client: BetterFetch) {
} | null;
}),
);
- return { $session };
+ return { $session, $sessionSignal: $signal };
}
diff --git a/packages/better-auth/src/client/solid.ts b/packages/better-auth/src/client/solid.ts
deleted file mode 100644
index 9b1b9055c9..0000000000
--- a/packages/better-auth/src/client/solid.ts
+++ /dev/null
@@ -1,2 +0,0 @@
-import { useStore } from "@nanostores/solid";
-export const useAuthStore = useStore;
diff --git a/packages/better-auth/src/client/type.ts b/packages/better-auth/src/client/type.ts
index 4f45a65232..001dccdba0 100644
--- a/packages/better-auth/src/client/type.ts
+++ b/packages/better-auth/src/client/type.ts
@@ -1,67 +1,11 @@
-import { Context, Endpoint } from "better-call";
-import { CamelCase } from "type-fest";
-import {
- Prettify,
- HasRequiredKeys,
- UnionToIntersection,
-} from "../types/helper";
-import { BetterFetchResponse } from "@better-fetch/fetch";
+import { UnionToIntersection } from "../types/helper";
import { BetterAuth } from "../auth";
import { CustomProvider } from "../providers";
+import { BetterFetchOption } from "@better-fetch/fetch";
+import type { useAuthStore as reactStore } from "./react";
+import type { useAuthStore as vueStore } from "./vue";
-export type InferKeys = T extends `/${infer A}/${infer B}`
- ? CamelCase<`${A}-${InferKeys}`>
- : T extends `${infer I}/:${infer _}`
- ? I
- : T extends `${infer I}:${infer _}`
- ? I
- : T extends `/${infer I}`
- ? CamelCase
- : CamelCase;
-
-export type InferActions = Actions extends {
- [key: string]: infer T;
-}
- ? UnionToIntersection<
- T extends Endpoint
- ? {
- [key in InferKeys]: T extends (ctx: infer C) => infer R
- ? C extends Context
- ? (
- ...data: HasRequiredKeys extends true
- ? [Prettify]
- : [Prettify?]
- ) => Promise>>
- : never
- : never;
- }
- : never
- >
- : never;
-
-export type ExcludeCredentialPaths =
- Auth["options"]["emailAndPassword"] extends {
- enabled: true;
- }
- ? ""
- : "signUpCredential" | "signInCredential";
-
-export type ExcludedPasskeyPaths =
- | "passkeyGenerateAuthenticateOptions"
- | "passkeyGenerateRegisterOptions"
- | "verifyPasskey";
-
-export type ExcludedPaths =
- | "signinOauth"
- | "signUpOauth"
- | "callback"
- | "session"
- | ExcludeCredentialPaths
- | ExcludedPasskeyPaths;
-
-export type OrganizationPaths = "$activeOrganization" | "setActiveOrg";
-
-type ProviderEndpoint = UnionToIntersection<
+export type ProviderEndpoint = UnionToIntersection<
Auth["options"]["providers"] extends Array
? T extends CustomProvider
? T["endpoints"]
@@ -69,32 +13,5 @@ type ProviderEndpoint = UnionToIntersection<
: {}
>;
-export type Actions = ProviderEndpoint &
- Auth["api"];
-
-export type InferredActions = Prettify<
- Omit>, ExcludedPaths>
->;
-
-export type PickOrganizationPaths =
- Auth["options"]["plugins"] extends Array
- ? T extends {
- id: "organization";
- }
- ? OrganizationPaths
- : never
- : never;
-
-export type PickProvidePaths<
- ID extends string,
- PickedPath extends string,
- Auth extends BetterAuth,
-> = Auth["options"]["providers"] extends Array
- ? P extends {
- id: ID;
- }
- ? PickedPath
- : never
- : never;
-
-export type PickDefaultPaths = "$session";
+export type AuthStore = typeof reactStore | typeof vueStore;
+export interface ClientOptions extends BetterFetchOption {}
diff --git a/packages/better-auth/src/client/vue.ts b/packages/better-auth/src/client/vue.ts
index 57c248f949..738caf4f5a 100644
--- a/packages/better-auth/src/client/vue.ts
+++ b/packages/better-auth/src/client/vue.ts
@@ -1,2 +1,23 @@
import { useStore } from "@nanostores/vue";
+import { BetterFetchOption } from "@better-fetch/fetch";
+import { createVanillaClient } from "./base";
+
+export const createAuthClient = (options?: BetterFetchOption) => {
+ const client = createVanillaClient(options);
+ function useSession() {
+ return useStore(client.$atoms.$session);
+ }
+ function useActiveOrganization() {
+ return useStore(client.$atoms.$activeOrganization);
+ }
+ function useListOrganization() {
+ return useStore(client.$atoms.$listOrganizations);
+ }
+ return Object.assign(client, {
+ useSession,
+ useActiveOrganization,
+ useListOrganization,
+ });
+};
+
export const useAuthStore = useStore;
diff --git a/packages/better-auth/src/plugins/two-factor/backup-codes/index.ts b/packages/better-auth/src/plugins/two-factor/backup-codes/index.ts
index 1c2488d856..a2dbcccb64 100644
--- a/packages/better-auth/src/plugins/two-factor/backup-codes/index.ts
+++ b/packages/better-auth/src/plugins/two-factor/backup-codes/index.ts
@@ -3,7 +3,7 @@ import { TwoFactorProvider, UserWithTwoFactor } from "../types";
import { symmetricDecrypt, symmetricEncrypt } from "../../../crypto";
import { z } from "zod";
import { createAuthEndpoint } from "../../../api/call";
-import { verifyTwoFactorMiddleware } from "../verify-middleware";
+import { verifyTwoFactorMiddleware } from "../two-fa-middleware";
import { sessionMiddleware } from "../../../api/middlewares/session";
export interface BackupCodeOptions {
@@ -78,37 +78,37 @@ export function getBackupCodes(user: UserWithTwoFactor, key: string) {
export const backupCode2fa = (options?: BackupCodeOptions) => {
return {
id: "backup_code",
- verify: createAuthEndpoint(
- "/verify/backup-code",
- {
- method: "POST",
- body: z.object({
- code: z.string(),
- }),
- use: [verifyTwoFactorMiddleware],
- },
- async (ctx) => {
- const validate = verifyBackupCode(
- {
- user: ctx.context.session.user,
- code: ctx.body.code,
- },
- ctx.context.secret,
- );
- if (!validate) {
- return ctx.json(
- { status: false },
+ endpoints: {
+ verifyBackupCode: createAuthEndpoint(
+ "/two-factor/verify-backup-code",
+ {
+ method: "POST",
+ body: z.object({
+ code: z.string(),
+ }),
+ use: [verifyTwoFactorMiddleware],
+ },
+ async (ctx) => {
+ const validate = verifyBackupCode(
{
- status: 401,
+ user: ctx.context.session.user,
+ code: ctx.body.code,
},
+ ctx.context.secret,
);
- }
- return ctx.json({ status: true });
- },
- ),
- customActions: {
+ if (!validate) {
+ return ctx.json(
+ { status: false },
+ {
+ status: 401,
+ },
+ );
+ }
+ return ctx.json({ status: true });
+ },
+ ),
generateBackupCodes: createAuthEndpoint(
- "/generate/backup-codes",
+ "/two-factor/generate-backup-codes",
{
method: "POST",
use: [sessionMiddleware],
diff --git a/packages/better-auth/src/plugins/two-factor/index.ts b/packages/better-auth/src/plugins/two-factor/index.ts
index 29b88c1f26..3ac7000d1b 100644
--- a/packages/better-auth/src/plugins/two-factor/index.ts
+++ b/packages/better-auth/src/plugins/two-factor/index.ts
@@ -6,7 +6,7 @@ import { TwoFactorOptions, UserWithTwoFactor } from "./types";
import {
twoFactorMiddleware,
verifyTwoFactorMiddleware,
-} from "./verify-middleware";
+} from "./two-fa-middleware";
import { sessionMiddleware } from "../../api/middlewares/session";
import { alphabet, generateRandomString } from "oslo/crypto";
import { backupCode2fa, generateBackupCodes } from "./backup-codes";
@@ -24,11 +24,11 @@ export const twoFactor = (options: O) => {
return {
id: "two-factor",
endpoints: {
- ...totp.customActions,
- ...otp.customActions,
- ...backupCode.customActions,
+ ...totp.endpoints,
+ ...otp.endpoints,
+ ...backupCode.endpoints,
enableTwoFactor: createAuthEndpoint(
- "/enable/two-factor",
+ "/two-factor/enable",
{
method: "POST",
use: [sessionMiddleware],
@@ -40,6 +40,7 @@ export const twoFactor = (options: O) => {
key: ctx.context.secret,
data: secret,
});
+ console.log({ encryptedSecret });
const backupCodes = await generateBackupCodes(
ctx.context.secret,
options.backupCodeOptions,
@@ -62,7 +63,7 @@ export const twoFactor = (options: O) => {
},
),
disableTwoFactor: createAuthEndpoint(
- "/disable/two-factor",
+ "/two-factor/disable",
{
method: "POST",
use: [sessionMiddleware],
@@ -84,51 +85,6 @@ export const twoFactor = (options: O) => {
return ctx.json({ status: true });
},
),
- verifyTwoFactor: createAuthEndpoint(
- "/verify/two-factor",
- {
- method: "POST",
- body: z.object({
- /**
- * The code to validate
- */
- code: z.string(),
- with: z.enum(["totp", "otp", "backup_code"]),
- callbackURL: z.string().optional(),
- }),
- use: [verifyTwoFactorMiddleware],
- },
- async (ctx) => {
- const providerId = ctx.body.with;
- const provider = providers.find((p) => p.id === providerId);
- if (!provider) {
- return ctx.json(
- { status: false },
- {
- status: 401,
- },
- );
- }
- const res = await provider.verify(ctx);
- if (!res.status) {
- return ctx.json(
- { status: false },
- {
- status: 401,
- },
- );
- }
- await ctx.context.createSession();
- if (ctx.body.callbackURL) {
- return ctx.json({
- status: true,
- callbackURL: ctx.body.callbackURL,
- redirect: true,
- });
- }
- return ctx.json({ status: true });
- },
- ),
},
options: options,
middlewares: [
@@ -149,18 +105,11 @@ export const twoFactor = (options: O) => {
type: "string",
required: false,
},
- backupCodes: {
+ twoFactorBackupCodes: {
type: "string",
required: false,
returned: false,
},
- /**
- * list of two factor providers id separated by comma
- */
- twoFactorProviders: {
- type: "string",
- required: false,
- },
},
},
},
diff --git a/packages/better-auth/src/plugins/two-factor/otp/index.ts b/packages/better-auth/src/plugins/two-factor/otp/index.ts
index e5f2a31d0f..5006153022 100644
--- a/packages/better-auth/src/plugins/two-factor/otp/index.ts
+++ b/packages/better-auth/src/plugins/two-factor/otp/index.ts
@@ -6,7 +6,7 @@ import { generateHOTP } from "oslo/otp";
import { generateRandomInteger } from "oslo/crypto";
import { OTP_RANDOM_NUMBER_COOKIE_NAME } from "../constant";
import { z } from "zod";
-import { verifyTwoFactorMiddleware } from "../verify-middleware";
+import { verifyTwoFactorMiddleware } from "../two-fa-middleware";
export interface OTPOptions {
/**
@@ -25,11 +25,11 @@ export const otp2fa = (options?: OTPOptions) => {
/**
* Generate OTP and send it to the user.
*/
- const generateOTP = createAuthEndpoint(
- "/generate/otp",
+ const send2FaOTP = createAuthEndpoint(
+ "/two-factor/send-otp",
{
method: "POST",
- use: [sessionMiddleware],
+ use: [verifyTwoFactorMiddleware],
},
async (ctx) => {
if (!options || !options.sendOTP) {
@@ -63,7 +63,7 @@ export const otp2fa = (options?: OTPOptions) => {
);
const verifyOTP = createAuthEndpoint(
- "/verify/otp",
+ "/two-factor/verify-otp",
{
method: "POST",
body: z.object({
@@ -86,34 +86,35 @@ export const otp2fa = (options?: OTPOptions) => {
ctx.context.secret,
);
if (!randomNumber) {
- throw new APIError("BAD_REQUEST", {
- message: "counter cookie not found",
+ throw new APIError("UNAUTHORIZED", {
+ message: "OTP is expired",
});
}
const toCheckOtp = await generateHOTP(
Buffer.from(ctx.context.secret),
parseInt(randomNumber),
);
+ console.log(toCheckOtp, ctx.body.code);
- if (toCheckOtp !== ctx.body.code) {
- await ctx.context.createSession();
- return ctx.json({ status: true });
+ if (toCheckOtp === ctx.body.code) {
+ ctx.setCookie(cookie.name, "", {
+ path: "/",
+ sameSite: "lax",
+ httpOnly: true,
+ secure: false,
+ maxAge: 0,
+ });
+ return ctx.context.valid();
} else {
- return ctx.json(
- { status: false },
- {
- status: 401,
- },
- );
+ return ctx.context.invalid();
}
},
);
-
return {
id: "otp",
- verify: verifyOTP,
- customActions: {
- generateOTP: generateOTP,
+ endpoints: {
+ send2FaOTP,
+ verifyOTP,
},
} satisfies TwoFactorProvider;
};
diff --git a/packages/better-auth/src/plugins/two-factor/totp/index.ts b/packages/better-auth/src/plugins/two-factor/totp/index.ts
index a5c830ed65..9ff8e561a8 100644
--- a/packages/better-auth/src/plugins/two-factor/totp/index.ts
+++ b/packages/better-auth/src/plugins/two-factor/totp/index.ts
@@ -1,13 +1,12 @@
import { TimeSpan } from "oslo";
-import { alphabet, generateRandomString } from "oslo/crypto";
import { TOTPController, createTOTPKeyURI } from "oslo/otp";
import { z } from "zod";
import { createAuthEndpoint } from "../../../api/call";
import { sessionMiddleware } from "../../../api/middlewares/session";
import { APIError } from "better-call";
import { TwoFactorProvider, UserWithTwoFactor } from "../types";
-import { verifyTwoFactorMiddleware } from "../verify-middleware";
-import { BackupCodeOptions, generateBackupCodes } from "../backup-codes";
+import { verifyTwoFactorMiddleware } from "../two-fa-middleware";
+import { BackupCodeOptions } from "../backup-codes";
import { symmetricDecrypt } from "../../../crypto";
export type TOTPOptions = {
@@ -36,105 +35,10 @@ export const totp2fa = (options: TOTPOptions) => {
const opts = {
digits: 6,
period: new TimeSpan(options?.period || 30, "s"),
- secret: {
- field: "twoFactorSecret",
- },
};
- const enableTOTP = createAuthEndpoint(
- "/enable/totp",
- {
- method: "POST",
- use: [sessionMiddleware],
- },
- async (ctx) => {
- if (!options) {
- ctx.context.logger.error(
- "totp isn't configured. please pass totp option on two factor plugin to enable totp",
- );
- throw new APIError("BAD_REQUEST", {
- message: "totp isn't configured",
- });
- }
- const secret = generateRandomString(16, alphabet("a-z", "0-9", "-"));
- const user = ctx.context.session.user as UserWithTwoFactor;
- const uri = createTOTPKeyURI(
- options.issuer || "BetterAuth",
- user.name,
- Buffer.from(secret),
- opts,
- );
- const backupCodes = await generateBackupCodes(
- secret,
- options.backupCodes,
- );
- await ctx.context.adapter.update({
- model: "user",
- update: {
- twoFactorSecret: secret,
- twoFactorEnabled: true,
- backupCodes: backupCodes.encryptedBackupCodes,
- },
- where: [
- {
- field: "id",
- value: user.id,
- },
- ],
- });
- return ctx.json({ uri, backupCodes: backupCodes.backupCodes });
- },
- );
-
- async function enable(user: UserWithTwoFactor) {
- const secret = generateRandomString(16, alphabet("a-z", "0-9", "-"));
- const uri = createTOTPKeyURI(
- options.issuer,
- user.name,
- Buffer.from(secret),
- opts,
- );
- const backupCodes = await generateBackupCodes(secret, options.backupCodes);
- return {
- uri,
- backupCodes: backupCodes.backupCodes,
- };
- }
-
- const disableTOTP = createAuthEndpoint(
- "/disable/totp",
- {
- method: "POST",
- use: [sessionMiddleware],
- },
- async (ctx) => {
- if (!options) {
- ctx.context.logger.error(
- "totp isn't configured. please pass totp option on two factor plugin to enable totp",
- );
- throw new APIError("BAD_REQUEST", {
- message: "totp isn't configured",
- });
- }
- const user = ctx.context.session.user as UserWithTwoFactor;
- await ctx.context.adapter.update({
- model: "user",
- update: {
- twoFactorEnabled: false,
- },
- where: [
- {
- field: "id",
- value: user.id,
- },
- ],
- });
- return ctx.json({ status: true });
- },
- );
-
const generateTOTP = createAuthEndpoint(
- "/generate/totp",
+ "/totp/generate",
{
method: "POST",
use: [sessionMiddleware],
@@ -148,16 +52,15 @@ export const totp2fa = (options: TOTPOptions) => {
message: "totp isn't configured",
});
}
- const session = ctx.context.session;
+ const session = ctx.context.session.user as UserWithTwoFactor;
const totp = new TOTPController(opts);
- const secret = (session.user as any).secret;
- const code = await totp.generate(secret);
+ const code = await totp.generate(Buffer.from(session.twoFactorSecret));
return { code };
},
);
const getTOTPURI = createAuthEndpoint(
- "/get/totp/uri",
+ "/two-factor/get-totp-uri",
{
method: "GET",
use: [sessionMiddleware],
@@ -175,7 +78,7 @@ export const totp2fa = (options: TOTPOptions) => {
return {
totpURI: createTOTPKeyURI(
options?.issuer || "BetterAuth",
- user.name,
+ user.email,
Buffer.from(user.twoFactorSecret),
opts,
),
@@ -184,7 +87,7 @@ export const totp2fa = (options: TOTPOptions) => {
);
const verifyTOTP = createAuthEndpoint(
- "/verify/totp",
+ "/two-factor/verify-totp",
{
method: "POST",
body: z.object({
@@ -210,17 +113,18 @@ export const totp2fa = (options: TOTPOptions) => {
}),
);
const status = await totp.verify(ctx.body.code, secret);
- return {
- status,
- };
+ if (!status) {
+ return ctx.context.invalid();
+ }
+ return ctx.context.valid();
},
);
return {
id: "totp",
- verify: verifyTOTP,
- customActions: {
+ endpoints: {
generateTOTP: generateTOTP,
viewTOTPURI: getTOTPURI,
+ verifyTOTP,
},
} satisfies TwoFactorProvider;
};
diff --git a/packages/better-auth/src/plugins/two-factor/verify-middleware.ts b/packages/better-auth/src/plugins/two-factor/two-fa-middleware.ts
similarity index 88%
rename from packages/better-auth/src/plugins/two-factor/verify-middleware.ts
rename to packages/better-auth/src/plugins/two-factor/two-fa-middleware.ts
index 1b4692d405..9ade2de653 100644
--- a/packages/better-auth/src/plugins/two-factor/verify-middleware.ts
+++ b/packages/better-auth/src/plugins/two-factor/two-fa-middleware.ts
@@ -5,6 +5,7 @@ import { hs256 } from "../../crypto";
import { TWO_FACTOR_COOKIE_NAME } from "./constant";
import { TwoFactorOptions, UserWithTwoFactor } from "./types";
import { z } from "zod";
+import { signInCredential } from "../../api/routes";
export const verifyTwoFactorMiddleware = createAuthMiddleware(async (ctx) => {
const cookie = await ctx.getSignedCookie(
@@ -62,7 +63,7 @@ export const verifyTwoFactorMiddleware = createAuthMiddleware(async (ctx) => {
}
if (hashToMatch === hash) {
return {
- createSession: async () => {
+ valid: async () => {
/**
* Set the session cookie
*/
@@ -72,6 +73,26 @@ export const verifyTwoFactorMiddleware = createAuthMiddleware(async (ctx) => {
ctx.context.secret,
ctx.context.authCookies.sessionToken.options,
);
+ if (ctx.body.callbackURL) {
+ return ctx.json({
+ status: true,
+ callbackURL: ctx.body.callbackURL,
+ redirect: true,
+ });
+ }
+
+ return ctx.json({ status: true });
+ },
+ invalid: async () => {
+ return ctx.json(
+ { status: false },
+ {
+ status: 401,
+ body: {
+ message: "Invalid code",
+ },
+ },
+ );
},
session: {
id: session.id,
diff --git a/packages/better-auth/src/plugins/two-factor/types.ts b/packages/better-auth/src/plugins/two-factor/types.ts
index f55aa04d2e..cea2247c9c 100644
--- a/packages/better-auth/src/plugins/two-factor/types.ts
+++ b/packages/better-auth/src/plugins/two-factor/types.ts
@@ -45,12 +45,5 @@ export interface UserWithTwoFactor extends User {
export interface TwoFactorProvider {
id: LiteralString;
- enable?: (user: UserWithTwoFactor) => Promise;
- disable?: () => Promise;
- verify: Endpoint<
- (ctx: any) => Promise<{
- status: boolean;
- }>
- >;
- customActions?: Record;
+ endpoints?: Record;
}
diff --git a/packages/better-auth/src/providers/passkey.ts b/packages/better-auth/src/providers/passkey.ts
index c81418f32a..5506fd72b4 100644
--- a/packages/better-auth/src/providers/passkey.ts
+++ b/packages/better-auth/src/providers/passkey.ts
@@ -82,6 +82,9 @@ export const passkey = (options: PasskeyOptions) => {
{
method: "GET",
use: [sessionMiddleware],
+ metadata: {
+ client: false,
+ },
},
async (ctx) => {
const session = ctx.context.session;
diff --git a/packages/better-auth/src/test-utils/test-instance.ts b/packages/better-auth/src/test-utils/test-instance.ts
index 623a42285e..a9d24554bc 100644
--- a/packages/better-auth/src/test-utils/test-instance.ts
+++ b/packages/better-auth/src/test-utils/test-instance.ts
@@ -4,9 +4,12 @@ import { beforeAll, afterAll } from "vitest";
import { type Listener, listen } from "listhen";
import { toNodeHandler } from "better-call";
import fs from "fs/promises";
+import { BetterAuthOptions } from "../types";
-export async function getTestInstance() {
- const auth = betterAuth({
+export async function getTestInstance>(
+ options?: O,
+) {
+ const opts = {
providers: [
github({
clientId: "test",
@@ -26,7 +29,12 @@ export async function getTestInstance() {
emailAndPassword: {
enabled: true,
},
- });
+ } satisfies BetterAuthOptions;
+
+ const auth = betterAuth({
+ ...opts,
+ ...options,
+ } as O extends undefined ? typeof opts : O & typeof opts);
let server: Listener;
diff --git a/packages/better-auth/src/types/options.ts b/packages/better-auth/src/types/options.ts
index da2ee8fb07..be7b02f796 100644
--- a/packages/better-auth/src/types/options.ts
+++ b/packages/better-auth/src/types/options.ts
@@ -146,11 +146,5 @@ export interface BetterAuthOptions {
* @default 32
*/
minPasswordLength?: number;
- /**
- * Two factor configuration
- */
- twoFactor?: {
- enabled: boolean;
- };
};
}
diff --git a/packages/better-auth/src/utils/cookies.ts b/packages/better-auth/src/utils/cookies.ts
index 8f45286741..232a66d128 100644
--- a/packages/better-auth/src/utils/cookies.ts
+++ b/packages/better-auth/src/utils/cookies.ts
@@ -74,7 +74,7 @@ export function createCookieGetter(options: BetterAuthOptions) {
name:
process.env.NODE_ENV === "production"
? `${secureCookiePrefix}${cookiePrefix}.${cookieName}`
- : `${cookiePrefix}${cookieName}`,
+ : `${cookiePrefix}.${cookieName}`,
options: {
secure,
sameSite: "lax",
diff --git a/packages/better-auth/tsup.config.ts b/packages/better-auth/tsup.config.ts
index f248c24c03..0aaaa52240 100644
--- a/packages/better-auth/tsup.config.ts
+++ b/packages/better-auth/tsup.config.ts
@@ -8,7 +8,6 @@ export default defineConfig({
cli: "./src/cli/index.ts",
react: "./src/client/react.ts",
preact: "./src/client/preact.ts",
- solid: "./src/client/solid.ts",
vue: "./src/client/vue.ts",
plugins: "./src/plugins/index.ts",
},
@@ -19,6 +18,7 @@ export default defineConfig({
external: [
"react",
"svelte",
+ "solid-js",
"$app/environment",
"next",
"pg",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index c443be8e4b..a634bbd302 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -127,6 +127,9 @@ importers:
react-hook-form:
specifier: ^7.52.2
version: 7.52.2(react@18.3.1)
+ react-qr-code:
+ specifier: ^2.0.15
+ version: 2.0.15(react@18.3.1)
sonner:
specifier: ^1.5.0
version: 1.5.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
@@ -198,8 +201,8 @@ importers:
specifier: ^1.9.2
version: 1.9.2
better-call:
- specifier: ^0.1.28
- version: 0.1.28(typescript@5.5.4)
+ specifier: ^0.1.33
+ version: 0.1.33(typescript@5.5.4)
chalk:
specifier: ^5.3.0
version: 5.3.0
@@ -215,6 +218,9 @@ importers:
jiti:
specifier: ^1.21.6
version: 1.21.6
+ jose:
+ specifier: ^5.7.0
+ version: 5.7.0
kysely:
specifier: ^0.27.4
version: 0.27.4
@@ -1733,8 +1739,8 @@ packages:
peerDependencies:
typescript: ^5.0.0
- better-call@0.1.28:
- resolution: {integrity: sha512-9jl3q7Mb+3lRGeJUqkHAzAHph8BKcGHsj84h1gkfcMy2skyrCUbpFuhxMNAKBDtg2ppa66wTWDv0mGLOshxtEA==}
+ better-call@0.1.33:
+ resolution: {integrity: sha512-gzthE/AnimwMCNBNyy9LRqqAtjXkqO+dR4n1OjCiUhiBK4X+NZMKekQUKIzpfwDRC4k3hCshb1LsPhqwiSM7Bw==}
peerDependencies:
typescript: ^5.0.0
@@ -2233,6 +2239,9 @@ packages:
resolution: {integrity: sha512-2yTgeWTWzMWkHu6Jp9NKgePDaYHbntiwvYuuJLbbN9vl7DC9DvXKOB2BC3ZZ92D3cvV/aflH0osDfwpHepQ53w==}
hasBin: true
+ jose@5.7.0:
+ resolution: {integrity: sha512-3P9qfTYDVnNn642LCAqIKbTGb9a1TBxZ9ti5zEVEr48aDdflgRjhspWFb6WM4PzAfFbGMJYC4+803v8riCRAKw==}
+
joycon@3.1.1:
resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==}
engines: {node: '>=10'}
@@ -2717,6 +2726,9 @@ packages:
resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==}
engines: {node: '>= 6'}
+ prop-types@15.8.1:
+ resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==}
+
pump@3.0.0:
resolution: {integrity: sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==}
@@ -2731,6 +2743,9 @@ packages:
resolution: {integrity: sha512-pMpnA0qRdFp32b1sJl1wOJNxZLQ2cbQx+k6tjNtZ8CpvVhNqEPRgivZ2WOUev2YMajecdH7ctUPDvEe87nariQ==}
engines: {node: '>=6.0.0'}
+ qr.js@0.0.0:
+ resolution: {integrity: sha512-c4iYnWb+k2E+vYpRimHqSu575b1/wKl4XFeJGpFmrJQz5I88v9aY2czh7s0w36srfCM1sXgC/xpoJz5dJfq+OQ==}
+
queue-microtask@1.2.3:
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
@@ -2755,9 +2770,17 @@ packages:
peerDependencies:
react: ^16.8.0 || ^17 || ^18 || ^19
+ react-is@16.13.1:
+ resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
+
react-is@18.3.1:
resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==}
+ react-qr-code@2.0.15:
+ resolution: {integrity: sha512-MkZcjEXqVKqXEIMVE0mbcGgDpkfSdd8zhuzXEl9QzYeNcw8Hq2oVIzDLWuZN2PQBwM5PWjc2S31K8Q1UbcFMfw==}
+ peerDependencies:
+ react: '*'
+
react-remove-scroll-bar@2.3.6:
resolution: {integrity: sha512-DtSYaao4mBmX+HDo5YWYdBWQwYIQQshUV/dVxFxK+KM26Wjwp1gZ6rv6OC3oujI6Bfu6Xyg3TwK533AQutsn/g==}
engines: {node: '>=10'}
@@ -4487,7 +4510,7 @@ snapshots:
rou3: 0.5.1
typescript: 5.5.4
- better-call@0.1.28(typescript@5.5.4):
+ better-call@0.1.33(typescript@5.5.4):
dependencies:
'@better-fetch/fetch': 1.1.4
'@types/set-cookie-parser': 2.4.10
@@ -5009,6 +5032,8 @@ snapshots:
jiti@1.21.6: {}
+ jose@5.7.0: {}
+
joycon@3.1.1: {}
js-tokens@4.0.0: {}
@@ -5469,6 +5494,12 @@ snapshots:
kleur: 3.0.3
sisteransi: 1.0.5
+ prop-types@15.8.1:
+ dependencies:
+ loose-envify: 1.4.0
+ object-assign: 4.1.1
+ react-is: 16.13.1
+
pump@3.0.0:
dependencies:
end-of-stream: 1.4.4
@@ -5482,6 +5513,8 @@ snapshots:
pvutils@1.1.3: {}
+ qr.js@0.0.0: {}
+
queue-microtask@1.2.3: {}
radix3@1.1.2: {}
@@ -5508,8 +5541,16 @@ snapshots:
dependencies:
react: 18.3.1
+ react-is@16.13.1: {}
+
react-is@18.3.1: {}
+ react-qr-code@2.0.15(react@18.3.1):
+ dependencies:
+ prop-types: 15.8.1
+ qr.js: 0.0.0
+ react: 18.3.1
+
react-remove-scroll-bar@2.3.6(@types/react@18.3.3)(react@18.3.1):
dependencies:
react: 18.3.1
diff --git a/todo.md b/todo.md
new file mode 100644
index 0000000000..e354c3a6a8
--- /dev/null
+++ b/todo.md
@@ -0,0 +1,2 @@
+## TODO
+[ ] handle migration when the config removes existing schema
\ No newline at end of file