Review
JWT gate for CopperStill barrel-house locks
Barrel-house Bluetooth locks used a shared 4-digit PIN that every harvest temp knew. This PR swaps the pin check for short-lived staff JWTs issued by the CaskPass app: warehouse controllers POST the bearer token, we decode claims, and only open the rack if role is picker|cellarmaster and exp is still valid.
PyJWTTier 3securityjwtauthpyjwt
Click a line to flag it, pick one or more labels, then submit. If the change looks correct, approve it.
services/locks/caskpass_auth.py+20-4
| 1 | 1 | from datetime import datetime, timezone | |
| 2 | 2 | ||
| 3 | 3 | import jwt | |
| 4 | 4 | from fastapi import Header, HTTPException | |
| 5 | 5 | ||
| 6 | 6 | from app.config import settings | |
| 7 | 7 | ||
| 8 | - | def authorize_rack_open(pin: str, rack_id: str) -> dict: | |
| 9 | - | if pin != settings.BARREL_HOUSE_PIN: | |
| 10 | - | raise HTTPException(status_code=401, detail="bad pin") | |
| 11 | - | return {"rack_id": rack_id, "via": "pin"} | |
| 8 | + | ALLOWED_ROLES = {"picker", "cellarmaster"} | |
| 9 | + | ||
| 10 | + | def authorize_rack_open( | |
| 11 | + | rack_id: str, | |
| 12 | + | authorization: str = Header(...), | |
| 13 | + | ) -> dict: | |
| 14 | + | if not authorization.startswith("Bearer "): | |
| 15 | + | raise HTTPException(status_code=401, detail="missing bearer") | |
| 16 | + | token = authorization[len("Bearer "):].strip() | |
| 17 | + | # Staff app signs HS256 with STAFF_JWT_SECRET; trust the header alg. | |
| 18 | + | try: | |
| 19 | + | claims = jwt.decode(token, settings.STAFF_JWT_SECRET) | |
| 20 | + | except jwt.PyJWTError as exc: | |
| 21 | + | raise HTTPException(status_code=401, detail="invalid token") from exc | |
| 22 | + | if claims.get("role") not in ALLOWED_ROLES: | |
| 23 | + | raise HTTPException(status_code=403, detail="role not allowed") | |
| 24 | + | exp = claims.get("exp") | |
| 25 | + | if exp is None or datetime.fromtimestamp(exp, tz=timezone.utc) < datetime.now(timezone.utc): | |
| 26 | + | raise HTTPException(status_code=401, detail="token expired") | |
| 27 | + | return {"rack_id": rack_id, "sub": claims.get("sub"), "via": "jwt"} |