backend apis for auth

This commit is contained in:
Blake Blackshear 2024-05-05 10:01:34 -05:00
parent c708953342
commit fca92811e7
9 changed files with 254 additions and 3 deletions

View File

@ -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 \

View File

@ -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

View 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;
}

View 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;

View File

@ -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;

View File

@ -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;

View File

@ -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
```

View File

@ -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
View 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)