mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-02-11 05:35:25 +03:00
backend apis for auth
This commit is contained in:
parent
c708953342
commit
fca92811e7
@ -55,6 +55,7 @@ cd /tmp/nginx
|
||||
--with-file-aio \
|
||||
--with-http_sub_module \
|
||||
--with-http_ssl_module \
|
||||
--with-http_auth_request_module \
|
||||
--with-threads \
|
||||
--add-module=../nginx-vod-module \
|
||||
--add-module=../nginx-secure-token-module \
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
click == 8.1.*
|
||||
Flask == 3.0.*
|
||||
imutils == 0.5.*
|
||||
joserfc == 0.9.*
|
||||
markupsafe == 2.1.*
|
||||
matplotlib == 3.8.*
|
||||
mypy == 1.6.1
|
||||
|
||||
32
docker/main/rootfs/usr/local/nginx/conf/auth_location.conf
Normal file
32
docker/main/rootfs/usr/local/nginx/conf/auth_location.conf
Normal file
@ -0,0 +1,32 @@
|
||||
set $upstream_auth http://127.0.0.1:5001/auth;
|
||||
|
||||
## Virtual endpoint created by nginx to forward auth requests.
|
||||
location /auth {
|
||||
## Essential Proxy Configuration
|
||||
internal;
|
||||
proxy_pass $upstream_auth;
|
||||
|
||||
## Headers
|
||||
## The headers starting with X-* are required.
|
||||
proxy_set_header X-Original-Method $request_method;
|
||||
proxy_set_header X-Original-URL $scheme://$http_host$request_uri;
|
||||
proxy_set_header X-Forwarded-For $remote_addr;
|
||||
proxy_set_header Content-Length "";
|
||||
proxy_set_header Connection "";
|
||||
|
||||
## Basic Proxy Configuration
|
||||
proxy_pass_request_body off;
|
||||
proxy_next_upstream error timeout invalid_header http_500 http_502 http_503; # Timeout if the real server is dead
|
||||
proxy_redirect http:// $scheme://;
|
||||
proxy_http_version 1.1;
|
||||
proxy_cache_bypass $cookie_session;
|
||||
proxy_no_cache $cookie_session;
|
||||
proxy_buffers 4 32k;
|
||||
client_body_buffer_size 128k;
|
||||
|
||||
## Advanced Proxy Configuration
|
||||
send_timeout 5m;
|
||||
proxy_read_timeout 240;
|
||||
proxy_send_timeout 240;
|
||||
proxy_connect_timeout 240;
|
||||
}
|
||||
18
docker/main/rootfs/usr/local/nginx/conf/auth_request.conf
Normal file
18
docker/main/rootfs/usr/local/nginx/conf/auth_request.conf
Normal file
@ -0,0 +1,18 @@
|
||||
## Send a subrequest to verify if the user is authenticated and has permission to access the resource.
|
||||
auth_request /auth;
|
||||
|
||||
## Save the upstream metadata response headers from Authelia to variables.
|
||||
auth_request_set $user $upstream_http_remote_user;
|
||||
auth_request_set $groups $upstream_http_remote_groups;
|
||||
auth_request_set $name $upstream_http_remote_name;
|
||||
auth_request_set $email $upstream_http_remote_email;
|
||||
|
||||
## Inject the metadata response headers from the variables into the request made to the backend.
|
||||
proxy_set_header Remote-User $user;
|
||||
proxy_set_header Remote-Groups $groups;
|
||||
proxy_set_header Remote-Email $email;
|
||||
proxy_set_header Remote-Name $name;
|
||||
|
||||
## Refresh the cookie as needed
|
||||
auth_request_set $auth_cookie $upstream_http_set_cookie;
|
||||
add_header Set-Cookie $auth_cookie;
|
||||
@ -95,7 +95,10 @@ http {
|
||||
gzip on;
|
||||
gzip_types application/vnd.apple.mpegurl;
|
||||
|
||||
include auth_location.conf;
|
||||
|
||||
location /vod/ {
|
||||
include auth_request.conf;
|
||||
aio threads;
|
||||
vod hls;
|
||||
|
||||
@ -107,6 +110,7 @@ http {
|
||||
}
|
||||
|
||||
location /stream/ {
|
||||
include auth_request.conf;
|
||||
add_header Cache-Control "no-store";
|
||||
expires off;
|
||||
|
||||
@ -121,7 +125,7 @@ http {
|
||||
}
|
||||
|
||||
location /clips/ {
|
||||
|
||||
include auth_request.conf;
|
||||
types {
|
||||
video/mp4 mp4;
|
||||
image/jpeg jpg;
|
||||
@ -137,6 +141,7 @@ http {
|
||||
}
|
||||
|
||||
location /recordings/ {
|
||||
include auth_request.conf;
|
||||
types {
|
||||
video/mp4 mp4;
|
||||
}
|
||||
@ -147,6 +152,7 @@ http {
|
||||
}
|
||||
|
||||
location /exports/ {
|
||||
include auth_request.conf;
|
||||
types {
|
||||
video/mp4 mp4;
|
||||
}
|
||||
@ -157,17 +163,20 @@ http {
|
||||
}
|
||||
|
||||
location /ws {
|
||||
include auth_request.conf;
|
||||
proxy_pass http://mqtt_ws/;
|
||||
include proxy.conf;
|
||||
}
|
||||
|
||||
location /live/jsmpeg/ {
|
||||
include auth_request.conf;
|
||||
proxy_pass http://jsmpeg/;
|
||||
include proxy.conf;
|
||||
}
|
||||
|
||||
# frigate lovelace card uses this path
|
||||
location /live/mse/api/ws {
|
||||
include auth_request.conf;
|
||||
limit_except GET {
|
||||
deny all;
|
||||
}
|
||||
@ -176,6 +185,7 @@ http {
|
||||
}
|
||||
|
||||
location /live/webrtc/api/ws {
|
||||
include auth_request.conf;
|
||||
limit_except GET {
|
||||
deny all;
|
||||
}
|
||||
@ -185,6 +195,7 @@ http {
|
||||
|
||||
# pass through go2rtc player
|
||||
location /live/webrtc/webrtc.html {
|
||||
include auth_request.conf;
|
||||
limit_except GET {
|
||||
deny all;
|
||||
}
|
||||
@ -194,6 +205,7 @@ http {
|
||||
|
||||
# frontend uses this to fetch the version
|
||||
location /api/go2rtc/api {
|
||||
include auth_request.conf;
|
||||
limit_except GET {
|
||||
deny all;
|
||||
}
|
||||
@ -203,6 +215,7 @@ http {
|
||||
|
||||
# integration uses this to add webrtc candidate
|
||||
location /api/go2rtc/webrtc {
|
||||
include auth_request.conf;
|
||||
limit_except POST {
|
||||
deny all;
|
||||
}
|
||||
@ -211,12 +224,14 @@ http {
|
||||
}
|
||||
|
||||
location ~* /api/.*\.(jpg|jpeg|png|webp|gif)$ {
|
||||
include auth_request.conf;
|
||||
rewrite ^/api/(.*)$ $1 break;
|
||||
proxy_pass http://frigate_api;
|
||||
include proxy.conf;
|
||||
}
|
||||
|
||||
location /api/ {
|
||||
include auth_request.conf;
|
||||
add_header Cache-Control "no-store";
|
||||
expires off;
|
||||
proxy_pass http://frigate_api/;
|
||||
@ -231,12 +246,21 @@ http {
|
||||
add_header X-Cache-Status $upstream_cache_status;
|
||||
|
||||
location /api/vod/ {
|
||||
include auth_request.conf;
|
||||
proxy_pass http://frigate_api/vod/;
|
||||
include proxy.conf;
|
||||
proxy_cache off;
|
||||
}
|
||||
|
||||
location /api/login {
|
||||
auth_request off;
|
||||
rewrite ^/api(/.*)$ $1 break;
|
||||
proxy_pass http://frigate_api;
|
||||
include proxy.conf;
|
||||
}
|
||||
|
||||
location /api/stats {
|
||||
include auth_request.conf;
|
||||
access_log off;
|
||||
rewrite ^/api(/.*)$ $1 break;
|
||||
proxy_pass http://frigate_api;
|
||||
@ -244,6 +268,7 @@ http {
|
||||
}
|
||||
|
||||
location /api/version {
|
||||
include auth_request.conf;
|
||||
access_log off;
|
||||
rewrite ^/api(/.*)$ $1 break;
|
||||
proxy_pass http://frigate_api;
|
||||
@ -252,6 +277,7 @@ http {
|
||||
}
|
||||
|
||||
location / {
|
||||
# do not require auth for static assets
|
||||
add_header Cache-Control "no-store";
|
||||
expires off;
|
||||
|
||||
|
||||
@ -1,4 +1,26 @@
|
||||
proxy_http_version 1.1;
|
||||
## Headers
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "Upgrade";
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Original-URL $scheme://$http_host$request_uri;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header X-Forwarded-Host $http_host;
|
||||
proxy_set_header X-Forwarded-URI $request_uri;
|
||||
proxy_set_header X-Forwarded-Ssl on;
|
||||
proxy_set_header X-Forwarded-For $remote_addr;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
|
||||
## Basic Proxy Configuration
|
||||
client_body_buffer_size 128k;
|
||||
proxy_next_upstream error timeout invalid_header http_500 http_502 http_503; ## Timeout if the real server is dead.
|
||||
proxy_redirect http:// $scheme://;
|
||||
proxy_http_version 1.1;
|
||||
proxy_cache_bypass $cookie_session;
|
||||
proxy_no_cache $cookie_session;
|
||||
proxy_buffers 64 256k;
|
||||
|
||||
## Advanced Proxy Configuration
|
||||
send_timeout 5m;
|
||||
proxy_read_timeout 360;
|
||||
proxy_send_timeout 360;
|
||||
proxy_connect_timeout 360;
|
||||
@ -225,3 +225,13 @@ docker buildx create --name builder --driver docker-container --driver-opt netwo
|
||||
docker buildx inspect builder --bootstrap
|
||||
make push
|
||||
```
|
||||
|
||||
## Other
|
||||
|
||||
### Nginx
|
||||
|
||||
When testing nginx config changes from within the dev container, the following command can be used to copy and reload the config for testing without rebuilding the container:
|
||||
|
||||
```console
|
||||
sudo cp docker/main/rootfs/usr/local/nginx/conf/* /usr/local/nginx/conf/ && sudo /usr/local/nginx/sbin/nginx -s reload
|
||||
```
|
||||
|
||||
@ -14,6 +14,7 @@ from markupsafe import escape
|
||||
from peewee import operator
|
||||
from playhouse.sqliteq import SqliteQueueDatabase
|
||||
|
||||
from frigate.api.auth import AuthBp
|
||||
from frigate.api.event import EventBp
|
||||
from frigate.api.export import ExportBp
|
||||
from frigate.api.media import MediaBp
|
||||
@ -44,6 +45,7 @@ bp.register_blueprint(ExportBp)
|
||||
bp.register_blueprint(MediaBp)
|
||||
bp.register_blueprint(PreviewBp)
|
||||
bp.register_blueprint(ReviewBp)
|
||||
bp.register_blueprint(AuthBp)
|
||||
|
||||
|
||||
def create_app(
|
||||
|
||||
139
frigate/api/auth.py
Normal file
139
frigate/api/auth.py
Normal file
@ -0,0 +1,139 @@
|
||||
"""Auth apis."""
|
||||
|
||||
import base64
|
||||
import hashlib
|
||||
import logging
|
||||
import secrets
|
||||
import time
|
||||
from datetime import datetime
|
||||
|
||||
from flask import Blueprint, make_response, request
|
||||
from joserfc import jwt
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
AuthBp = Blueprint("auth", __name__)
|
||||
|
||||
|
||||
ALGORITHM = "pbkdf2_sha256"
|
||||
JWT_COOKIE_NAME = "jwt.cookie"
|
||||
JWT_SECRET = "secret"
|
||||
JWT_REFRESH = 120
|
||||
JWT_SESSION_LENGTH = 300
|
||||
|
||||
|
||||
def hash_password(password, salt=None, iterations=260000):
|
||||
if salt is None:
|
||||
salt = secrets.token_hex(16)
|
||||
assert salt and isinstance(salt, str) and "$" not in salt
|
||||
assert isinstance(password, str)
|
||||
pw_hash = hashlib.pbkdf2_hmac(
|
||||
"sha256", password.encode("utf-8"), salt.encode("utf-8"), iterations
|
||||
)
|
||||
b64_hash = base64.b64encode(pw_hash).decode("ascii").strip()
|
||||
return "{}${}${}${}".format(ALGORITHM, iterations, salt, b64_hash)
|
||||
|
||||
|
||||
def verify_password(password, password_hash):
|
||||
if (password_hash or "").count("$") != 3:
|
||||
return False
|
||||
algorithm, iterations, salt, b64_hash = password_hash.split("$", 3)
|
||||
iterations = int(iterations)
|
||||
assert algorithm == ALGORITHM
|
||||
compare_hash = hash_password(password, salt, iterations)
|
||||
return secrets.compare_digest(password_hash, compare_hash)
|
||||
|
||||
|
||||
def create_encoded_jwt(user, expiration, secret):
|
||||
return jwt.encode({"alg": "HS256"}, {"sub": user, "exp": expiration}, secret)
|
||||
|
||||
|
||||
def set_jwt_cookie(response, cookie_name, encoded_jwt, expiration):
|
||||
# TODO: ideally this would set secure as well, but that requires TLS
|
||||
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
|
||||
# - add users to config
|
||||
# - create login page
|
||||
# -
|
||||
|
||||
|
||||
@AuthBp.route("/auth")
|
||||
def auth():
|
||||
jwt_source = None
|
||||
encoded_token = None
|
||||
if "authorization" in request.headers and request.headers[
|
||||
"authorization"
|
||||
].startswith("Bearer "):
|
||||
jwt_source = "authorization"
|
||||
logger.debug("Found authorization header")
|
||||
encoded_token = request.headers["authorization"].replace("Bearer ", "")
|
||||
elif JWT_COOKIE_NAME in request.cookies:
|
||||
jwt_source = "cookie"
|
||||
logger.debug("Found jwt cookie")
|
||||
encoded_token = request.cookies[JWT_COOKIE_NAME]
|
||||
|
||||
if encoded_token is None:
|
||||
logger.debug("No jwt token found")
|
||||
return make_response({}, 401)
|
||||
|
||||
try:
|
||||
response = make_response({}, 202)
|
||||
|
||||
token = jwt.decode(encoded_token, JWT_SECRET)
|
||||
if "sub" not in token.claims:
|
||||
logger.debug("user not set in jwt token")
|
||||
return make_response({}, 401)
|
||||
if "exp" not in token.claims:
|
||||
logger.debug("exp not set in jwt token")
|
||||
return make_response({}, 401)
|
||||
|
||||
user = token.claims.get("sub")
|
||||
current_time = int(time.time())
|
||||
|
||||
# if the jwt is expired
|
||||
expiration = int(token.claims.get("exp"))
|
||||
logger.debug(
|
||||
f"current time: {datetime.fromtimestamp(current_time).strftime('%c')}"
|
||||
)
|
||||
logger.debug(
|
||||
f"jwt expires at: {datetime.fromtimestamp(expiration).strftime('%c')}"
|
||||
)
|
||||
logger.debug(
|
||||
f"jwt refresh at: {datetime.fromtimestamp(expiration - JWT_REFRESH).strftime('%c')}"
|
||||
)
|
||||
if expiration <= current_time:
|
||||
logger.debug("jwt token expired")
|
||||
return make_response({}, 401)
|
||||
|
||||
# if the jwt cookie is expiring soon
|
||||
elif jwt_source == "cookie" and expiration - JWT_REFRESH <= current_time:
|
||||
logger.debug("jwt token expiring soon, refreshing cookie")
|
||||
new_expiration = current_time + JWT_SESSION_LENGTH
|
||||
new_encoded_jwt = create_encoded_jwt(user, new_expiration, JWT_SECRET)
|
||||
set_jwt_cookie(response, JWT_COOKIE_NAME, new_encoded_jwt, new_expiration)
|
||||
|
||||
response.headers["remote-user"] = user
|
||||
return response
|
||||
except Exception as e:
|
||||
logger.error(f"Error parsing jwt: {e}")
|
||||
return make_response({}, 401)
|
||||
|
||||
# grab the jwt token from the authorization header or a cookie
|
||||
|
||||
|
||||
@AuthBp.route("/login", methods=["POST"])
|
||||
def login():
|
||||
password_hash = "pbkdf2_sha256$260000$2e68caab677bb466138d21d18ac94033$3VZhLOSiY9AqD2Y37DgVOerx4L2wi6nPyruoVXd06VQ="
|
||||
content = request.get_json()
|
||||
user = content["user"]
|
||||
password = content["password"]
|
||||
if verify_password(password, password_hash):
|
||||
expiration = int(time.time()) + JWT_SESSION_LENGTH
|
||||
encoded_jwt = create_encoded_jwt(user, expiration, JWT_SECRET)
|
||||
response = make_response({}, 200)
|
||||
set_jwt_cookie(response, JWT_COOKIE_NAME, encoded_jwt, expiration)
|
||||
return response
|
||||
return make_response({"message": "Login failed"}, 400)
|
||||
Loading…
Reference in New Issue
Block a user