mirror of
https://github.com/better-auth/better-auth.git
synced 2026-05-25 16:36:34 -05:00
feat: allow signin with phone number and password (#531)
This commit is contained in:
@@ -111,6 +111,18 @@ export const auth = betterAuth({
|
||||
})
|
||||
```
|
||||
|
||||
### SignIn with Phone number
|
||||
|
||||
In addition to signing in a user using send-verify flow, you can also use phone number as an identifier and sign in a user using phone number and password.
|
||||
|
||||
```ts
|
||||
await authClient.signIn.phoneNumber({
|
||||
phoneNumber: "+123456789",
|
||||
password: "password",
|
||||
remeberMe: true //optional defaults to true
|
||||
})
|
||||
```
|
||||
|
||||
### Update Phone Number
|
||||
|
||||
Updating phone number uses the same process as verifying a phone number. The user will receive an OTP code to verify the new phone number.
|
||||
|
||||
@@ -62,6 +62,7 @@ exports[`init > should match config 1`] = `
|
||||
"deleteVerificationByIdentifier": [Function],
|
||||
"deleteVerificationValue": [Function],
|
||||
"findAccount": [Function],
|
||||
"findAccountByUserId": [Function],
|
||||
"findAccounts": [Function],
|
||||
"findSession": [Function],
|
||||
"findSessions": [Function],
|
||||
|
||||
@@ -622,6 +622,18 @@ export const createInternalAdapter = (
|
||||
});
|
||||
return account;
|
||||
},
|
||||
findAccountByUserId: async (userId: string) => {
|
||||
const account = await adapter.findMany<Account>({
|
||||
model: "account",
|
||||
where: [
|
||||
{
|
||||
field: "userId",
|
||||
value: userId,
|
||||
},
|
||||
],
|
||||
});
|
||||
return account;
|
||||
},
|
||||
updateAccount: async (accountId: string, data: Partial<Account>) => {
|
||||
const account = await updateWithHooks<Account>(
|
||||
data,
|
||||
|
||||
@@ -48,7 +48,7 @@ export const phoneNumber = (options?: {
|
||||
*
|
||||
* by default any string is accepted
|
||||
*/
|
||||
phoneNumberValidator?: (phoneNumber: string) => boolean;
|
||||
phoneNumberValidator?: (phoneNumber: string) => boolean | Promise<boolean>;
|
||||
/**
|
||||
* Callback when phone number is verified
|
||||
*/
|
||||
@@ -93,16 +93,111 @@ export const phoneNumber = (options?: {
|
||||
schema?: InferOptionSchema<typeof schema>;
|
||||
}) => {
|
||||
const opts = {
|
||||
expiresIn: options?.expiresIn || 300,
|
||||
otpLength: options?.otpLength || 6,
|
||||
...options,
|
||||
phoneNumber: "phoneNumber",
|
||||
phoneNumberVerified: "phoneNumberVerified",
|
||||
code: "code",
|
||||
createdAt: "createdAt",
|
||||
expiresIn: options?.expiresIn || 300,
|
||||
otpLength: options?.otpLength || 6,
|
||||
};
|
||||
return {
|
||||
id: "phone-number",
|
||||
endpoints: {
|
||||
signInPhoneNumber: createAuthEndpoint(
|
||||
"/sign-in/phone-number",
|
||||
{
|
||||
method: "POST",
|
||||
body: z.object({
|
||||
phoneNumber: z.string(),
|
||||
password: z.string(),
|
||||
rememberMe: z.boolean().optional(),
|
||||
}),
|
||||
},
|
||||
async (ctx) => {
|
||||
const { password, phoneNumber } = ctx.body;
|
||||
|
||||
if (opts.phoneNumberValidator) {
|
||||
const isValidNumber = await opts.phoneNumberValidator(
|
||||
ctx.body.phoneNumber,
|
||||
);
|
||||
if (!isValidNumber) {
|
||||
throw new APIError("BAD_REQUEST", {
|
||||
message: "Invalid phone number!",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const user = await ctx.context.adapter.findOne<UserWithPhoneNumber>({
|
||||
model: "user",
|
||||
where: [
|
||||
{
|
||||
field: "phoneNumber",
|
||||
value: phoneNumber,
|
||||
},
|
||||
],
|
||||
});
|
||||
if (!user) {
|
||||
throw new APIError("UNAUTHORIZED", {
|
||||
message: "Invalid phone number or password",
|
||||
});
|
||||
}
|
||||
const accounts =
|
||||
await ctx.context.internalAdapter.findAccountByUserId(user.id);
|
||||
const credentialAccount = accounts.find(
|
||||
(a) => a.providerId === "credential",
|
||||
);
|
||||
if (!credentialAccount) {
|
||||
ctx.context.logger.error("Credential account not found", {
|
||||
phoneNumber,
|
||||
});
|
||||
throw new APIError("UNAUTHORIZED", {
|
||||
message: "Invalid password or password",
|
||||
});
|
||||
}
|
||||
const currentPassword = credentialAccount?.password;
|
||||
if (!currentPassword) {
|
||||
ctx.context.logger.error("Password not found", { phoneNumber });
|
||||
throw new APIError("UNAUTHORIZED", {
|
||||
message: "Unexpected error",
|
||||
});
|
||||
}
|
||||
const validPassword = await ctx.context.password.verify(
|
||||
currentPassword,
|
||||
password,
|
||||
);
|
||||
if (!validPassword) {
|
||||
ctx.context.logger.error("Invalid password");
|
||||
throw new APIError("UNAUTHORIZED", {
|
||||
message: "Invalid email or password",
|
||||
});
|
||||
}
|
||||
const session = await ctx.context.internalAdapter.createSession(
|
||||
user.id,
|
||||
ctx.headers,
|
||||
ctx.body.rememberMe === false,
|
||||
);
|
||||
if (!session) {
|
||||
ctx.context.logger.error("Failed to create session");
|
||||
throw new APIError("UNAUTHORIZED", {
|
||||
message: "Failed to create session",
|
||||
});
|
||||
}
|
||||
|
||||
await setSessionCookie(
|
||||
ctx,
|
||||
{
|
||||
session,
|
||||
user: user,
|
||||
},
|
||||
ctx.body.rememberMe === false,
|
||||
);
|
||||
return ctx.json({
|
||||
user: user,
|
||||
session,
|
||||
});
|
||||
},
|
||||
),
|
||||
sendPhoneNumberOTP: createAuthEndpoint(
|
||||
"/phone-number/send-otp",
|
||||
{
|
||||
@@ -118,6 +213,18 @@ export const phoneNumber = (options?: {
|
||||
message: "sendOTP not implemented",
|
||||
});
|
||||
}
|
||||
|
||||
if (opts.phoneNumberValidator) {
|
||||
const isValidNumber = await opts.phoneNumberValidator(
|
||||
ctx.body.phoneNumber,
|
||||
);
|
||||
if (!isValidNumber) {
|
||||
throw new APIError("BAD_REQUEST", {
|
||||
message: "Invalid phone number!",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const code = generateOTP(opts.otpLength);
|
||||
await ctx.context.internalAdapter.createVerificationValue({
|
||||
value: code,
|
||||
|
||||
@@ -186,6 +186,14 @@ describe("phone auth flow", async () => {
|
||||
expect(changedEmailRes.data?.user.email).toBe(newEmail);
|
||||
});
|
||||
|
||||
it("should sign in with phone number and password", async () => {
|
||||
const res = await client.signIn.phoneNumber({
|
||||
phoneNumber: "+251911121314",
|
||||
password: "password",
|
||||
});
|
||||
expect(res.data?.session).toBeDefined();
|
||||
});
|
||||
|
||||
it("should sign in with new email", async () => {
|
||||
const res = await client.signIn.email({
|
||||
email: newEmail,
|
||||
|
||||
Reference in New Issue
Block a user