Make player token reset non-breaking (deferred rotation)
The maintenance-page reset used to rotate auth_code immediately. Because the token also guards the playlist endpoints, that stopped a player from syncing content the instant the button was pressed -- before the player ever had a chance to react -- and clients that do not understand the heartbeat's 401 auth_required would have stayed broken until redeployed. Split the model behaviour: * request_reauth() -- sets the flag only (used by the admin reset endpoints and the automatic interval). The current token stays valid, so there is no outage while the player is being asked to re-authenticate. * issue_new_token() -- replaces the token now (used by the existing "regenerate auth code" action, where the admin is shown the new value). * mark_authenticated() -- if a reset was pending, this is where the old token is retired: a new token is issued and returned to the player in the /api/auth/player response. Net effect: the reset is a request the player acts on, never a forced logout. A player that ignores it keeps working and shows as "awaiting re-auth" in the UI; one that handles it gets a fresh token and the old one stops working. Maintenance page wording updated to match (request vs reset; "awaiting re-auth" badge), plus a themed info note explaining it is not a forced logout. Adds docs/player-auth-reset.md describing the protocol and exactly what the player client (Kiwy-Signage) still needs to implement: detect 401 / auth_required on the heartbeat (and on the token-guarded endpoints), re-run POST /api/auth/player with the stored quick-connect code, save the new auth_code and retry. No password or SSH redeploy required. Verified end-to-end: token unchanged and still accepted after the request (200), heartbeat answers 401 auth_required=token_reset, re-auth issues a different token, the old token is then rejected (403) and the new one accepted (200), flag cleared; the interval path only flags without rotating.
This commit is contained in:
@@ -0,0 +1,166 @@
|
||||
# Player authentication & token re-authentication
|
||||
|
||||
Server-side implementation reference for the player client
|
||||
(`Kiwy-Signage`, https://gitea.moto-adv.com/ske087/Kiwy-Signage.git).
|
||||
|
||||
This document describes how the DigiServer side behaves so the player can be
|
||||
updated to match. **The server side is complete; the client still needs to
|
||||
handle the re-authentication request** (see "What the player must implement").
|
||||
|
||||
---
|
||||
|
||||
## 1. How a player authenticates today
|
||||
|
||||
The player reads `config/app_config.json` (written by the SSH deploy step),
|
||||
which contains:
|
||||
|
||||
```json
|
||||
{
|
||||
"server_ip": "192.168.0.159",
|
||||
"port": "443",
|
||||
"screen_name": "rpi-test1", // == Player.hostname on the server
|
||||
"quickconnect_key": "AB12CD", // == Player.quickconnect_code (plaintext)
|
||||
"orientation": "Landscape",
|
||||
"use_https": true,
|
||||
"verify_ssl": false
|
||||
}
|
||||
```
|
||||
|
||||
Note the player builds URLs as `{scheme}://{server_ip}:{port}/api/...` — it does
|
||||
**not** use any path prefix.
|
||||
|
||||
### Authentication flow (documented in `docs/05-blueprints-api.md`)
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant P as Player
|
||||
participant A as DigiServer
|
||||
P->>A: POST /api/auth/player {hostname, quickconnect_code}
|
||||
A-->>P: {auth_code, player_id, playlist_id, orientation}
|
||||
Note over P: stores auth_code (player_auth.json)
|
||||
P->>A: GET /api/playlists/<player_id> (Bearer auth_code)
|
||||
A-->>P: playlist + playlist_version
|
||||
loop refresh poll
|
||||
P->>A: GET /api/playlist-version/<player_id> (Bearer auth_code)
|
||||
A-->>P: {playlist_version, content_count}
|
||||
end
|
||||
loop heartbeat (~15s)
|
||||
P->>A: POST /api/player-feedback {hostname, status, ...}
|
||||
A-->>P: {success: true}
|
||||
end
|
||||
```
|
||||
|
||||
### Which endpoints use the token
|
||||
|
||||
| Endpoint | Auth |
|
||||
|---|---|
|
||||
| `POST /api/auth/player` | hostname + password **or** quickconnect code (no token) |
|
||||
| `GET /api/playlists?hostname=&quickconnect_code=` | quickconnect code (no token) |
|
||||
| `GET /api/playlists/<player_id>` | **Bearer `auth_code`** |
|
||||
| `GET /api/playlist-version/<player_id>` | **Bearer `auth_code`** |
|
||||
| `POST /api/player-edit-media` | **Bearer `auth_code`** |
|
||||
| `POST /api/player-feedback` (heartbeat) | none — the player is looked up by hostname |
|
||||
|
||||
Because the token guards the playlist endpoints, **an invalid token stops
|
||||
content sync**, not just auth.
|
||||
|
||||
---
|
||||
|
||||
## 2. Re-authentication request (what changed on the server)
|
||||
|
||||
An admin can request re-authentication from **Admin → Maintenance → Player
|
||||
Authentication Tokens**, either per player or for all players. There is also an
|
||||
optional automatic interval (days) after which the request fires on its own.
|
||||
|
||||
A re-authentication request is **deferred** — the server does *not* invalidate
|
||||
the current token:
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Admin
|
||||
participant A as DigiServer
|
||||
participant P as Player
|
||||
Admin->>A: request re-auth
|
||||
A->>A: auth_reset_required = true (token unchanged)
|
||||
P->>A: POST /api/player-feedback
|
||||
A-->>P: 401 {auth_required: true, reason: "token_reset"}
|
||||
Note over P: ⚠️ CLIENT ACTION REQUIRED HERE
|
||||
P->>A: POST /api/auth/player {hostname, quickconnect_code}
|
||||
A->>A: issue NEW token, clear flag
|
||||
A-->>P: {auth_code: NEW}
|
||||
Note over P: store new auth_code, retry requests
|
||||
P->>A: GET /api/playlists/<id> (Bearer NEW) ✅
|
||||
```
|
||||
|
||||
Key points:
|
||||
|
||||
* Until the player re-authenticates, its **old token remains valid** — so a
|
||||
player that ignores the request keeps syncing content (no outage).
|
||||
* The new token is issued **during** `POST /api/auth/player`, and returned in
|
||||
the `auth_code` field of the response, exactly like first-time setup.
|
||||
* The player needs **no password**: the `quickconnect_key` already in
|
||||
`app_config.json` is sufficient.
|
||||
* No SSH, restart, or redeploy is required.
|
||||
* The admin UI shows the player as **awaiting re-auth** until this completes;
|
||||
the admin can also **Cancel** the request.
|
||||
|
||||
---
|
||||
|
||||
## 3. What the player must implement
|
||||
|
||||
Today, if the client never handles the request, nothing breaks — but the reset
|
||||
never happens (the player keeps its old token forever and the admin UI stays on
|
||||
"awaiting re-auth").
|
||||
|
||||
To make the reset effective, the player client should:
|
||||
|
||||
1. **Detect the request.** On the heartbeat response, treat
|
||||
`HTTP 401` with `auth_required: true` as "re-authenticate", e.g.:
|
||||
|
||||
```python
|
||||
resp = requests.post(f"{base}/api/player-feedback", json=payload, timeout=10)
|
||||
if resp.status_code == 401:
|
||||
body = resp.json() if resp.content else {}
|
||||
if body.get("auth_required"):
|
||||
reauthenticate(reason=body.get("reason")) # "token_reset" | "token_expired"
|
||||
return
|
||||
```
|
||||
|
||||
2. **Also handle 401 on the token-guarded calls.** If the token is ever
|
||||
rejected by `/api/playlists/<id>` or `/api/playlist-version/<id>`, run the
|
||||
same re-authentication path. This makes the client self-healing even if the
|
||||
heartbeat was missed.
|
||||
|
||||
3. **Re-authenticate using the stored quick-connect code:**
|
||||
|
||||
```python
|
||||
def reauthenticate():
|
||||
cfg = load_app_config() # config/app_config.json
|
||||
r = requests.post(f"{base}/api/auth/player", json={
|
||||
"hostname": cfg["screen_name"],
|
||||
"quickconnect_code": cfg["quickconnect_key"],
|
||||
}, timeout=15)
|
||||
r.raise_for_status()
|
||||
save_auth(r.json()["auth_code"]) # player_auth.json
|
||||
# then resume: fetch playlist + version with the new token
|
||||
```
|
||||
|
||||
4. **Retry the failed request** with the new token so sync resumes seamlessly.
|
||||
|
||||
5. **Be idempotent / back off.** While a request is pending the heartbeat keeps
|
||||
answering `401`; avoid a tight retry loop (e.g. re-authenticate at most once
|
||||
per N seconds, or immediately then resume the normal heartbeat cadence).
|
||||
|
||||
### Recommended: also add `re-auth` to the edit-media path
|
||||
|
||||
`POST /api/player-edit-media` is token-guarded too, so the same 401 →
|
||||
re-authenticate → retry wrapper should apply there.
|
||||
|
||||
---
|
||||
|
||||
## 4. Manual alternative (no client change needed)
|
||||
|
||||
Redeploying the player via **Admin → Build Player Files / Deploy** rewrites
|
||||
`config/app_config.json` and deletes `player_auth.json`, which forces a clean
|
||||
authentication on next start. This is the deterministic path and is what the
|
||||
maintenance reset is intended to replace (it is remote and non-disruptive).
|
||||
Reference in New Issue
Block a user