mirror of
https://github.com/better-auth/better-auth.git
synced 2026-10-25 10:13:54 -05:00
feat: add auth.api.verifyPassword (#6934)
Co-authored-by: Alex Yang <himself65@outlook.com>
This commit is contained in:
@@ -140,6 +140,21 @@ await auth.api.setPassword({
|
||||
headers: await headers() // headers containing the user's session token
|
||||
});
|
||||
```
|
||||
### Verify Password
|
||||
|
||||
The `verifyPassword` function allows you to verify a user's current password. This is useful for confirming user identity before performing sensitive operations like updating security settings. This function can only be called from the server.
|
||||
|
||||
```ts
|
||||
await auth.api.verifyPassword({
|
||||
body: {
|
||||
password: "user-password" // required
|
||||
},
|
||||
headers: await headers() // headers containing the user's session token
|
||||
});
|
||||
```
|
||||
<Callout type="info">
|
||||
For OAuth users who don't have passwords, consider using email verification or fresh session checks for sensitive operations instead.
|
||||
</Callout>
|
||||
|
||||
## Delete User
|
||||
|
||||
@@ -453,8 +468,10 @@ Users already signed in can manually link their account to additional social pro
|
||||
|
||||
```ts
|
||||
await auth.api.setPassword({
|
||||
headers: /* headers containing the user's session token */,
|
||||
password: /* new password */
|
||||
body: {
|
||||
newPassword: "new-password", // required
|
||||
},
|
||||
headers: await headers() // headers containing the user's session token
|
||||
});
|
||||
```
|
||||
|
||||
|
||||
@@ -42,6 +42,7 @@ import {
|
||||
unlinkAccount,
|
||||
updateUser,
|
||||
verifyEmail,
|
||||
verifyPassword,
|
||||
} from "./routes";
|
||||
import { toAuthEndpoints } from "./to-auth-endpoints";
|
||||
|
||||
@@ -216,6 +217,7 @@ export function getEndpoints<Option extends BetterAuthOptions>(
|
||||
signUpEmail: signUpEmail<Option>(),
|
||||
signInEmail: signInEmail<Option>(),
|
||||
resetPassword,
|
||||
verifyPassword,
|
||||
verifyEmail,
|
||||
sendVerificationEmail,
|
||||
changeEmail,
|
||||
|
||||
@@ -3,7 +3,7 @@ export * from "./callback";
|
||||
export * from "./email-verification";
|
||||
export * from "./error";
|
||||
export * from "./ok";
|
||||
export * from "./reset-password";
|
||||
export * from "./password";
|
||||
export * from "./session";
|
||||
export * from "./sign-in";
|
||||
export * from "./sign-out";
|
||||
|
||||
+75
@@ -1,3 +1,4 @@
|
||||
import { APIError } from "better-call";
|
||||
import { describe, expect, vi } from "vitest";
|
||||
import { getTestInstance } from "../../test-utils/test-instance";
|
||||
import type { Account } from "../../types";
|
||||
@@ -402,3 +403,77 @@ describe("revoke sessions on password reset", async (it) => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("verify password", async (it) => {
|
||||
const { testUser, auth } = await getTestInstance({
|
||||
emailAndPassword: {
|
||||
enabled: true,
|
||||
},
|
||||
});
|
||||
|
||||
const getSessionHeaders = async () => {
|
||||
const signInRes = await auth.api.signInEmail({
|
||||
body: {
|
||||
email: testUser.email,
|
||||
password: testUser.password,
|
||||
},
|
||||
returnHeaders: true,
|
||||
});
|
||||
|
||||
const headers = new Headers();
|
||||
headers.set("cookie", signInRes.headers.getSetCookie()[0]!);
|
||||
return headers;
|
||||
};
|
||||
|
||||
it("should verify password with correct password", async () => {
|
||||
const headers = await getSessionHeaders();
|
||||
|
||||
const verifyRes = await auth.api.verifyPassword({
|
||||
body: {
|
||||
password: testUser.password,
|
||||
},
|
||||
headers,
|
||||
});
|
||||
|
||||
expect(verifyRes).toMatchObject({
|
||||
status: true,
|
||||
});
|
||||
});
|
||||
|
||||
it("should fail to verify password with incorrect password", async () => {
|
||||
const headers = await getSessionHeaders();
|
||||
|
||||
try {
|
||||
await auth.api.verifyPassword({
|
||||
body: {
|
||||
password: "wrong-password",
|
||||
},
|
||||
headers,
|
||||
});
|
||||
expect.fail("Should have thrown an error");
|
||||
} catch (error) {
|
||||
expect(error).toBeInstanceOf(APIError);
|
||||
if (error instanceof APIError) {
|
||||
expect(error.status).toBe("BAD_REQUEST");
|
||||
expect(error.message).toBe("Invalid password");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it("should require a session to verify password", async () => {
|
||||
try {
|
||||
await auth.api.verifyPassword({
|
||||
body: {
|
||||
password: testUser.password,
|
||||
},
|
||||
headers: new Headers(),
|
||||
});
|
||||
expect.fail("Should have thrown an error");
|
||||
} catch (error) {
|
||||
expect(error).toBeInstanceOf(APIError);
|
||||
if (error instanceof APIError) {
|
||||
expect(error.status).toBe("UNAUTHORIZED");
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
+59
@@ -4,7 +4,9 @@ import { APIError, BASE_ERROR_CODES } from "@better-auth/core/error";
|
||||
import * as z from "zod";
|
||||
import { generateId } from "../../utils";
|
||||
import { getDate } from "../../utils/date";
|
||||
import { validatePassword } from "../../utils/password";
|
||||
import { originCheck } from "../middlewares";
|
||||
import { sensitiveSessionMiddleware } from "./session";
|
||||
|
||||
function redirectError(
|
||||
ctx: AuthContext,
|
||||
@@ -327,3 +329,60 @@ export const resetPassword = createAuthEndpoint(
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
export const verifyPassword = createAuthEndpoint(
|
||||
"/verify-password",
|
||||
{
|
||||
method: "POST",
|
||||
body: z.object({
|
||||
/**
|
||||
* The password to verify
|
||||
*/
|
||||
password: z.string().meta({
|
||||
description: "The password to verify",
|
||||
}),
|
||||
}),
|
||||
metadata: {
|
||||
scope: "server",
|
||||
openapi: {
|
||||
operationId: "verifyPassword",
|
||||
description: "Verify the current user's password",
|
||||
responses: {
|
||||
"200": {
|
||||
description: "Success",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
status: {
|
||||
type: "boolean",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
use: [sensitiveSessionMiddleware],
|
||||
},
|
||||
async (ctx) => {
|
||||
const { password } = ctx.body;
|
||||
const session = ctx.context.session;
|
||||
|
||||
const isValid = await validatePassword(ctx, {
|
||||
password,
|
||||
userId: session.user.id,
|
||||
});
|
||||
|
||||
if (!isValid) {
|
||||
throw new APIError("BAD_REQUEST", BASE_ERROR_CODES.INVALID_PASSWORD);
|
||||
}
|
||||
|
||||
return ctx.json({
|
||||
status: true,
|
||||
});
|
||||
},
|
||||
);
|
||||
@@ -4663,6 +4663,153 @@ exports[`open-api > should generate OpenAPI schema > openAPISchema 1`] = `
|
||||
],
|
||||
},
|
||||
},
|
||||
"/verify-password": {
|
||||
"post": {
|
||||
"description": "Verify the current user's password",
|
||||
"operationId": "verifyPassword",
|
||||
"parameters": [],
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"properties": {
|
||||
"password": {
|
||||
"description": "The password to verify",
|
||||
"type": "string",
|
||||
},
|
||||
},
|
||||
"required": [
|
||||
"password",
|
||||
],
|
||||
"type": "object",
|
||||
},
|
||||
},
|
||||
},
|
||||
"required": true,
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"properties": {
|
||||
"status": {
|
||||
"type": "boolean",
|
||||
},
|
||||
},
|
||||
"type": "object",
|
||||
},
|
||||
},
|
||||
},
|
||||
"description": "Success",
|
||||
},
|
||||
"400": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"properties": {
|
||||
"message": {
|
||||
"type": "string",
|
||||
},
|
||||
},
|
||||
"required": [
|
||||
"message",
|
||||
],
|
||||
"type": "object",
|
||||
},
|
||||
},
|
||||
},
|
||||
"description": "Bad Request. Usually due to missing parameters, or invalid parameters.",
|
||||
},
|
||||
"401": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"properties": {
|
||||
"message": {
|
||||
"type": "string",
|
||||
},
|
||||
},
|
||||
"required": [
|
||||
"message",
|
||||
],
|
||||
"type": "object",
|
||||
},
|
||||
},
|
||||
},
|
||||
"description": "Unauthorized. Due to missing or invalid authentication.",
|
||||
},
|
||||
"403": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"properties": {
|
||||
"message": {
|
||||
"type": "string",
|
||||
},
|
||||
},
|
||||
"type": "object",
|
||||
},
|
||||
},
|
||||
},
|
||||
"description": "Forbidden. You do not have permission to access this resource or to perform this action.",
|
||||
},
|
||||
"404": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"properties": {
|
||||
"message": {
|
||||
"type": "string",
|
||||
},
|
||||
},
|
||||
"type": "object",
|
||||
},
|
||||
},
|
||||
},
|
||||
"description": "Not Found. The requested resource was not found.",
|
||||
},
|
||||
"429": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"properties": {
|
||||
"message": {
|
||||
"type": "string",
|
||||
},
|
||||
},
|
||||
"type": "object",
|
||||
},
|
||||
},
|
||||
},
|
||||
"description": "Too Many Requests. You have exceeded the rate limit. Try again later.",
|
||||
},
|
||||
"500": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"properties": {
|
||||
"message": {
|
||||
"type": "string",
|
||||
},
|
||||
},
|
||||
"type": "object",
|
||||
},
|
||||
},
|
||||
},
|
||||
"description": "Internal Server Error. This is a problem with the server that you cannot fix.",
|
||||
},
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"bearerAuth": [],
|
||||
},
|
||||
],
|
||||
"tags": [
|
||||
"Default",
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user