mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-03-21 23:58:22 +03:00
change password requirements
This commit is contained in:
parent
8b426743c2
commit
a8cccaaf1c
@ -29,6 +29,10 @@ auth:
|
||||
reset_admin_password: true
|
||||
```
|
||||
|
||||
## Password guidance
|
||||
|
||||
Constructing secure passwords and managing them properly is important. Frigate requires a minimum length of 12 characters. For guidance on password standards see [NIST SP 800-63B](https://pages.nist.gov/800-63-3/sp800-63b.html). To learn what makes a password truly secure, read this [article](https://medium.com/peerio/how-to-build-a-billion-dollar-password-3d92568d9277).
|
||||
|
||||
## Login failure rate limiting
|
||||
|
||||
In order to limit the risk of brute force attacks, rate limiting is available for login failures. This is implemented with SlowApi, and the string notation for valid values is available in [the documentation](https://limits.readthedocs.io/en/stable/quickstart.html#examples).
|
||||
|
||||
@ -350,21 +350,15 @@ def validate_password_strength(password: str) -> tuple[bool, Optional[str]]:
|
||||
Validate password strength.
|
||||
|
||||
Returns a tuple of (is_valid, error_message).
|
||||
|
||||
Longer passwords are harder to crack than shorter complex ones.
|
||||
https://pages.nist.gov/800-63-3/sp800-63b.html
|
||||
"""
|
||||
if not password:
|
||||
return False, "Password cannot be empty"
|
||||
|
||||
if len(password) < 8:
|
||||
return False, "Password must be at least 8 characters long"
|
||||
|
||||
if not any(c.isupper() for c in password):
|
||||
return False, "Password must contain at least one uppercase letter"
|
||||
|
||||
if not any(c.isdigit() for c in password):
|
||||
return False, "Password must contain at least one digit"
|
||||
|
||||
if not any(c in '!@#$%^&*(),.?":{}|<>' for c in password):
|
||||
return False, "Password must contain at least one special character"
|
||||
if len(password) < 12:
|
||||
return False, "Password must be at least 12 characters long"
|
||||
|
||||
return True, None
|
||||
|
||||
@ -800,7 +794,7 @@ def get_users():
|
||||
"/users",
|
||||
dependencies=[Depends(require_role(["admin"]))],
|
||||
summary="Create new user",
|
||||
description='Creates a new user with the specified username, password, and role. Requires admin role. Password must meet strength requirements: minimum 8 characters, at least one uppercase letter, at least one digit, and at least one special character (!@#$%^&*(),.?":{} |<>).',
|
||||
description="Creates a new user with the specified username, password, and role. Requires admin role. Password must be at least 12 characters long.",
|
||||
)
|
||||
def create_user(
|
||||
request: Request,
|
||||
@ -817,6 +811,15 @@ def create_user(
|
||||
content={"message": f"Role must be one of: {', '.join(config_roles)}"},
|
||||
status_code=400,
|
||||
)
|
||||
|
||||
# Validate password strength
|
||||
is_valid, error_message = validate_password_strength(body.password)
|
||||
if not is_valid:
|
||||
return JSONResponse(
|
||||
content={"message": error_message},
|
||||
status_code=400,
|
||||
)
|
||||
|
||||
role = body.role or "viewer"
|
||||
password_hash = hash_password(body.password, iterations=HASH_ITERATIONS)
|
||||
User.insert(
|
||||
@ -851,7 +854,7 @@ def delete_user(request: Request, username: str):
|
||||
"/users/{username}/password",
|
||||
dependencies=[Depends(allow_any_authenticated())],
|
||||
summary="Update user password",
|
||||
description="Updates a user's password. Users can only change their own password unless they have admin role. Requires the current password to verify identity for non-admin users. Password must meet strength requirements: minimum 8 characters, at least one uppercase letter, at least one digit, and at least one special character (!@#$%^&*(),.?\":{} |<>). If user changes their own password, a new JWT cookie is automatically issued.",
|
||||
description="Updates a user's password. Users can only change their own password unless they have admin role. Requires the current password to verify identity for non-admin users. Password must be at least 12 characters long. If user changes their own password, a new JWT cookie is automatically issued.",
|
||||
)
|
||||
async def update_password(
|
||||
request: Request,
|
||||
|
||||
@ -728,10 +728,7 @@
|
||||
},
|
||||
"requirements": {
|
||||
"title": "Password requirements:",
|
||||
"length": "At least 8 characters",
|
||||
"uppercase": "At least one uppercase letter",
|
||||
"digit": "At least one digit",
|
||||
"special": "At least one special character (!@#$%^&*(),.?\":{}|<>)"
|
||||
"length": "At least 12 characters"
|
||||
},
|
||||
"match": "Passwords match",
|
||||
"notMatch": "Passwords don't match"
|
||||
|
||||
@ -85,13 +85,7 @@ export default function CreateUserDialog({
|
||||
}),
|
||||
password: z
|
||||
.string()
|
||||
.min(8, t("users.dialog.form.password.requirements.length"))
|
||||
.regex(/[A-Z]/, t("users.dialog.form.password.requirements.uppercase"))
|
||||
.regex(/\d/, t("users.dialog.form.password.requirements.digit"))
|
||||
.regex(
|
||||
/[!@#$%^&*(),.?":{}|<>]/,
|
||||
t("users.dialog.form.password.requirements.special"),
|
||||
),
|
||||
.min(12, t("users.dialog.form.password.requirements.length")),
|
||||
confirmPassword: z
|
||||
.string()
|
||||
.min(1, t("users.dialog.createUser.confirmPassword")),
|
||||
@ -263,7 +257,7 @@ export default function CreateUserDialog({
|
||||
className={`${getPasswordStrengthColor(
|
||||
password,
|
||||
)} transition-all duration-300`}
|
||||
style={{ width: `${(passwordStrength / 4) * 100}%` }}
|
||||
style={{ width: `${passwordStrength * 100}%` }}
|
||||
/>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
@ -296,60 +290,6 @@ export default function CreateUserDialog({
|
||||
)}
|
||||
</span>
|
||||
</li>
|
||||
<li className="flex items-center gap-2 text-xs">
|
||||
{requirements.uppercase ? (
|
||||
<LuCheck className="size-3.5 text-green-500" />
|
||||
) : (
|
||||
<LuX className="size-3.5 text-red-500" />
|
||||
)}
|
||||
<span
|
||||
className={
|
||||
requirements.uppercase
|
||||
? "text-green-600"
|
||||
: "text-red-600"
|
||||
}
|
||||
>
|
||||
{t(
|
||||
"users.dialog.form.password.requirements.uppercase",
|
||||
)}
|
||||
</span>
|
||||
</li>
|
||||
<li className="flex items-center gap-2 text-xs">
|
||||
{requirements.digit ? (
|
||||
<LuCheck className="size-3.5 text-green-500" />
|
||||
) : (
|
||||
<LuX className="size-3.5 text-red-500" />
|
||||
)}
|
||||
<span
|
||||
className={
|
||||
requirements.digit
|
||||
? "text-green-600"
|
||||
: "text-red-600"
|
||||
}
|
||||
>
|
||||
{t(
|
||||
"users.dialog.form.password.requirements.digit",
|
||||
)}
|
||||
</span>
|
||||
</li>
|
||||
<li className="flex items-center gap-2 text-xs">
|
||||
{requirements.special ? (
|
||||
<LuCheck className="size-3.5 text-green-500" />
|
||||
) : (
|
||||
<LuX className="size-3.5 text-red-500" />
|
||||
)}
|
||||
<span
|
||||
className={
|
||||
requirements.special
|
||||
? "text-green-600"
|
||||
: "text-red-600"
|
||||
}
|
||||
>
|
||||
{t(
|
||||
"users.dialog.form.password.requirements.special",
|
||||
)}
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -76,13 +76,7 @@ export default function SetPasswordDialog({
|
||||
const baseSchema = {
|
||||
password: z
|
||||
.string()
|
||||
.min(8, t("users.dialog.form.password.requirements.length"))
|
||||
.regex(/[A-Z]/, t("users.dialog.form.password.requirements.uppercase"))
|
||||
.regex(/\d/, t("users.dialog.form.password.requirements.digit"))
|
||||
.regex(
|
||||
/[!@#$%^&*(),.?":{}|<>]/,
|
||||
t("users.dialog.form.password.requirements.special"),
|
||||
),
|
||||
.min(12, t("users.dialog.form.password.requirements.length")),
|
||||
confirmPassword: z.string(),
|
||||
};
|
||||
|
||||
@ -345,7 +339,7 @@ export default function SetPasswordDialog({
|
||||
className={`${getPasswordStrengthColor(
|
||||
password,
|
||||
)} transition-all duration-300`}
|
||||
style={{ width: `${(passwordStrength / 4) * 100}%` }}
|
||||
style={{ width: `${passwordStrength * 100}%` }}
|
||||
/>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
@ -378,60 +372,6 @@ export default function SetPasswordDialog({
|
||||
)}
|
||||
</span>
|
||||
</li>
|
||||
<li className="flex items-center gap-2 text-xs">
|
||||
{requirements.uppercase ? (
|
||||
<LuCheck className="size-3.5 text-green-500" />
|
||||
) : (
|
||||
<LuX className="size-3.5 text-red-500" />
|
||||
)}
|
||||
<span
|
||||
className={
|
||||
requirements.uppercase
|
||||
? "text-green-600"
|
||||
: "text-red-600"
|
||||
}
|
||||
>
|
||||
{t(
|
||||
"users.dialog.form.password.requirements.uppercase",
|
||||
)}
|
||||
</span>
|
||||
</li>
|
||||
<li className="flex items-center gap-2 text-xs">
|
||||
{requirements.digit ? (
|
||||
<LuCheck className="size-3.5 text-green-500" />
|
||||
) : (
|
||||
<LuX className="size-3.5 text-red-500" />
|
||||
)}
|
||||
<span
|
||||
className={
|
||||
requirements.digit
|
||||
? "text-green-600"
|
||||
: "text-red-600"
|
||||
}
|
||||
>
|
||||
{t(
|
||||
"users.dialog.form.password.requirements.digit",
|
||||
)}
|
||||
</span>
|
||||
</li>
|
||||
<li className="flex items-center gap-2 text-xs">
|
||||
{requirements.special ? (
|
||||
<LuCheck className="size-3.5 text-green-500" />
|
||||
) : (
|
||||
<LuX className="size-3.5 text-red-500" />
|
||||
)}
|
||||
<span
|
||||
className={
|
||||
requirements.special
|
||||
? "text-green-600"
|
||||
: "text-red-600"
|
||||
}
|
||||
>
|
||||
{t(
|
||||
"users.dialog.form.password.requirements.special",
|
||||
)}
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -11,10 +11,7 @@ export const calculatePasswordStrength = (password: string): number => {
|
||||
};
|
||||
|
||||
export const getPasswordRequirements = (password: string) => ({
|
||||
length: password?.length >= 8,
|
||||
uppercase: /[A-Z]/.test(password || ""),
|
||||
digit: /\d/.test(password || ""),
|
||||
special: /[!@#$%^&*(),.?":{}|<>]/.test(password || ""),
|
||||
length: password?.length >= 12,
|
||||
});
|
||||
|
||||
export const getPasswordStrengthLabel = (
|
||||
@ -24,9 +21,7 @@ export const getPasswordStrengthLabel = (
|
||||
const strength = calculatePasswordStrength(password);
|
||||
|
||||
if (!password) return "";
|
||||
if (strength <= 1) return t("users.dialog.form.password.strength.weak");
|
||||
if (strength === 2) return t("users.dialog.form.password.strength.medium");
|
||||
if (strength === 3) return t("users.dialog.form.password.strength.strong");
|
||||
if (strength < 1) return t("users.dialog.form.password.strength.weak");
|
||||
return t("users.dialog.form.password.strength.veryStrong");
|
||||
};
|
||||
|
||||
@ -34,8 +29,6 @@ export const getPasswordStrengthColor = (password: string): string => {
|
||||
const strength = calculatePasswordStrength(password);
|
||||
|
||||
if (!password) return "bg-gray-200";
|
||||
if (strength <= 1) return "bg-red-500";
|
||||
if (strength === 2) return "bg-yellow-500";
|
||||
if (strength === 3) return "bg-green-500";
|
||||
return "bg-green-600";
|
||||
if (strength === 0) return "bg-red-500";
|
||||
return "bg-green-500";
|
||||
};
|
||||
|
||||
Loading…
Reference in New Issue
Block a user