diff --git a/dev/bc-fe/hono/db.sqlite b/dev/bc-fe/hono/db.sqlite
index a0bf2dde41..ce459eab41 100644
Binary files a/dev/bc-fe/hono/db.sqlite and b/dev/bc-fe/hono/db.sqlite differ
diff --git a/dev/bc-fe/react/src/App.tsx b/dev/bc-fe/react/src/App.tsx
index 625fa22b3a..b1c1f40301 100644
--- a/dev/bc-fe/react/src/App.tsx
+++ b/dev/bc-fe/react/src/App.tsx
@@ -21,6 +21,9 @@ function App() {
{session.user.name}
+
+ {session.user.username}
+
{session.user.email}
@@ -58,6 +61,7 @@ function App() {
}}>
Continue with github
+
)
@@ -73,6 +77,7 @@ export default App;
function SignUp() {
const [email, setEmail] = useState("")
const [name, setName] = useState("")
+ const [username, setUsername] = useState("")
const [password, setPassword] = useState("")
return (
setName(e.target.value)}
/>
+ setUsername(e.target.value)}
+ />
setPassword(e.target.value)}
/>
)
+}
+
+
+
+function SignIn() {
+ const [email, setEmail] = useState("")
+ const [password, setPassword] = useState("")
+ return (
+
+ setEmail(e.target.value)}
+ />
+
+ setPassword(e.target.value)}
+ />
+
+
+ )
}
\ No newline at end of file
diff --git a/dev/bc-fe/react/src/lib/auth.ts b/dev/bc-fe/react/src/lib/auth.ts
index 2e7d688a55..c9ad1c643e 100644
--- a/dev/bc-fe/react/src/lib/auth.ts
+++ b/dev/bc-fe/react/src/lib/auth.ts
@@ -1,7 +1,12 @@
import { createAuthClient } from "better-auth/react";
-import { twoFactorClient } from "better-auth/client";
+import { twoFactorClient, usernameClient } from "better-auth/client";
export const auth = createAuthClient({
baseURL: "http://localhost:3000/api/auth",
- authPlugins: [twoFactorClient],
+ authPlugins: [
+ twoFactorClient({
+ twoFactorPage: "/two-factor",
+ }),
+ usernameClient,
+ ],
});
diff --git a/packages/better-auth/src/api/index.ts b/packages/better-auth/src/api/index.ts
index 56b9281cd1..94f48ac06f 100644
--- a/packages/better-auth/src/api/index.ts
+++ b/packages/better-auth/src/api/index.ts
@@ -10,14 +10,14 @@ import {
getSession,
resetPassword,
sendVerificationEmail,
- signInCredential,
+ signInEmail,
signInOAuth,
signOut,
verifyEmail,
} from "./routes";
import { getCSRFToken } from "./routes/csrf";
import { ok, welcome } from "./routes/ok";
-import { signUpCredential } from "./routes/sign-up";
+import { signUpEmail } from "./routes/sign-up";
import { error } from "./routes/error";
import type { z, ZodAny, ZodObject, ZodOptional, ZodString } from "zod";
@@ -85,8 +85,8 @@ export const router = (
getCSRFToken,
getSession: typedSession,
signOut,
- signUpCredential,
- signInCredential,
+ signUpEmail,
+ signInEmail,
forgetPassword,
resetPassword,
verifyEmail,
@@ -136,10 +136,10 @@ export const router = (
for (const hook of plugin.hooks.after) {
const match = hook.matcher(context);
if (match) {
- const hookRes = await hook.handler({
- ...context,
+ const obj = Object.assign(context, {
returned: endpointRes,
});
+ const hookRes = await hook.handler(obj);
if (hookRes && "response" in hookRes) {
response = hookRes.response as any;
}
diff --git a/packages/better-auth/src/api/routes/sign-in.ts b/packages/better-auth/src/api/routes/sign-in.ts
index ed3341aab8..5d8d0032e5 100644
--- a/packages/better-auth/src/api/routes/sign-in.ts
+++ b/packages/better-auth/src/api/routes/sign-in.ts
@@ -39,7 +39,6 @@ export const signInOAuth = createAuthEndpoint(
if (!provider) {
throw new APIError("NOT_FOUND");
}
-
const cookie = c.context.authCookies;
const currentURL = c.query?.currentURL
? new URL(c.query?.currentURL)
@@ -78,8 +77,8 @@ export const signInOAuth = createAuthEndpoint(
},
);
-export const signInCredential = createAuthEndpoint(
- "/sign-in/email-password",
+export const signInEmail = createAuthEndpoint(
+ "/sign-in/email",
{
method: "POST",
body: z.object({
diff --git a/packages/better-auth/src/api/routes/sign-up.ts b/packages/better-auth/src/api/routes/sign-up.ts
index 6c21e5b81f..f8682d0b28 100644
--- a/packages/better-auth/src/api/routes/sign-up.ts
+++ b/packages/better-auth/src/api/routes/sign-up.ts
@@ -3,8 +3,8 @@ import { Argon2id } from "oslo/password";
import { z } from "zod";
import { createAuthEndpoint } from "../call";
-export const signUpCredential = createAuthEndpoint(
- "/sign-up/credential",
+export const signUpEmail = createAuthEndpoint(
+ "/sign-up/email",
{
method: "POST",
body: z.object({
diff --git a/packages/better-auth/src/client/base.ts b/packages/better-auth/src/client/base.ts
index fe57a0af7a..2d31191f27 100644
--- a/packages/better-auth/src/client/base.ts
+++ b/packages/better-auth/src/client/base.ts
@@ -28,15 +28,25 @@ export const createAuthClient = (
: {}) &
Auth["api"]
: Auth["api"];
+ /**
+ * used for plugins only
+ */
+ const $baseFetch = createFetch({
+ ...options,
+ baseURL: getBaseURL(options?.baseURL).withPath,
+ });
const $fetch = createFetch({
method: "GET",
...options,
baseURL: getBaseURL(options?.baseURL).withPath,
plugins: [
+ ...(options?.plugins || []),
+ ...(options?.authPlugins
+ ?.flatMap((plugin) => plugin($baseFetch).fetchPlugins)
+ .filter((plugin) => plugin !== undefined) || []),
+ ...(options?.csrfPlugin !== false ? [csrfPlugin] : []),
redirectPlugin,
addCurrentURL,
- ...(options?.csrfPlugin !== false ? [csrfPlugin] : []),
- ...(options?.plugins || []),
],
});
@@ -121,11 +131,10 @@ export const createAuthClient = (
atom: "$activeOrgSignal",
},
{
- matcher: (path) => path === "/sign-out",
- atom: "$sessionSignal",
- },
- {
- matcher: (path) => path.startsWith("/sign-up"),
+ matcher: (path) =>
+ path === "/sign-out" ||
+ path.startsWith("/sign-up") ||
+ path.startsWith("/sign-in"),
atom: "$sessionSignal",
},
...pluginProxySignals,
diff --git a/packages/better-auth/src/client/client.test.ts b/packages/better-auth/src/client/client.test.ts
index 749bf812cd..ab2cc2c944 100644
--- a/packages/better-auth/src/client/client.test.ts
+++ b/packages/better-auth/src/client/client.test.ts
@@ -30,7 +30,13 @@ describe("client path to object", async () => {
client.$atoms.$session;
const client2 = createReactClient({
- authPlugins: [organization, twoFactorClient, usernameClient],
+ authPlugins: [
+ organization,
+ twoFactorClient({
+ twoFactorPage: "/two-factor",
+ }),
+ usernameClient,
+ ],
});
});
});
diff --git a/packages/better-auth/src/client/create-client-plugin.ts b/packages/better-auth/src/client/create-client-plugin.ts
index f142a6392b..16626933ba 100644
--- a/packages/better-auth/src/client/create-client-plugin.ts
+++ b/packages/better-auth/src/client/create-client-plugin.ts
@@ -1,4 +1,4 @@
-import type { BetterFetch } from "@better-fetch/fetch";
+import type { BetterFetch, BetterFetchPlugin } from "@better-fetch/fetch";
import type { Endpoint } from "better-call";
import type { AuthProxySignal } from "./proxy";
import type { Atom, PreinitializedWritableAtom } from "nanostores";
@@ -25,6 +25,7 @@ export const createClientPlugin = () => {
atoms?: Record>;
integrations?: Integrations;
pathMethods?: Record;
+ fetchPlugins?: BetterFetchPlugin[];
},
) => {
return ($fetch: BetterFetch) => {
diff --git a/packages/better-auth/src/client/fetch-plugins.ts b/packages/better-auth/src/client/fetch-plugins.ts
index 1dfb3f29ec..2ce822d53e 100644
--- a/packages/better-auth/src/client/fetch-plugins.ts
+++ b/packages/better-auth/src/client/fetch-plugins.ts
@@ -7,7 +7,6 @@ export const redirectPlugin = {
hooks: {
onSuccess(context) {
if (context.data?.url && context.data?.redirect) {
- console.log("redirecting to", context.data.url);
window.location.href = context.data.url;
}
},
diff --git a/packages/better-auth/src/client/proxy.ts b/packages/better-auth/src/client/proxy.ts
index c6baebcbc1..544aef2359 100644
--- a/packages/better-auth/src/client/proxy.ts
+++ b/packages/better-auth/src/client/proxy.ts
@@ -64,12 +64,18 @@ export function createDynamicPathProxy>(
body: method === "GET" ? undefined : body,
query: query,
method,
- onSuccess() {
+ async onSuccess(context) {
const signal = $signal?.find((s) => s.matcher(routePath));
if (!signal) return;
const signalAtom = $signals?.[signal.atom];
if (!signalAtom) return;
signalAtom.set(!signalAtom.get());
+ /**
+ * call if options.onSuccess
+ * is passed since we are
+ * overriding onSuccess
+ */
+ await options?.onSuccess?.(context);
},
});
},
diff --git a/packages/better-auth/src/client/type.ts b/packages/better-auth/src/client/type.ts
index f7db5f290e..b3121f21f5 100644
--- a/packages/better-auth/src/client/type.ts
+++ b/packages/better-auth/src/client/type.ts
@@ -1,4 +1,8 @@
-import type { BetterFetch, BetterFetchOption } from "@better-fetch/fetch";
+import type {
+ BetterFetch,
+ BetterFetchOption,
+ BetterFetchPlugin,
+} from "@better-fetch/fetch";
import type { Auth } from "../auth";
import type { UnionToIntersection } from "../types/helper";
import type { useAuthStore as reactStore } from "./react";
@@ -30,6 +34,7 @@ export type AuthPlugin = ($fetch: BetterFetch) => {
preact?: (useStore: typeof preactStore) => Record;
};
pathMethods?: Record;
+ fetchPlugins?: BetterFetchPlugin[];
};
export interface ClientOptions extends BetterFetchOption {
/**
diff --git a/packages/better-auth/src/plugins/bearer/index.ts b/packages/better-auth/src/plugins/bearer/index.ts
index 19ceed8cd0..a9a7774021 100644
--- a/packages/better-auth/src/plugins/bearer/index.ts
+++ b/packages/better-auth/src/plugins/bearer/index.ts
@@ -19,7 +19,7 @@ export const bearer = () => {
?.startsWith("Bearer ") || false
);
},
- handler: createAuthMiddleware(async (ctx) => {
+ handler: async (ctx) => {
const token = ctx.request?.headers
.get("authorization")
?.replace("Bearer ", "");
@@ -38,7 +38,7 @@ export const bearer = () => {
ctx.context.authCookies.sessionToken.name
}=${signedToken.replace("=", "")}`,
);
- }),
+ },
},
],
},
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 f12cc0546a..f3cc0a7685 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 { z } from "zod";
import { createAuthEndpoint } from "../../../api/call";
import { sessionMiddleware } from "../../../api/middlewares/session";
import { symmetricDecrypt, symmetricEncrypt } from "../../../crypto";
-import { verifyTwoFactorMiddleware } from "../two-fa-middleware";
+import { verifyTwoFactorMiddleware } from "../verify-middleware";
import type { TwoFactorProvider, UserWithTwoFactor } from "../types";
export interface BackupCodeOptions {
diff --git a/packages/better-auth/src/plugins/two-factor/client.ts b/packages/better-auth/src/plugins/two-factor/client.ts
index 263f7c299f..34926504b8 100644
--- a/packages/better-auth/src/plugins/two-factor/client.ts
+++ b/packages/better-auth/src/plugins/two-factor/client.ts
@@ -1,8 +1,21 @@
import { createClientPlugin } from "../../client/create-client-plugin";
import type { twoFactor as twoFa } from "../../plugins/two-factor";
-export const twoFactorClient = createClientPlugin>()(
- ($fetch) => {
+export const twoFactorClient = (
+ options: {
+ twoFactorPage: string;
+ /**
+ * Redirect to the two factor page. If twoFactorPage
+ * is not set this will redirect to the root path.
+ * @default true
+ */
+ redirect?: boolean;
+ } = {
+ redirect: true,
+ twoFactorPage: "/",
+ },
+) => {
+ return createClientPlugin>()(($fetch) => {
return {
id: "two-factor",
authProxySignal: [
@@ -18,6 +31,21 @@ export const twoFactorClient = createClientPlugin>()(
"/two-factor/enable": "POST",
"/two-factor/send-otp": "POST",
},
+ fetchPlugins: [
+ {
+ id: "two-factor",
+ name: "two-factor",
+ hooks: {
+ async onSuccess(context) {
+ if (context.data?.twoFactorRedirect) {
+ if (options.redirect) {
+ window.location.href = options.twoFactorPage;
+ }
+ }
+ },
+ },
+ },
+ ],
};
- },
-);
+ });
+};
diff --git a/packages/better-auth/src/plugins/two-factor/index.ts b/packages/better-auth/src/plugins/two-factor/index.ts
index 95e6e9f5c8..6bf597e578 100644
--- a/packages/better-auth/src/plugins/two-factor/index.ts
+++ b/packages/better-auth/src/plugins/two-factor/index.ts
@@ -1,17 +1,15 @@
import { alphabet, generateRandomString } from "oslo/crypto";
import { z } from "zod";
-import { createAuthEndpoint } from "../../api/call";
+import { createAuthEndpoint, createAuthMiddleware } from "../../api/call";
import { sessionMiddleware } from "../../api/middlewares/session";
-import { symmetricEncrypt } from "../../crypto";
+import { hs256, symmetricEncrypt } from "../../crypto";
import type { BetterAuthPlugin } from "../../types/plugins";
import { backupCode2fa, generateBackupCodes } from "./backup-codes";
import { otp2fa } from "./otp";
import { totp2fa } from "./totp";
-import {
- twoFactorMiddleware,
- verifyTwoFactorMiddleware,
-} from "./two-fa-middleware";
+
import type { TwoFactorOptions, UserWithTwoFactor } from "./types";
+import type { Session } from "../../adapters/schema";
export const twoFactor = (options: O) => {
const totp = totp2fa({
@@ -86,12 +84,65 @@ export const twoFactor = (options: O) => {
),
},
options: options,
- middlewares: [
- {
- path: "/sign-in/credential",
- middleware: twoFactorMiddleware(options),
- },
- ],
+ hooks: {
+ after: [
+ {
+ matcher(context) {
+ return (
+ context.path === "/sign-in/email" ||
+ context.path === "/sign-in/username"
+ );
+ },
+ handler: createAuthMiddleware(async (ctx) => {
+ const returned = (await (ctx as any).returned) as Response;
+ if (returned?.status !== 200) {
+ return;
+ }
+ const response = (await returned.json()) as {
+ user: UserWithTwoFactor;
+ session: Session;
+ };
+ if (!response.user.twoFactorEnabled) {
+ return;
+ }
+ /**
+ * remove the session cookie. It's set by the sign in credential
+ */
+ ctx.setCookie(ctx.context.authCookies.sessionToken.name, "", {
+ path: "/",
+ sameSite: "lax",
+ httpOnly: true,
+ secure: false,
+ maxAge: 0,
+ });
+ const hash = await hs256(ctx.context.secret, response.session.id);
+ /**
+ * We set the user id and the session
+ * id as a hash. Later will fetch for
+ * sessions with the user id compare
+ * the hash and set that as session.
+ */
+ await ctx.setSignedCookie(
+ "better-auth.two-factor",
+ `${response.session.userId}!${hash}`,
+ ctx.context.secret,
+ ctx.context.authCookies.sessionToken.options,
+ );
+ const res = new Response(
+ JSON.stringify({
+ twoFactorRedirect: true,
+ }),
+ {
+ headers: ctx.responseHeader,
+ },
+ );
+ return {
+ response: res,
+ };
+ }),
+ },
+ ],
+ },
schema: {
user: {
fields: {
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 f39130990b..8619154293 100644
--- a/packages/better-auth/src/plugins/two-factor/otp/index.ts
+++ b/packages/better-auth/src/plugins/two-factor/otp/index.ts
@@ -3,9 +3,8 @@ import { generateRandomInteger } from "oslo/crypto";
import { generateHOTP } from "oslo/otp";
import { z } from "zod";
import { createAuthEndpoint } from "../../../api/call";
-import { sessionMiddleware } from "../../../api/middlewares/session";
import { OTP_RANDOM_NUMBER_COOKIE_NAME } from "../constant";
-import { verifyTwoFactorMiddleware } from "../two-fa-middleware";
+import { verifyTwoFactorMiddleware } from "../verify-middleware";
import type { TwoFactorProvider, UserWithTwoFactor } from "../types";
export interface OTPOptions {
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 9cd1bb4fe7..ce6c95c8bc 100644
--- a/packages/better-auth/src/plugins/two-factor/totp/index.ts
+++ b/packages/better-auth/src/plugins/two-factor/totp/index.ts
@@ -6,7 +6,7 @@ import { createAuthEndpoint } from "../../../api/call";
import { sessionMiddleware } from "../../../api/middlewares/session";
import { symmetricDecrypt } from "../../../crypto";
import type { BackupCodeOptions } from "../backup-codes";
-import { verifyTwoFactorMiddleware } from "../two-fa-middleware";
+import { verifyTwoFactorMiddleware } from "../verify-middleware";
import type { TwoFactorProvider, UserWithTwoFactor } from "../types";
export type TOTPOptions = {
diff --git a/packages/better-auth/src/plugins/two-factor/two-fa-middleware.ts b/packages/better-auth/src/plugins/two-factor/verify-middleware.ts
similarity index 58%
rename from packages/better-auth/src/plugins/two-factor/two-fa-middleware.ts
rename to packages/better-auth/src/plugins/two-factor/verify-middleware.ts
index ffd1650005..3f77c6599a 100644
--- a/packages/better-auth/src/plugins/two-factor/two-fa-middleware.ts
+++ b/packages/better-auth/src/plugins/two-factor/verify-middleware.ts
@@ -1,11 +1,9 @@
import { APIError } from "better-call";
-import { z } from "zod";
import type { Session } from "../../adapters/schema";
import { createAuthMiddleware } from "../../api/call";
-import { signInCredential } from "../../api/routes";
import { hs256 } from "../../crypto";
import { TWO_FACTOR_COOKIE_NAME } from "./constant";
-import type { TwoFactorOptions, UserWithTwoFactor } from "./types";
+import type { UserWithTwoFactor } from "./types";
export const verifyTwoFactorMiddleware = createAuthMiddleware(async (ctx) => {
const cookie = await ctx.getSignedCookie(
@@ -107,69 +105,3 @@ export const verifyTwoFactorMiddleware = createAuthMiddleware(async (ctx) => {
message: "invalid two factor authentication",
});
});
-
-export const twoFactorMiddleware = (options: TwoFactorOptions) =>
- createAuthMiddleware(
- {
- body: z.object({
- email: z.string().email(),
- password: z.string(),
- /**
- * Callback URL to
- * redirect to after
- * the user has signed in.
- */
- callbackURL: z.string().optional(),
- }),
- },
- async (ctx) => {
- //@ts-ignore
- const signIn = await signInCredential({
- ...ctx,
- body: ctx.body,
- });
- if (!signIn?.user) {
- return new Response(null, {
- status: 401,
- });
- }
- const user = signIn.user as UserWithTwoFactor;
- if (!user.twoFactorEnabled) {
- return new Response(JSON.stringify(signIn), {
- headers: ctx.responseHeader,
- });
- }
- /**
- * remove the session cookie. It's set by the sign in credential
- */
- ctx.setCookie(ctx.context.authCookies.sessionToken.name, "", {
- path: "/",
- sameSite: "lax",
- httpOnly: true,
- secure: false,
- maxAge: 0,
- });
- const hash = await hs256(ctx.context.secret, signIn.session.id);
- /**
- * We set the user id and the session
- * id as a hash. Later will fetch for
- * sessions with the user id compare
- * the hash and set that as session.
- */
- await ctx.setSignedCookie(
- "better-auth.two-factor",
- `${signIn.session.userId}!${hash}`,
- ctx.context.secret,
- ctx.context.authCookies.sessionToken.options,
- );
- return new Response(
- JSON.stringify({
- url: options.twoFactorURL || ctx.body.callbackURL || "/",
- redirect: true,
- }),
- {
- headers: ctx.responseHeader,
- },
- );
- },
- );
diff --git a/packages/better-auth/src/plugins/username/index.ts b/packages/better-auth/src/plugins/username/index.ts
index d8aa9a80fc..440d2ff700 100644
--- a/packages/better-auth/src/plugins/username/index.ts
+++ b/packages/better-auth/src/plugins/username/index.ts
@@ -4,14 +4,14 @@ import type { BetterAuthPlugin } from "../../types/plugins";
import { Argon2id } from "oslo/password";
import { APIError } from "better-call";
import type { Account, User } from "../../adapters/schema";
-import { signUpCredential } from "../../api/routes/sign-up";
+import { signUpEmail } from "../../api/routes/sign-up";
export const username = () => {
return {
id: "username",
endpoints: {
signInUsername: createAuthEndpoint(
- "/sign-in/username-password",
+ "/sign-in/username",
{
method: "POST",
body: z.object({
@@ -102,7 +102,7 @@ export const username = () => {
{
method: "POST",
body: z.object({
- username: z.string().min(3).max(20).optional(),
+ username: z.string().min(3).max(20),
name: z.string(),
email: z.string().email(),
password: z.string(),
@@ -111,7 +111,11 @@ export const username = () => {
}),
},
async (ctx) => {
- const res = await signUpCredential(ctx);
+ const res = await signUpEmail({
+ ...ctx,
+ //@ts-expect-error
+ _flag: undefined,
+ });
if (!res) {
return ctx.json(null, {
status: 400,
@@ -121,9 +125,14 @@ export const username = () => {
},
});
}
- await ctx.context.internalAdapter.updateUserByEmail(res.user.email, {
- username: ctx.body.username,
- });
+ const updatedUser =
+ await ctx.context.internalAdapter.updateUserByEmail(
+ res.user.email,
+ {
+ username: ctx.body.username,
+ },
+ );
+ console.log(updatedUser);
return ctx.json(res);
},
),
@@ -135,6 +144,8 @@ export const username = () => {
username: {
type: "string",
required: false,
+ unique: true,
+ returned: true,
},
},
},
diff --git a/packages/better-auth/src/types/plugins.ts b/packages/better-auth/src/types/plugins.ts
index e944387dfc..e9483e114b 100644
--- a/packages/better-auth/src/types/plugins.ts
+++ b/packages/better-auth/src/types/plugins.ts
@@ -26,23 +26,19 @@ export type BetterAuthPlugin = {
hooks?: {
before?: {
matcher: (context: GenericEndpointContext) => boolean;
- handler: Endpoint<
- (context: GenericEndpointContext) => Promise;
- }>
- >;
+ handler: (context: GenericEndpointContext) => Promise;
+ }>;
}[];
after?: {
matcher: (context: GenericEndpointContext) => boolean;
- handler: Endpoint<
- (
- context: GenericEndpointContext & {
- returned: EndpointResponse;
- },
- ) => Promise
- >;
+ handler: (
+ context: GenericEndpointContext & {
+ returned: EndpointResponse;
+ },
+ ) => Promise;
}[];
};
/**