mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-02-11 13:45:25 +03:00
implement JWT actual secret
This commit is contained in:
parent
38f25548b1
commit
bf35d16a89
@ -14,7 +14,7 @@ from markupsafe import escape
|
|||||||
from peewee import operator
|
from peewee import operator
|
||||||
from playhouse.sqliteq import SqliteQueueDatabase
|
from playhouse.sqliteq import SqliteQueueDatabase
|
||||||
|
|
||||||
from frigate.api.auth import AuthBp
|
from frigate.api.auth import AuthBp, get_jwt_secret
|
||||||
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
|
||||||
@ -85,6 +85,7 @@ def create_app(
|
|||||||
app.plus_api = plus_api
|
app.plus_api = plus_api
|
||||||
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.register_blueprint(bp)
|
app.register_blueprint(bp)
|
||||||
|
|
||||||
|
|||||||
@ -2,21 +2,78 @@
|
|||||||
|
|
||||||
import base64
|
import base64
|
||||||
import hashlib
|
import hashlib
|
||||||
|
import json
|
||||||
import logging
|
import logging
|
||||||
|
import os
|
||||||
import secrets
|
import secrets
|
||||||
import time
|
import time
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
from flask import Blueprint, current_app, make_response, request
|
from flask import Blueprint, current_app, make_response, request
|
||||||
from joserfc import jwt
|
from joserfc import jwt
|
||||||
|
|
||||||
|
from frigate.const import CONFIG_DIR, JWT_SECRET_ENV_VAR, PASSWORD_HASH_ALGORITHM
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
AuthBp = Blueprint("auth", __name__)
|
AuthBp = Blueprint("auth", __name__)
|
||||||
|
|
||||||
|
|
||||||
ALGORITHM = "pbkdf2_sha256"
|
def get_jwt_secret() -> str:
|
||||||
JWT_SECRET = "secret"
|
jwt_secret = None
|
||||||
|
# check env var
|
||||||
|
if JWT_SECRET_ENV_VAR in os.environ:
|
||||||
|
logger.debug(
|
||||||
|
f"Using jwt secret from {JWT_SECRET_ENV_VAR} environment variable."
|
||||||
|
)
|
||||||
|
jwt_secret = os.environ.get(JWT_SECRET_ENV_VAR)
|
||||||
|
# check docker secrets
|
||||||
|
elif (
|
||||||
|
os.path.isdir("/run/secrets")
|
||||||
|
and os.access("/run/secrets", os.R_OK)
|
||||||
|
and JWT_SECRET_ENV_VAR in os.listdir("/run/secrets")
|
||||||
|
):
|
||||||
|
logger.debug(f"Using jwt secret from {JWT_SECRET_ENV_VAR} docker secret file.")
|
||||||
|
jwt_secret = Path(os.path.join("/run/secrets", JWT_SECRET_ENV_VAR)).read_text()
|
||||||
|
# check for the addon options file
|
||||||
|
elif os.path.isfile("/data/options.json"):
|
||||||
|
with open("/data/options.json") as f:
|
||||||
|
raw_options = f.read()
|
||||||
|
logger.debug("Using jwt secret from Home Assistant addon options file.")
|
||||||
|
options = json.loads(raw_options)
|
||||||
|
jwt_secret = options.get("jwt_secret")
|
||||||
|
|
||||||
|
if jwt_secret is None:
|
||||||
|
jwt_secret_file = os.path.join(CONFIG_DIR, ".jwt_secret")
|
||||||
|
# check .jwt_secrets file
|
||||||
|
if not os.path.isfile(jwt_secret_file):
|
||||||
|
logger.debug(
|
||||||
|
"No jwt secret found. Generating one and storing in .jwt_secret file in config directory."
|
||||||
|
)
|
||||||
|
jwt_secret = secrets.token_hex(64)
|
||||||
|
try:
|
||||||
|
with open(jwt_secret_file, "w") as f:
|
||||||
|
f.write(str(jwt_secret))
|
||||||
|
except Exception:
|
||||||
|
logger.warn(
|
||||||
|
"Unable to write jwt token file to config directory. A new jwt token will be created at each startup."
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
logger.debug("Using jwt secret from .jwt_secret file in config directory.")
|
||||||
|
with open(jwt_secret_file) as f:
|
||||||
|
try:
|
||||||
|
jwt_secret = f.readline()
|
||||||
|
except Exception:
|
||||||
|
logger.warn(
|
||||||
|
"Unable to read jwt token from .jwt_secret file in config directory. A new jwt token will be created at each startup."
|
||||||
|
)
|
||||||
|
jwt_secret = secrets.token_hex(64)
|
||||||
|
|
||||||
|
if len(jwt_secret) < 64:
|
||||||
|
logger.warn("JWT Secret is recommended to be 64 characters or more")
|
||||||
|
|
||||||
|
return jwt_secret
|
||||||
|
|
||||||
|
|
||||||
def hash_password(password, salt=None, iterations=260000):
|
def hash_password(password, salt=None, iterations=260000):
|
||||||
@ -28,7 +85,7 @@ def hash_password(password, salt=None, iterations=260000):
|
|||||||
"sha256", password.encode("utf-8"), salt.encode("utf-8"), iterations
|
"sha256", password.encode("utf-8"), salt.encode("utf-8"), iterations
|
||||||
)
|
)
|
||||||
b64_hash = base64.b64encode(pw_hash).decode("ascii").strip()
|
b64_hash = base64.b64encode(pw_hash).decode("ascii").strip()
|
||||||
return "{}${}${}${}".format(ALGORITHM, iterations, salt, b64_hash)
|
return "{}${}${}${}".format(PASSWORD_HASH_ALGORITHM, iterations, salt, b64_hash)
|
||||||
|
|
||||||
|
|
||||||
def verify_password(password, password_hash):
|
def verify_password(password, password_hash):
|
||||||
@ -36,7 +93,7 @@ def verify_password(password, password_hash):
|
|||||||
return False
|
return False
|
||||||
algorithm, iterations, salt, b64_hash = password_hash.split("$", 3)
|
algorithm, iterations, salt, b64_hash = password_hash.split("$", 3)
|
||||||
iterations = int(iterations)
|
iterations = int(iterations)
|
||||||
assert algorithm == ALGORITHM
|
assert algorithm == PASSWORD_HASH_ALGORITHM
|
||||||
compare_hash = hash_password(password, salt, iterations)
|
compare_hash = hash_password(password, salt, iterations)
|
||||||
return secrets.compare_digest(password_hash, compare_hash)
|
return secrets.compare_digest(password_hash, compare_hash)
|
||||||
|
|
||||||
@ -50,13 +107,11 @@ def set_jwt_cookie(response, cookie_name, encoded_jwt, expiration):
|
|||||||
response.set_cookie(cookie_name, encoded_jwt, httponly=True, expires=expiration)
|
response.set_cookie(cookie_name, encoded_jwt, httponly=True, expires=expiration)
|
||||||
|
|
||||||
|
|
||||||
# TODO:
|
|
||||||
# - on startup, generate a signing secret for jwt if it doesn't exist and save as ".auth-token" in the config folder
|
|
||||||
# -
|
|
||||||
|
|
||||||
|
|
||||||
@AuthBp.route("/auth")
|
@AuthBp.route("/auth")
|
||||||
def auth():
|
def auth():
|
||||||
|
if not current_app.frigate_config.auth.enabled:
|
||||||
|
return make_response({}, 202)
|
||||||
|
|
||||||
JWT_COOKIE_NAME = current_app.frigate_config.auth.cookie_name
|
JWT_COOKIE_NAME = current_app.frigate_config.auth.cookie_name
|
||||||
JWT_REFRESH = current_app.frigate_config.auth.refresh_time
|
JWT_REFRESH = current_app.frigate_config.auth.refresh_time
|
||||||
JWT_SESSION_LENGTH = current_app.frigate_config.auth.session_length
|
JWT_SESSION_LENGTH = current_app.frigate_config.auth.session_length
|
||||||
@ -81,7 +136,7 @@ def auth():
|
|||||||
try:
|
try:
|
||||||
response = make_response({}, 202)
|
response = make_response({}, 202)
|
||||||
|
|
||||||
token = jwt.decode(encoded_token, JWT_SECRET)
|
token = jwt.decode(encoded_token, current_app.jwt_token)
|
||||||
if "sub" not in token.claims:
|
if "sub" not in token.claims:
|
||||||
logger.debug("user not set in jwt token")
|
logger.debug("user not set in jwt token")
|
||||||
return make_response({}, 401)
|
return make_response({}, 401)
|
||||||
@ -111,7 +166,9 @@ def auth():
|
|||||||
elif jwt_source == "cookie" and expiration - JWT_REFRESH <= current_time:
|
elif jwt_source == "cookie" and expiration - JWT_REFRESH <= current_time:
|
||||||
logger.debug("jwt token expiring soon, refreshing cookie")
|
logger.debug("jwt token expiring soon, refreshing cookie")
|
||||||
new_expiration = current_time + JWT_SESSION_LENGTH
|
new_expiration = current_time + JWT_SESSION_LENGTH
|
||||||
new_encoded_jwt = create_encoded_jwt(user, new_expiration, JWT_SECRET)
|
new_encoded_jwt = create_encoded_jwt(
|
||||||
|
user, new_expiration, current_app.jwt_token
|
||||||
|
)
|
||||||
set_jwt_cookie(response, JWT_COOKIE_NAME, new_encoded_jwt, new_expiration)
|
set_jwt_cookie(response, JWT_COOKIE_NAME, new_encoded_jwt, new_expiration)
|
||||||
|
|
||||||
response.headers["remote-user"] = user
|
response.headers["remote-user"] = user
|
||||||
@ -120,8 +177,6 @@ def auth():
|
|||||||
logger.error(f"Error parsing jwt: {e}")
|
logger.error(f"Error parsing jwt: {e}")
|
||||||
return make_response({}, 401)
|
return make_response({}, 401)
|
||||||
|
|
||||||
# grab the jwt token from the authorization header or a cookie
|
|
||||||
|
|
||||||
|
|
||||||
@AuthBp.route("/login", methods=["POST"])
|
@AuthBp.route("/login", methods=["POST"])
|
||||||
def login():
|
def login():
|
||||||
@ -143,7 +198,7 @@ def login():
|
|||||||
make_response({"message": "Login failed"}, 400)
|
make_response({"message": "Login failed"}, 400)
|
||||||
if verify_password(password, password_hash):
|
if verify_password(password, password_hash):
|
||||||
expiration = int(time.time()) + JWT_SESSION_LENGTH
|
expiration = int(time.time()) + JWT_SESSION_LENGTH
|
||||||
encoded_jwt = create_encoded_jwt(user, expiration, JWT_SECRET)
|
encoded_jwt = create_encoded_jwt(user, expiration, current_app.jwt_token)
|
||||||
response = make_response({}, 200)
|
response = make_response({}, 200)
|
||||||
set_jwt_cookie(response, JWT_COOKIE_NAME, encoded_jwt, expiration)
|
set_jwt_cookie(response, JWT_COOKIE_NAME, encoded_jwt, expiration)
|
||||||
return response
|
return response
|
||||||
|
|||||||
@ -91,3 +91,8 @@ AUTOTRACKING_MAX_MOVE_METRICS = 500
|
|||||||
AUTOTRACKING_ZOOM_OUT_HYSTERESIS = 1.1
|
AUTOTRACKING_ZOOM_OUT_HYSTERESIS = 1.1
|
||||||
AUTOTRACKING_ZOOM_IN_HYSTERESIS = 0.95
|
AUTOTRACKING_ZOOM_IN_HYSTERESIS = 0.95
|
||||||
AUTOTRACKING_ZOOM_EDGE_THRESHOLD = 0.05
|
AUTOTRACKING_ZOOM_EDGE_THRESHOLD = 0.05
|
||||||
|
|
||||||
|
# Auth
|
||||||
|
|
||||||
|
JWT_SECRET_ENV_VAR = "FRIGATE_JWT_SECRET"
|
||||||
|
PASSWORD_HASH_ALGORITHM = "pbkdf2_sha256"
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user