From f8f79e74270e2097715ab922de1a69f4110ea30f Mon Sep 17 00:00:00 2001 From: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> Date: Sun, 18 Oct 2026 13:12:43 -0500 Subject: [PATCH] reject users whose role was removed from the config A user whose role was removed from the config fell back to viewer, which has no camera restrictions, so a restricted role removed from yaml gained access to every camera. /auth now rejects that user's tokens, and login refuses them until an admin assigns a valid role. --- frigate/api/auth.py | 19 ++++++++++++------- .../test/http_api/test_http_auth_jwt_role.py | 16 ++++++++++++---- 2 files changed, 24 insertions(+), 11 deletions(-) diff --git a/frigate/api/auth.py b/frigate/api/auth.py index ca1a9422b9..29813e5a62 100644 --- a/frigate/api/auth.py +++ b/frigate/api/auth.py @@ -824,9 +824,11 @@ def auth(request: Request): logger.debug("jwt token issued before password change") return fail_response - # matches the fallback login applies to a role removed from the config if role not in auth_config.roles: - role = "viewer" + logger.debug( + "jwt user %s has role %s, which is not in the config", user, role + ) + return fail_response current_time = int(time.time()) @@ -961,13 +963,16 @@ def login(request: Request, body: AppPostLoginBody): password_hash = db_user.password_hash if verify_password(password, password_hash): - role = getattr(db_user, "role", "viewer") - config_roles_set = set(request.app.frigate_config.auth.roles.keys()) - if role not in config_roles_set: + role = db_user.role + if role not in request.app.frigate_config.auth.roles: logger.warning( - f"User {db_user.username} has an invalid role {role}, falling back to 'viewer'." + "Login refused for user '%s' with role '%s', which is not in the config", + user, + role, ) - role = "viewer" + failed_logins.record(user, time.time(), known=True) + return JSONResponse(content={"message": "Login failed"}, status_code=401) + expiration = int(time.time()) + JWT_SESSION_LENGTH encoded_jwt = create_encoded_jwt(user, role, expiration, request.app.jwt_token) response = Response("", 200) 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 0e200be495..f4814c829a 100644 --- a/frigate/test/http_api/test_http_auth_jwt_role.py +++ b/frigate/test/http_api/test_http_auth_jwt_role.py @@ -115,14 +115,22 @@ class TestAuthJwtRole(BaseTestHttp): self.assertEqual(resp.status_code, 202) self.assertEqual(resp.headers["remote-role"], "garage") - def test_role_removed_from_config_falls_back_to_viewer(self): + def test_role_removed_from_config_is_rejected(self): User.update(role="removed_role").where(User.username == "bob").execute() app = self._create_app() with AuthTestClient(app) as client: - resp = self._auth(client, self._token(app, "bob", "removed_role")) + for source in ("bearer", "cookie"): + resp = self._auth(client, self._token(app, "bob", "garage"), source) + self.assertEqual(resp.status_code, 401) + self.assertNotIn("remote-role", resp.headers) - self.assertEqual(resp.status_code, 202) - self.assertEqual(resp.headers["remote-role"], "viewer") + def test_login_refused_for_role_removed_from_config(self): + User.update(role="removed_role").where(User.username == "bob").execute() + with AuthTestClient(self._create_app()) as client: + resp = client.post("/login", json={"user": "bob", "password": PASSWORD}) + + self.assertEqual(resp.status_code, 401) + self.assertNotIn("set-cookie", resp.headers) def test_demoted_user_gets_new_role_on_next_request(self): app = self._create_app()