use verify endpoint for existing password verification

avoid /login side effects (creating a new session)
This commit is contained in:
Josh Hawkins 2025-12-08 07:48:48 -06:00
parent d654ac2755
commit 96731ead74
5 changed files with 59 additions and 30 deletions

View File

@ -55,6 +55,7 @@ def require_admin_by_default():
"/auth",
"/auth/first_time_login",
"/login",
"/auth/verify",
# Authenticated user endpoints (allow_any_authenticated)
"/logout",
"/profile",
@ -751,6 +752,30 @@ def login(request: Request, body: AppPostLoginBody):
return JSONResponse(content={"message": "Login failed"}, status_code=401)
@router.post("/auth/verify", dependencies=[Depends(allow_public())])
@limiter.limit(limit_value=rateLimiter.get_limit)
def verify(request: Request, body: AppPostLoginBody):
"""Verify credentials without creating a session.
This endpoint is used for password change verification and other
credential validation scenarios that don't require session creation.
"""
user = body.user
password = body.password
try:
db_user: User = User.get_by_id(user)
except DoesNotExist:
return JSONResponse(content={"message": "Verification failed"}, status_code=401)
password_hash = db_user.password_hash
if verify_password(password, password_hash):
return JSONResponse(
content={"message": "Verification successful"}, status_code=200
)
return JSONResponse(content={"message": "Verification failed"}, status_code=401)
@router.get("/users", dependencies=[Depends(require_role(["admin"]))])
def get_users():
exports = (
@ -863,6 +888,8 @@ async def update_password(
}
).where(User.username == username).execute()
response = JSONResponse(content={"success": True})
# If user changed their own password, issue a new JWT to keep them logged in
if current_username == username:
JWT_COOKIE_NAME = request.app.frigate_config.auth.cookie_name
@ -873,13 +900,12 @@ async def update_password(
encoded_jwt = create_encoded_jwt(
username, current_role, expiration, request.app.jwt_token
)
response = JSONResponse(content={"success": True})
# Set new JWT cookie on response
set_jwt_cookie(
response, JWT_COOKIE_NAME, encoded_jwt, expiration, JWT_COOKIE_SECURE
)
return response
return JSONResponse(content={"success": True})
return response
@router.put(

View File

@ -30,6 +30,7 @@ import axios from "axios";
import { toast } from "sonner";
import SetPasswordDialog from "../overlay/SetPasswordDialog";
import { useTranslation } from "react-i18next";
import { verifyPassword } from "@/utils/authUtil";
type AccountSettingsProps = {
className?: string;
@ -51,15 +52,7 @@ export default function AccountSettings({ className }: AccountSettingsProps) {
const verifyOldPassword = async (oldPassword: string): Promise<boolean> => {
if (!profile?.username || profile.username === "anonymous") return false;
try {
const response = await axios.post("login", {
user: profile.username,
password: oldPassword,
});
return response.status === 200;
} catch (error) {
return false;
}
return verifyPassword(profile.username, oldPassword);
};
const handlePasswordSave = async (password: string, oldPassword?: string) => {

View File

@ -66,6 +66,7 @@ import { supportedLanguageKeys } from "@/lib/const";
import { useDocDomain } from "@/hooks/use-doc-domain";
import { MdCategory } from "react-icons/md";
import { verifyPassword } from "@/utils/authUtil";
type GeneralSettingsProps = {
className?: string;
@ -120,15 +121,7 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) {
const verifyOldPassword = async (oldPassword: string): Promise<boolean> => {
if (!profile?.username || profile.username === "anonymous") return false;
try {
const response = await axios.post("login", {
user: profile.username,
password: oldPassword,
});
return response.status === 200;
} catch (error) {
return false;
}
return verifyPassword(profile.username, oldPassword);
};
const handlePasswordSave = async (password: string, oldPassword?: string) => {

24
web/src/utils/authUtil.ts Normal file
View File

@ -0,0 +1,24 @@
import axios from "axios";
/**
* Verifies a user's password without creating a session.
* This is used for password change verification.
*
* @param username - The username to verify
* @param password - The password to verify
* @returns true if credentials are valid, false otherwise
*/
export async function verifyPassword(
username: string,
password: string,
): Promise<boolean> {
try {
const response = await axios.post("auth/verify", {
user: username,
password,
});
return response.status === 200;
} catch (error) {
return false;
}
}

View File

@ -37,6 +37,7 @@ import { useTranslation } from "react-i18next";
import DeleteRoleDialog from "@/components/overlay/DeleteRoleDialog";
import { Separator } from "@/components/ui/separator";
import { CameraNameLabel } from "@/components/camera/FriendlyNameLabel";
import { verifyPassword } from "@/utils/authUtil";
type AuthenticationViewProps = {
section?: "users" | "roles";
@ -73,15 +74,7 @@ export default function AuthenticationView({
const onVerifyOldPassword = useCallback(
async (oldPassword: string): Promise<boolean> => {
if (!selectedUser) return false;
try {
const response = await axios.post("login", {
user: selectedUser,
password: oldPassword,
});
return response.status === 200;
} catch (error) {
return false;
}
return verifyPassword(selectedUser, oldPassword);
},
[selectedUser],
);