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
|
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
|
## 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).
|
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.
|
Validate password strength.
|
||||||
|
|
||||||
Returns a tuple of (is_valid, error_message).
|
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:
|
if not password:
|
||||||
return False, "Password cannot be empty"
|
return False, "Password cannot be empty"
|
||||||
|
|
||||||
if len(password) < 8:
|
if len(password) < 12:
|
||||||
return False, "Password must be at least 8 characters long"
|
return False, "Password must be at least 12 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"
|
|
||||||
|
|
||||||
return True, None
|
return True, None
|
||||||
|
|
||||||
@ -800,7 +794,7 @@ def get_users():
|
|||||||
"/users",
|
"/users",
|
||||||
dependencies=[Depends(require_role(["admin"]))],
|
dependencies=[Depends(require_role(["admin"]))],
|
||||||
summary="Create new user",
|
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(
|
def create_user(
|
||||||
request: Request,
|
request: Request,
|
||||||
@ -817,6 +811,15 @@ def create_user(
|
|||||||
content={"message": f"Role must be one of: {', '.join(config_roles)}"},
|
content={"message": f"Role must be one of: {', '.join(config_roles)}"},
|
||||||
status_code=400,
|
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"
|
role = body.role or "viewer"
|
||||||
password_hash = hash_password(body.password, iterations=HASH_ITERATIONS)
|
password_hash = hash_password(body.password, iterations=HASH_ITERATIONS)
|
||||||
User.insert(
|
User.insert(
|
||||||
@ -851,7 +854,7 @@ def delete_user(request: Request, username: str):
|
|||||||
"/users/{username}/password",
|
"/users/{username}/password",
|
||||||
dependencies=[Depends(allow_any_authenticated())],
|
dependencies=[Depends(allow_any_authenticated())],
|
||||||
summary="Update user password",
|
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(
|
async def update_password(
|
||||||
request: Request,
|
request: Request,
|
||||||
|
|||||||
@ -728,10 +728,7 @@
|
|||||||
},
|
},
|
||||||
"requirements": {
|
"requirements": {
|
||||||
"title": "Password requirements:",
|
"title": "Password requirements:",
|
||||||
"length": "At least 8 characters",
|
"length": "At least 12 characters"
|
||||||
"uppercase": "At least one uppercase letter",
|
|
||||||
"digit": "At least one digit",
|
|
||||||
"special": "At least one special character (!@#$%^&*(),.?\":{}|<>)"
|
|
||||||
},
|
},
|
||||||
"match": "Passwords match",
|
"match": "Passwords match",
|
||||||
"notMatch": "Passwords don't match"
|
"notMatch": "Passwords don't match"
|
||||||
|
|||||||
@ -85,13 +85,7 @@ export default function CreateUserDialog({
|
|||||||
}),
|
}),
|
||||||
password: z
|
password: z
|
||||||
.string()
|
.string()
|
||||||
.min(8, t("users.dialog.form.password.requirements.length"))
|
.min(12, 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"),
|
|
||||||
),
|
|
||||||
confirmPassword: z
|
confirmPassword: z
|
||||||
.string()
|
.string()
|
||||||
.min(1, t("users.dialog.createUser.confirmPassword")),
|
.min(1, t("users.dialog.createUser.confirmPassword")),
|
||||||
@ -263,7 +257,7 @@ export default function CreateUserDialog({
|
|||||||
className={`${getPasswordStrengthColor(
|
className={`${getPasswordStrengthColor(
|
||||||
password,
|
password,
|
||||||
)} transition-all duration-300`}
|
)} transition-all duration-300`}
|
||||||
style={{ width: `${(passwordStrength / 4) * 100}%` }}
|
style={{ width: `${passwordStrength * 100}%` }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-xs text-muted-foreground">
|
<p className="text-xs text-muted-foreground">
|
||||||
@ -296,60 +290,6 @@ export default function CreateUserDialog({
|
|||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
</li>
|
</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>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -76,13 +76,7 @@ export default function SetPasswordDialog({
|
|||||||
const baseSchema = {
|
const baseSchema = {
|
||||||
password: z
|
password: z
|
||||||
.string()
|
.string()
|
||||||
.min(8, t("users.dialog.form.password.requirements.length"))
|
.min(12, 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"),
|
|
||||||
),
|
|
||||||
confirmPassword: z.string(),
|
confirmPassword: z.string(),
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -345,7 +339,7 @@ export default function SetPasswordDialog({
|
|||||||
className={`${getPasswordStrengthColor(
|
className={`${getPasswordStrengthColor(
|
||||||
password,
|
password,
|
||||||
)} transition-all duration-300`}
|
)} transition-all duration-300`}
|
||||||
style={{ width: `${(passwordStrength / 4) * 100}%` }}
|
style={{ width: `${passwordStrength * 100}%` }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-xs text-muted-foreground">
|
<p className="text-xs text-muted-foreground">
|
||||||
@ -378,60 +372,6 @@ export default function SetPasswordDialog({
|
|||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
</li>
|
</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>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -11,10 +11,7 @@ export const calculatePasswordStrength = (password: string): number => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const getPasswordRequirements = (password: string) => ({
|
export const getPasswordRequirements = (password: string) => ({
|
||||||
length: password?.length >= 8,
|
length: password?.length >= 12,
|
||||||
uppercase: /[A-Z]/.test(password || ""),
|
|
||||||
digit: /\d/.test(password || ""),
|
|
||||||
special: /[!@#$%^&*(),.?":{}|<>]/.test(password || ""),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export const getPasswordStrengthLabel = (
|
export const getPasswordStrengthLabel = (
|
||||||
@ -24,9 +21,7 @@ export const getPasswordStrengthLabel = (
|
|||||||
const strength = calculatePasswordStrength(password);
|
const strength = calculatePasswordStrength(password);
|
||||||
|
|
||||||
if (!password) return "";
|
if (!password) return "";
|
||||||
if (strength <= 1) return t("users.dialog.form.password.strength.weak");
|
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");
|
|
||||||
return t("users.dialog.form.password.strength.veryStrong");
|
return t("users.dialog.form.password.strength.veryStrong");
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -34,8 +29,6 @@ export const getPasswordStrengthColor = (password: string): string => {
|
|||||||
const strength = calculatePasswordStrength(password);
|
const strength = calculatePasswordStrength(password);
|
||||||
|
|
||||||
if (!password) return "bg-gray-200";
|
if (!password) return "bg-gray-200";
|
||||||
if (strength <= 1) return "bg-red-500";
|
if (strength === 0) return "bg-red-500";
|
||||||
if (strength === 2) return "bg-yellow-500";
|
return "bg-green-500";
|
||||||
if (strength === 3) return "bg-green-500";
|
|
||||||
return "bg-green-600";
|
|
||||||
};
|
};
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user