removed authorization for browser user

This commit is contained in:
2026-08-09 17:25:45 +02:00
parent b477737bad
commit 2677c81d0f
4 changed files with 18 additions and 61 deletions
+13 -19
View File
@@ -29,9 +29,8 @@ python3 -m venv .venv
.venv/bin/python app.py
```
On the iPhone, open `http://YOUR-COMPUTER-IP:5000/?key=YOUR_API_KEY` in Safari.
The key is saved only for that browser session and removed from the displayed
URL. The iPhone and computer must be on the same local network.
On the iPhone, open `http://YOUR-COMPUTER-IP:5000/` in Safari. The iPhone and
computer must be on the same local network.
## Configure buttons and scenes
@@ -71,16 +70,14 @@ Credentials are saved with owner-only permissions in
```yaml
bridge: 192.168.1.10
app_key: your-hue-application-key. # the hue bride api key
api_key: your-dashboard-api-key # the key for this services api
app_key: your-hue-application-key
```
The file is ignored by Git and should not be shared. `hue.py register` creates
or updates it, including a random dashboard API key. You can override values
for either tool with environment variables:
or updates it. You can override the bridge values with environment variables:
```sh
FASTHUE_API_KEY=YOUR_DASHBOARD_KEY HUE_BRIDGE=192.168.1.10 HUE_APP_KEY=YOUR_HUE_KEY .venv/bin/python app.py
HUE_BRIDGE=192.168.1.10 HUE_APP_KEY=YOUR_HUE_KEY .venv/bin/python app.py
```
The reverse-proxy path is configured separately in `config.yaml`:
@@ -93,17 +90,16 @@ Set it to an empty value when FastHue is served at `/`.
## HTTP API
Every API request needs the single `api_key` from `secrets.yaml` as a Bearer
token. Override it with `FASTHUE_API_KEY` or `--api-key` when necessary.
The dashboard API does not require a key. Do not expose it to untrusted users:
every client that can reach it can read and control the configured lights.
```sh
export FASTHUE_API_KEY='the-api_key-from-secrets.yaml'
curl -H "Authorization: Bearer $FASTHUE_API_KEY" http://localhost:5000/api/groups
curl -X POST -H "Authorization: Bearer $FASTHUE_API_KEY" \
curl http://localhost:5000/api/groups
curl -X POST \
http://localhost:5000/api/groups/GROUP_UUID/next-scene
curl -X POST -H "Authorization: Bearer $FASTHUE_API_KEY" \
curl -X POST \
http://localhost:5000/api/groups/GROUP_UUID/off
curl -X POST -H "Authorization: Bearer $FASTHUE_API_KEY" \
curl -X POST \
-H 'Content-Type: application/json' \
-d '{"name":"Relax"}' \
'http://localhost:5000/api/groups/Ground%20floor/scene'
@@ -190,7 +186,6 @@ server {
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Prefix /hue;
proxy_set_header Authorization $http_authorization;
}
}
```
@@ -199,9 +194,8 @@ The `www-data` group in the systemd unit and `--umask 007` make the socket
readable by Nginx. If your Nginx workers use another group, replace `www-data`
in the unit with that group. Test and reload Nginx with
`sudo nginx -t && sudo systemctl reload nginx`.
Use the HTTPS URL on the iPhone with `/hue/?key=YOUR_API_KEY` once per browser
session. Set `url_prefix` in `config.yaml` to another leading-slash prefix when
needed, and use the same value in Nginx's `location` and
Use the HTTPS URL on the iPhone with `/hue/`. Set `url_prefix` in `config.yaml`
to another leading-slash prefix when needed, and use the same value in Nginx's `location` and
`X-Forwarded-Prefix`. Keep
`secrets.yaml` readable only by the service account.
+3 -27
View File
@@ -4,7 +4,6 @@
from __future__ import annotations
import argparse
import hmac
import os
from pathlib import Path
from typing import Any
@@ -124,7 +123,7 @@ def next_scene(button: dict[str, Any], bridge: HueBridge) -> dict[str, Any]:
return {"name": selected.get("metadata", {}).get("name", "scene")}
def create_app(yaml_path: Path, verify_tls: bool, api_key: str, url_prefix: str = "") -> Flask:
def create_app(yaml_path: Path, verify_tls: bool, url_prefix: str = "") -> Flask:
app = Flask(__name__)
if url_prefix:
# Gunicorn only receives traffic over its private socket from Nginx.
@@ -132,22 +131,11 @@ def create_app(yaml_path: Path, verify_tls: bool, api_key: str, url_prefix: str
app.wsgi_app = ProxyFix(app.wsgi_app, x_prefix=1)
buttons = read_dashboard(yaml_path)
def api_key_required(view: Any) -> Any:
def protected(*args: Any, **kwargs: Any) -> Any:
authorization = request.headers.get("Authorization", "")
expected = f"Bearer {api_key}"
if not hmac.compare_digest(authorization, expected):
return jsonify({"error": "Unauthorized"}), 401
return view(*args, **kwargs)
protected.__name__ = view.__name__
return protected
@app.get("/")
def index() -> str:
return render_template("index.html", buttons=buttons)
@app.get("/api/groups")
@api_key_required
def groups() -> Any:
try:
return jsonify({"groups": dashboard_state(buttons, verify_tls)})
@@ -155,7 +143,6 @@ def create_app(yaml_path: Path, verify_tls: bool, api_key: str, url_prefix: str
return jsonify({"error": str(exc)}), 503
@app.post("/api/groups/<group_id>/next-scene")
@api_key_required
def activate_next_scene(group_id: str) -> Any:
try:
button = next(item for item in buttons if item["id"] == group_id)
@@ -168,7 +155,6 @@ def create_app(yaml_path: Path, verify_tls: bool, api_key: str, url_prefix: str
return jsonify({"error": str(exc)}), 503
@app.post("/api/groups/<group_id>/off")
@api_key_required
def turn_group_off(group_id: str) -> Any:
try:
button = next(item for item in buttons if item["id"] == group_id)
@@ -182,7 +168,6 @@ def create_app(yaml_path: Path, verify_tls: bool, api_key: str, url_prefix: str
return jsonify({"error": str(exc)}), 503
@app.post("/api/groups/<group_name>/scene")
@api_key_required
def activate_group_scene(group_name: str) -> Any:
payload = request.get_json(silent=True) or {}
scene_name = payload.get("name")
@@ -226,12 +211,8 @@ def configured_url_prefix(path: Path = APP_CONFIG) -> str:
def create_gunicorn_app() -> Flask:
"""Gunicorn application factory; configuration is read from environment/files."""
config_path = Path(os.environ.get("FASTHUE_DASHBOARD_CONFIG", ROOT / "hue.yaml"))
secrets_config = load_config(Path(os.environ.get("FASTHUE_SECRETS_FILE", DEFAULT_CONFIG)).expanduser())
api_key = os.environ.get("FASTHUE_API_KEY") or secrets_config.get("api_key")
if not api_key:
raise RuntimeError("add api_key to secrets.yaml or set FASTHUE_API_KEY")
verify_tls = os.environ.get("FASTHUE_VERIFY_TLS", "").casefold() in {"1", "true", "yes"}
return create_app(config_path, verify_tls, api_key, configured_url_prefix())
return create_app(config_path, verify_tls, configured_url_prefix())
# Older Gunicorn releases do not support --factory. Expose a WSGI callable at
@@ -244,19 +225,14 @@ def main() -> None:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--config", type=Path, default=ROOT / "hue.yaml", help="Dashboard YAML file")
parser.add_argument("--verify-tls", action="store_true", help="Verify the Hue bridge certificate")
parser.add_argument("--api-key", help="Required Bearer token for the dashboard API")
parser.add_argument("--url-prefix", help="Override the reverse-proxy URL prefix from config.yaml")
parser.add_argument("--host", default="0.0.0.0", help="Listen address")
parser.add_argument("--port", type=int, default=5000, help="Listen port")
args = parser.parse_args()
secrets_config = load_config(Path(os.environ.get("FASTHUE_SECRETS_FILE", DEFAULT_CONFIG)).expanduser())
api_key = args.api_key or os.environ.get("FASTHUE_API_KEY") or secrets_config.get("api_key")
if not api_key:
parser.error("add api_key to secrets.yaml, set FASTHUE_API_KEY, or supply --api-key")
url_prefix = args.url_prefix.rstrip("/") if args.url_prefix is not None else configured_url_prefix()
if url_prefix and not url_prefix.startswith("/"):
parser.error("--url-prefix must start with /, for example /hue")
create_app(args.config, args.verify_tls, api_key, url_prefix).run(host=args.host, port=args.port, debug=False)
create_app(args.config, args.verify_tls, url_prefix).run(host=args.host, port=args.port, debug=False)
if __name__ == "__main__":
+1 -3
View File
@@ -9,7 +9,6 @@ from __future__ import annotations
import argparse
import json
import secrets
import ssl
import sys
from dataclasses import dataclass
@@ -229,9 +228,8 @@ def register(args: argparse.Namespace) -> None:
**existing,
"bridge": bridge.host,
"app_key": app_key,
"api_key": existing.get("api_key", secrets.token_urlsafe(32)),
})
print(f"Saved bridge address, application key, and dashboard API key in {config_path}")
print(f"Saved bridge address and application key in {config_path}")
def build_parser() -> argparse.ArgumentParser:
+1 -12
View File
@@ -1,20 +1,9 @@
const status = document.querySelector('#status');
const buttons = [...document.querySelectorAll('.group-button')];
const apiPrefix = document.body.dataset.apiPrefix;
const url = new URL(window.location.href);
const suppliedKey = url.searchParams.get('key');
if (suppliedKey) {
sessionStorage.setItem('fasthue-api-key', suppliedKey);
url.searchParams.delete('key');
history.replaceState({}, '', url);
}
const apiKey = sessionStorage.getItem('fasthue-api-key');
function apiFetch(path, options = {}) {
return fetch(path, {
...options,
headers: { ...options.headers, Authorization: `Bearer ${apiKey || ''}` },
});
return fetch(path, options);
}
function showStatus(message, persistent = false) {