docs: the ack token is scoped, not single-use
The handler never deletes the token: it stays valid until expires_at and is purged by the sweeper, so a second tap is an idempotent no-op rather than a rejection. Caught by pressing Acknowledge twice against the live server. What bounds the token is scope -- one incident, one action, one day -- not a use count.
This commit is contained in:
@@ -211,10 +211,17 @@ Three things get pushed:
|
|||||||
|
|
||||||
Notifications carry an **Acknowledge** button that acknowledges the incident
|
Notifications carry an **Acknowledge** button that acknowledges the incident
|
||||||
without opening anything. It POSTs to `/api/notify/ack/{token}`, an
|
without opening anything. It POSTs to `/api/notify/ack/{token}`, an
|
||||||
unauthenticated route authorised by the 256-bit single-use token in its path —
|
unauthenticated route authorised by the 256-bit token in its path — minted fresh
|
||||||
minted fresh per notification, scoped to one incident and one action, and valid
|
per notification, scoped to one incident and one action, and valid for 24 hours.
|
||||||
for 24 hours. A real API key is never put in a notification, because the message
|
A real API key is never put in a notification, because the message is stored on
|
||||||
is stored on the ntfy server and cached on the device.
|
the ntfy server and cached on the device.
|
||||||
|
|
||||||
|
The token is **not** consumed by use. Acknowledging is idempotent, so a token
|
||||||
|
stays valid for its full 24 hours and a second tap is a no-op that reports the
|
||||||
|
incident's current state rather than an error — which is what you want when a
|
||||||
|
tap is retried on a flaky mobile connection. What bounds it is scope, not a use
|
||||||
|
count: one incident, one action, one day. Expired tokens are purged by the
|
||||||
|
sweeper.
|
||||||
|
|
||||||
Two consequences worth planning for:
|
Two consequences worth planning for:
|
||||||
|
|
||||||
@@ -281,7 +288,7 @@ Authorization: Bearer <api-key>
|
|||||||
|
|
||||||
| Method | Path | Description |
|
| Method | Path | Description |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `POST` | `/api/notify/ack/{token}` | Acknowledge an incident from a push notification's Acknowledge button. No auth: the single-use token in the path is the credential. Must stay publicly reachable |
|
| `POST` | `/api/notify/ack/{token}` | Acknowledge an incident from a push notification's Acknowledge button. No auth: the token in the path is the credential — one incident, one action, 24 hours, idempotent. Must stay publicly reachable |
|
||||||
|
|
||||||
### Incidents
|
### Incidents
|
||||||
|
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ notify:
|
|||||||
#
|
#
|
||||||
# The Acknowledge button is a POST to /api/notify/ack/{token} from the
|
# The Acknowledge button is a POST to /api/notify/ack/{token} from the
|
||||||
# responder's phone, so that path has to stay publicly reachable — it is
|
# responder's phone, so that path has to stay publicly reachable — it is
|
||||||
# authorised by the single-use token in the URL, not by network placement.
|
# authorised by the scoped token in the URL, not by network placement.
|
||||||
publicUrl: ""
|
publicUrl: ""
|
||||||
# Optional bearer token for an access-controlled ntfy, read from an existing
|
# Optional bearer token for an access-controlled ntfy, read from an existing
|
||||||
# Secret. Leave name empty for an open ntfy.
|
# Secret. Leave name empty for an open ntfy.
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ func NewRouter(db *sql.DB, notify NotifyConfig) http.Handler {
|
|||||||
|
|
||||||
// Unauthenticated: bootstrap, the Alertmanager webhook receiver, and the
|
// Unauthenticated: bootstrap, the Alertmanager webhook receiver, and the
|
||||||
// Acknowledge button in a push notification. The last one is authorised by
|
// Acknowledge button in a push notification. The last one is authorised by
|
||||||
// the single-use token in its path rather than an API key, and has to stay
|
// the scoped token in its path rather than an API key, and has to stay
|
||||||
// reachable from outside the cluster for the button to work.
|
// reachable from outside the cluster for the button to work.
|
||||||
r.Post("/api/bootstrap", handleBootstrap(db))
|
r.Post("/api/bootstrap", handleBootstrap(db))
|
||||||
r.Post("/api/alertmanager/webhook", handleAlertmanagerWebhook(db, notify))
|
r.Post("/api/alertmanager/webhook", handleAlertmanagerWebhook(db, notify))
|
||||||
|
|||||||
Reference in New Issue
Block a user