add brute force protection on login

This commit is contained in:
Blake Blackshear 2024-05-07 05:15:17 -05:00
parent bf35d16a89
commit 115d2bc35a
5 changed files with 46 additions and 6 deletions

View File

@ -1,5 +1,6 @@
click == 8.1.* click == 8.1.*
Flask == 3.0.* Flask == 3.0.*
Flask_Limiter == 3.6.*
imutils == 0.5.* imutils == 0.5.*
joserfc == 0.9.* joserfc == 0.9.*
markupsafe == 2.1.* markupsafe == 2.1.*

View File

@ -13,8 +13,9 @@ from flask import Blueprint, Flask, current_app, jsonify, make_response, request
from markupsafe import escape from markupsafe import escape
from peewee import operator from peewee import operator
from playhouse.sqliteq import SqliteQueueDatabase from playhouse.sqliteq import SqliteQueueDatabase
from werkzeug.middleware.proxy_fix import ProxyFix
from frigate.api.auth import AuthBp, get_jwt_secret from frigate.api.auth import AuthBp, get_jwt_secret, limiter
from frigate.api.event import EventBp from frigate.api.event import EventBp
from frigate.api.export import ExportBp from frigate.api.export import ExportBp
from frigate.api.media import MediaBp from frigate.api.media import MediaBp
@ -86,6 +87,11 @@ def create_app(
app.camera_error_image = None app.camera_error_image = None
app.stats_emitter = stats_emitter app.stats_emitter = stats_emitter
app.jwt_token = get_jwt_secret() if frigate_config.auth.enabled else None app.jwt_token = get_jwt_secret() if frigate_config.auth.enabled else None
# initialize the rate limiter for the login endpoint
app.wsgi_app = ProxyFix(app.wsgi_app, x_for=1)
limiter.init_app(app)
if frigate_config.auth.failed_login_rate_limit is None:
limiter.enabled = False
app.register_blueprint(bp) app.register_blueprint(bp)

View File

@ -11,6 +11,8 @@ from datetime import datetime
from pathlib import Path from pathlib import Path
from flask import Blueprint, current_app, make_response, request from flask import Blueprint, current_app, make_response, request
from flask_limiter import Limiter
from flask_limiter.util import get_remote_address
from joserfc import jwt from joserfc import jwt
from frigate.const import CONFIG_DIR, JWT_SECRET_ENV_VAR, PASSWORD_HASH_ALGORITHM from frigate.const import CONFIG_DIR, JWT_SECRET_ENV_VAR, PASSWORD_HASH_ALGORITHM
@ -19,6 +21,15 @@ logger = logging.getLogger(__name__)
AuthBp = Blueprint("auth", __name__) AuthBp = Blueprint("auth", __name__)
limiter = Limiter(
lambda: get_remote_address,
storage_uri="memory://",
)
def get_rate_limit():
return current_app.frigate_config.auth.failed_login_rate_limit
def get_jwt_secret() -> str: def get_jwt_secret() -> str:
jwt_secret = None jwt_secret = None
@ -179,6 +190,7 @@ def auth():
@AuthBp.route("/login", methods=["POST"]) @AuthBp.route("/login", methods=["POST"])
@limiter.limit(get_rate_limit, deduct_when=lambda response: response.status_code == 400)
def login(): def login():
JWT_COOKIE_NAME = current_app.frigate_config.auth.cookie_name JWT_COOKIE_NAME = current_app.frigate_config.auth.cookie_name
JWT_SESSION_LENGTH = current_app.frigate_config.auth.session_length JWT_SESSION_LENGTH = current_app.frigate_config.auth.session_length

View File

@ -125,7 +125,6 @@ class UserConfig(FrigateBaseModel):
class AuthConfig(FrigateBaseModel): class AuthConfig(FrigateBaseModel):
enabled: bool = Field(default=False, title="Enable authentication") enabled: bool = Field(default=False, title="Enable authentication")
# TODO: validation for cookie names
cookie_name: str = Field( cookie_name: str = Field(
default="jwt.token", title="Name for jwt token cookie", pattern=r"^[a-z]_*$" default="jwt.token", title="Name for jwt token cookie", pattern=r"^[a-z]_*$"
) )
@ -137,6 +136,10 @@ class AuthConfig(FrigateBaseModel):
title="Refresh the session if it is going to expire in this many seconds", title="Refresh the session if it is going to expire in this many seconds",
ge=30, ge=30,
) )
failed_login_rate_limit: Optional[str] = Field(
default="1/second;5/minute;20/hour",
title="Rate limits for failed login attempts.",
)
users: Optional[List[UserConfig]] = Field(default=[], title="Users") users: Optional[List[UserConfig]] = Field(default=[], title="Users")

View File

@ -6,7 +6,7 @@ import { cn } from "@/lib/utils";
import { Input } from "./ui/input"; import { Input } from "./ui/input";
import { Button } from "./ui/button"; import { Button } from "./ui/button";
import ActivityIndicator from "./indicators/activity-indicator"; import ActivityIndicator from "./indicators/activity-indicator";
import axios from "axios"; import axios, { AxiosError } from "axios";
import { Toaster } from "./ui/sonner"; import { Toaster } from "./ui/sonner";
import { toast } from "sonner"; import { toast } from "sonner";
import { import {
@ -56,9 +56,27 @@ export function UserAuthForm({ className, ...props }: UserAuthFormProps) {
); );
window.location.href = "/"; window.location.href = "/";
} catch (error) { } catch (error) {
toast.error("Login failed", { if (axios.isAxiosError(error)) {
position: "top-center", const err = error as AxiosError;
}); if (err.response?.status === 429) {
toast.error("Exceeded rate limit. Try again in 1 minute.", {
position: "top-center",
});
} else if (err.response?.status === 400) {
toast.error("Login failed", {
position: "top-center",
});
} else {
toast.error("Unknown error. Check logs.", {
position: "top-center",
});
}
} else {
toast.error("Unknown error. Check console logs.", {
position: "top-center",
});
}
setIsLoading(false); setIsLoading(false);
} }
}; };