fix: better logs on validation fail for oauth (#100)

* fix: better errors on phone number plugin

* fix: add validation log on oauth
This commit is contained in:
Bereket Engida
2024-10-05 20:34:07 +03:00
committed by GitHub
parent 5c9519c4f0
commit 3b3598aca9
7 changed files with 194 additions and 58 deletions

View File

@@ -1,5 +1,10 @@
import { betterAuth } from "better-auth"; import { betterAuth } from "better-auth";
import { organization, passkey, twoFactor } from "better-auth/plugins"; import {
organization,
passkey,
phoneNumber,
twoFactor,
} from "better-auth/plugins";
import { reactInvitationEmail } from "./email/invitation"; import { reactInvitationEmail } from "./email/invitation";
import { LibsqlDialect } from "@libsql/kysely-libsql"; import { LibsqlDialect } from "@libsql/kysely-libsql";
import { reactResetPasswordEmail } from "./email/rest-password"; import { reactResetPasswordEmail } from "./email/rest-password";
@@ -24,6 +29,7 @@ export const auth = betterAuth({
}, },
sendEmailVerificationOnSignUp: true, sendEmailVerificationOnSignUp: true,
async sendVerificationEmail(email, url) { async sendVerificationEmail(email, url) {
console.log("Sending verification email to", email);
const res = await resend.emails.send({ const res = await resend.emails.send({
from, from,
to: to || email, to: to || email,
@@ -34,6 +40,14 @@ export const auth = betterAuth({
}, },
}, },
plugins: [ plugins: [
phoneNumber({
otp: {
sendOTP(phoneNumber, code) {
console.log(`Sending OTP to ${phoneNumber}: ${code}`);
},
sendOTPonSignUp: true,
},
}),
organization({ organization({
async sendInvitationEmail(data) { async sendInvitationEmail(data) {
const res = await resend.emails.send({ const res = await resend.emails.send({

View File

@@ -52,6 +52,7 @@
"class-variance-authority": "^0.7.0", "class-variance-authority": "^0.7.0",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"cmdk": "1.0.0", "cmdk": "1.0.0",
"consola": "^3.2.3",
"date-fns": "^3.6.0", "date-fns": "^3.6.0",
"embla-carousel-react": "^8.2.1", "embla-carousel-react": "^8.2.1",
"framer-motion": "^11.5.4", "framer-motion": "^11.5.4",

View File

@@ -79,6 +79,7 @@ export const callbackOAuth = createAuthEndpoint(
const { callbackURL, currentURL, dontRememberMe } = parsedState.data; const { callbackURL, currentURL, dontRememberMe } = parsedState.data;
if (!user || data.success === false) { if (!user || data.success === false) {
logger.error("Unable to get user info", data.error);
throw c.redirect( throw c.redirect(
`${c.context.baseURL}/error?error=oauth_validation_failed`, `${c.context.baseURL}/error?error=oauth_validation_failed`,
); );

View File

@@ -23,53 +23,93 @@ export const signUpEmail = createAuthEndpoint(
}, },
async (ctx) => { async (ctx) => {
if (!ctx.context.options.emailAndPassword?.enabled) { if (!ctx.context.options.emailAndPassword?.enabled) {
return ctx.json(null, { return ctx.json(
status: 400, {
body: { user: null,
message: "Email and password is not enabled", session: null,
error: {
message: "Email and password is not enabled",
},
}, },
}); {
status: 400,
body: {
message: "Email and password is not enabled",
},
},
);
} }
const { name, email, password, image } = ctx.body; const { name, email, password, image } = ctx.body;
const isValidEmail = z.string().email().safeParse(email); const isValidEmail = z.string().email().safeParse(email);
if (!isValidEmail.success) { if (!isValidEmail.success) {
return ctx.json(null, { return ctx.json(
status: 400, {
body: { user: null,
message: "Invalid email address", session: null,
error: {
message: "Invalid email address",
},
}, },
}); {
status: 400,
body: {
message: "Invalid email address",
},
},
);
} }
const minPasswordLength = ctx.context.password.config.minPasswordLength; const minPasswordLength = ctx.context.password.config.minPasswordLength;
if (password.length < minPasswordLength) { if (password.length < minPasswordLength) {
ctx.context.logger.error("Password is too short"); ctx.context.logger.error("Password is too short");
return ctx.json(null, { return ctx.json(
status: 400, {
body: { message: "Password is too short" }, user: null,
}); session: null,
error: {
message: "Password is too short",
},
},
{
status: 400,
body: { message: "Password is too short" },
},
);
} }
const maxPasswordLength = ctx.context.password.config.maxPasswordLength; const maxPasswordLength = ctx.context.password.config.maxPasswordLength;
if (password.length > maxPasswordLength) { if (password.length > maxPasswordLength) {
ctx.context.logger.error("Password is too long"); ctx.context.logger.error("Password is too long");
return ctx.json(null, { return ctx.json(
status: 400, {
body: { message: "Password is too long" }, user: null,
}); session: null,
} error: {
message: "Password is too long",
const dbUser = await ctx.context.internalAdapter.findUserByEmail(email); },
/**
* hash first to avoid timing attacks
*/
const hash = await ctx.context.password.hash(password);
if (dbUser?.user) {
return ctx.json(null, {
status: 400,
body: {
message: "User already exists",
}, },
}); {
status: 400,
body: { message: "Password is too long" },
},
);
}
const dbUser = await ctx.context.internalAdapter.findUserByEmail(email);
if (dbUser?.user) {
return ctx.json(
{
user: null,
session: null,
error: {
message: "User already exists",
},
},
{
status: 400,
body: {
message: "User already exists",
},
},
);
} }
const createdUser = await ctx.context.internalAdapter.createUser({ const createdUser = await ctx.context.internalAdapter.createUser({
id: generateRandomString(32, alphabet("a-z", "0-9", "A-Z")), id: generateRandomString(32, alphabet("a-z", "0-9", "A-Z")),
@@ -81,16 +121,26 @@ export const signUpEmail = createAuthEndpoint(
updatedAt: new Date(), updatedAt: new Date(),
}); });
if (!createdUser) { if (!createdUser) {
return ctx.json(null, { return ctx.json(
status: 400, {
body: { user: null,
message: "Could not create user", session: null,
error: {
message: "Could not create user",
},
}, },
}); {
status: 400,
body: {
message: "Could not create user",
},
},
);
} }
/** /**
* Link the account to the user * Link the account to the user
*/ */
const hash = await ctx.context.password.hash(password);
await ctx.context.internalAdapter.linkAccount({ await ctx.context.internalAdapter.linkAccount({
id: generateRandomString(32, alphabet("a-z", "0-9", "A-Z")), id: generateRandomString(32, alphabet("a-z", "0-9", "A-Z")),
userId: createdUser.id, userId: createdUser.id,
@@ -103,12 +153,21 @@ export const signUpEmail = createAuthEndpoint(
ctx.request, ctx.request,
); );
if (!session) { if (!session) {
return ctx.json(null, { return ctx.json(
status: 400, {
body: { user: null,
message: "Could not create session", session: null,
error: {
message: "Could not create session",
},
}, },
}); {
status: 400,
body: {
message: "Could not create session",
},
},
);
} }
await setSessionCookie(ctx, session.id); await setSessionCookie(ctx, session.id);
if (ctx.context.options.emailAndPassword.sendEmailVerificationOnSignUp) { if (ctx.context.options.emailAndPassword.sendEmailVerificationOnSignUp) {
@@ -131,6 +190,7 @@ export const signUpEmail = createAuthEndpoint(
{ {
user: createdUser, user: createdUser,
session, session,
error: null,
}, },
{ {
body: ctx.body.callbackURL body: ctx.body.callbackURL

View File

@@ -10,13 +10,7 @@ import { getDate } from "../../utils/date";
import { logger } from "../../utils/logger"; import { logger } from "../../utils/logger";
import { setSessionCookie } from "../../utils/cookies"; import { setSessionCookie } from "../../utils/cookies";
interface OTP { export interface UserWithPhoneNumber extends User {
code: string;
phoneNumber: string;
createdAt: Date;
}
interface UserWithPhoneNumber extends User {
phoneNumber: string; phoneNumber: string;
phoneNumberVerified: boolean; phoneNumberVerified: boolean;
} }
@@ -46,6 +40,12 @@ export const phoneNumber = (options?: {
expiresIn?: number; expiresIn?: number;
}; };
enableAutoSignIn?: boolean; enableAutoSignIn?: boolean;
/**
* Function to validate phone number
*
* by default any string is accepted
*/
phoneNumberValidator?: (phoneNumber: string) => boolean;
}) => { }) => {
const opts = { const opts = {
phoneNumber: "phoneNumber", phoneNumber: "phoneNumber",
@@ -171,19 +171,77 @@ export const phoneNumber = (options?: {
}), }),
}, },
async (ctx) => { async (ctx) => {
if (
options?.phoneNumberValidator &&
!options.phoneNumberValidator(ctx.body.phoneNumber)
) {
return ctx.json(
{
user: null,
session: null,
error: {
message: "Invalid phone number",
},
},
{
status: 400,
body: {
message: "Invalid phone number",
status: 400,
},
},
);
}
const existing = await ctx.context.adapter.findOne<User>({
model: ctx.context.tables.user.tableName,
where: [
{
field: opts.phoneNumber,
value: ctx.body.phoneNumber,
},
],
});
if (existing) {
return ctx.json(
{
user: null,
session: null,
error: {
message: "Phone number already exists",
},
},
{
status: 400,
body: {
message: "Phone number already exists",
status: 400,
},
},
);
}
const res = await signUpEmail({ const res = await signUpEmail({
...ctx, ...ctx,
//@ts-expect-error //@ts-expect-error
options: {
...ctx.context.options,
},
_flag: undefined, _flag: undefined,
}); });
if (!res) { if (res.error) {
return ctx.json(null, { return ctx.json(
status: 400, {
body: { user: null,
message: "Sign up failed", session: null,
status: 400,
}, },
}); {
status: 400,
body: {
message: res.error.message,
status: 400,
},
},
);
} }
if (options?.otp?.sendOTPonSignUp) { if (options?.otp?.sendOTPonSignUp) {
if (!options.otp.sendOTP) { if (!options.otp.sendOTP) {
@@ -200,6 +258,7 @@ export const phoneNumber = (options?: {
}); });
await options.otp.sendOTP(ctx.body.phoneNumber, code); await options.otp.sendOTP(ctx.body.phoneNumber, code);
} }
const updated = await ctx.context.internalAdapter.updateUserByEmail( const updated = await ctx.context.internalAdapter.updateUserByEmail(
res.user.email, res.user.email,
{ {

View File

@@ -117,8 +117,6 @@ export const github = (options: GithubOptions) => {
email: profile.email, email: profile.email,
image: profile.avatar_url, image: profile.avatar_url,
emailVerified, emailVerified,
createdAt: new Date(),
updatedAt: new Date(),
}, },
data: profile, data: profile,
}; };

3
pnpm-lock.yaml generated
View File

@@ -170,6 +170,9 @@ importers:
cmdk: cmdk:
specifier: 1.0.0 specifier: 1.0.0
version: 1.0.0(@types/react-dom@18.3.0)(@types/react@18.3.9)(react-dom@19.0.0-rc-7771d3a7-20240827)(react@19.0.0-rc-7771d3a7-20240827) version: 1.0.0(@types/react-dom@18.3.0)(@types/react@18.3.9)(react-dom@19.0.0-rc-7771d3a7-20240827)(react@19.0.0-rc-7771d3a7-20240827)
consola:
specifier: ^3.2.3
version: 3.2.3
date-fns: date-fns:
specifier: ^3.6.0 specifier: ^3.6.0
version: 3.6.0 version: 3.6.0