diff --git a/docker/main/rootfs/usr/local/nginx/conf/auth_location.conf b/docker/main/rootfs/usr/local/nginx/conf/auth_location.conf index 9e2827c3d2..b14cd6ef50 100644 --- a/docker/main/rootfs/usr/local/nginx/conf/auth_location.conf +++ b/docker/main/rootfs/usr/local/nginx/conf/auth_location.conf @@ -19,6 +19,7 @@ location /auth { # this cannot be spoofed proxy_set_header X-Go2rtc-Stream-Proxy $go2rtc_stream_proxy; proxy_set_header X-Server-Port $server_port; + proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header Content-Length ""; # Pass along auth related info proxy_set_header Authorization $http_authorization; diff --git a/docs/docs/configuration/advanced/reference.md b/docs/docs/configuration/advanced/reference.md index 91e3b3b12e..eb6c980766 100644 --- a/docs/docs/configuration/advanced/reference.md +++ b/docs/docs/configuration/advanced/reference.md @@ -117,7 +117,8 @@ auth: # Optional: Cookie to store the JWT token for native auth (default: shown below) cookie_name: frigate_token # Optional: Set secure flag on cookie. (default: shown below) - # NOTE: This should be set to True if you are using TLS + # NOTE: Frigate sets the flag automatically when serving TLS. Set this to True + # when a reverse proxy terminates TLS in front of Frigate cookie_secure: False # Optional: Session length in seconds (default: shown below) session_length: 86400 # 24 hours diff --git a/frigate/api/auth.py b/frigate/api/auth.py index 235ac2dd8c..6ddd36fd4c 100644 --- a/frigate/api/auth.py +++ b/frigate/api/auth.py @@ -490,8 +490,15 @@ def refresh_user_session(request: Request, username: str) -> None: request.app.user_sessions[username] = _user_session(user) +def is_secure_request(request: Request) -> bool: + # nginx sets X-Forwarded-Proto to its own scheme, replacing any client value + return ( + request.app.frigate_config.auth.cookie_secure + or request.headers.get("x-forwarded-proto") == "https" + ) + + def set_jwt_cookie(response: Response, cookie_name, encoded_jwt, max_age, secure): - # TODO: ideally this would set secure as well, but that requires TLS # Starlette sets SameSite=Lax by default. The cookie is still sent to # same-site iframes (e.g. Home Assistant on the same host or domain), but # not to cross-site ones. CSRF is also mitigated by requiring a custom @@ -844,7 +851,7 @@ def auth(request: Request): JWT_COOKIE_NAME, new_encoded_jwt, JWT_SESSION_LENGTH, - request.app.frigate_config.auth.cookie_secure, + is_secure_request(request), ) success_response.headers["remote-user"] = user @@ -963,7 +970,7 @@ def login(request: Request, body: AppPostLoginBody): JWT_COOKIE_NAME, encoded_jwt, JWT_SESSION_LENGTH, - request.app.frigate_config.auth.cookie_secure, + is_secure_request(request), ) # Clear admin_first_time_login flag after successful admin login so the # UI stops showing the first-time login documentation link. @@ -1138,7 +1145,7 @@ async def update_password( JWT_COOKIE_NAME, encoded_jwt, JWT_SESSION_LENGTH, - request.app.frigate_config.auth.cookie_secure, + is_secure_request(request), ) return response diff --git a/frigate/config/auth.py b/frigate/config/auth.py index f821f0f2bf..a5f1990f2a 100644 --- a/frigate/config/auth.py +++ b/frigate/config/auth.py @@ -27,7 +27,7 @@ class AuthConfig(FrigateBaseModel): cookie_secure: bool = Field( default=False, title="Secure cookie flag", - description="Set the secure flag on the auth cookie; should be true when using TLS.", + description="Always set the secure flag on the auth cookie. Frigate sets it automatically when serving TLS; enable this when a reverse proxy terminates TLS in front of Frigate.", ) session_length: int = Field( default=86400, diff --git a/frigate/test/http_api/test_http_auth_jwt_role.py b/frigate/test/http_api/test_http_auth_jwt_role.py index 9dda7c1c70..b8004534a1 100644 --- a/frigate/test/http_api/test_http_auth_jwt_role.py +++ b/frigate/test/http_api/test_http_auth_jwt_role.py @@ -229,3 +229,45 @@ class TestAuthJwtRole(BaseTestHttp): self.assertEqual(resp.status_code, 202) self.assertIn("frigate_token=", resp.headers["set-cookie"]) + + def _login_cookie(self, headers: dict | None = None) -> str: + with AuthTestClient(self._create_app()) as client: + resp = client.post( + "/login", + json={"user": "bob", "password": PASSWORD}, + headers=headers or {}, + ) + + self.assertEqual(resp.status_code, 200) + return resp.headers["set-cookie"].lower() + + def test_login_cookie_is_secure_over_tls(self): + cookie = self._login_cookie({"x-forwarded-proto": "https"}) + self.assertIn("; secure", cookie) + + def test_login_cookie_is_not_secure_over_http(self): + cookie = self._login_cookie({"x-forwarded-proto": "http"}) + self.assertNotIn("; secure", cookie) + + def test_cookie_secure_config_forces_secure(self): + self.minimal_config["auth"]["cookie_secure"] = True + cookie = self._login_cookie() + self.assertIn("; secure", cookie) + + def test_refreshed_cookie_is_secure_over_tls(self): + app = self._create_app() + token = jwt.encode( + {"alg": "HS256"}, + {"sub": "bob", "exp": int(time.time()) + 60, "iat": int(time.time())}, + app.jwt_token, + ) + + with AuthTestClient(app) as client: + client.cookies.set("frigate_token", token) + resp = client.get( + "/auth", + headers={"x-server-port": "8971", "x-forwarded-proto": "https"}, + ) + + self.assertEqual(resp.status_code, 202) + self.assertIn("; secure", resp.headers["set-cookie"].lower()) diff --git a/web/public/locales/en/config/global.json b/web/public/locales/en/config/global.json index 35ce6f1051..2ce13b7808 100644 --- a/web/public/locales/en/config/global.json +++ b/web/public/locales/en/config/global.json @@ -40,7 +40,7 @@ }, "cookie_secure": { "label": "Secure cookie flag", - "description": "Set the secure flag on the auth cookie; should be true when using TLS." + "description": "Always set the secure flag on the auth cookie. Frigate sets it automatically when serving TLS; enable this when a reverse proxy terminates TLS in front of Frigate." }, "session_length": { "label": "Session length",