p24-auth — Operations
Purpose
Cloudflare Worker (p24-auth) that acts as a secure auth proxy between n8n and the Pinbox24 API.
It removes the hardcoded Pinbox24 MD5 password hash from 6+ n8n workflow nodes (cloud + bms-4) and keeps it out of the n8n execution history (PostgreSQL). Instead of each node holding the MD5, n8n presents a Google OAuth2 Bearer token and the Worker performs the Pinbox24 auth on its behalf:
n8n → GET /token (Authorization: Bearer <google_access_token>)
→ CF Worker validates the Google token (tokeninfo)
→ checks the caller email against P24_ALLOWED_EMAILS
→ returns a cached Pinbox24 JWT from KV (if fresh)
→ otherwise POSTs Pinbox24 /api/auth with P24_LOGIN + P24_MD5 (CF secrets), caches 7 h, returns JWT
- Route:
https://p24-auth.radieu.workers.dev - Source:
infra-src/p24-auth-worker/ - Implementing issue: #1999
Endpoints
| Method | Path | Auth | Response |
|---|---|---|---|
GET | /health | none | 200 ok — confirms the Worker script is live |
GET | /token | Authorization: Bearer <google_access_token> | 200 {"token":"<pinbox24_jwt>","expiresAt":"<ISO8601>"} |
GET | /token-status | same as /token | 200 {"expiresAt":"<ISO8601>","cachedAt":"<ISO8601>"} / 404 no_status_cached |
expiresAt is the ISO8601 form of the JWT’s own exp claim (real Pinbox24
validity). It is null when the JWT payload is unparseable — existing
consumers reading only .token continue to work.
/token-status reads p24auth:jwt_status from the bms-4 redis-bridge (see
Redis bridge) and returns the same expiresAt field the mint
path publishes. It exists so n8n can perform an independent freshness check
without routing through the Worker’s KV cache. It returns 404 no_status_cached
when the bridge is not configured or hasn’t seen a mint yet.
n8n consumption pattern
For long-running workflows (~4 h), fetch /token once at workflow start, stash
{ token, expiresAt } in n8n’s workflow static data, and let every downstream
node reference the stashed values directly. Given the p24-back-ts
authGuard.middleware.ts audit (issue #3832) — verifyAuthToken does plain
jwt.verify with no per-user session pointer, no jti/tokenVersion claim —
a Pinbox24 JWT is not revoked when the same account logs in again, so a
background cacheJwt() refresh never invalidates the token a running workflow
is holding. No per-node retry logic needed.
/token error responses:
| Status | Body | Cause |
|---|---|---|
401 unauthorized | {"error":"unauthorized"} | Missing/empty Bearer, or invalid/expired Google token |
403 forbidden | {"error":"forbidden"} | Google email not in P24_ALLOWED_EMAILS |
502 upstream_auth_failed | {"error":"upstream_auth_failed"} | Pinbox24 /api/auth returned no result.token |
404 | Not Found | Unknown path/method |
Upstream call: POST https://api.w4.pinbox24.com/api/auth with
{login, password: <md5>, appType: "ng5"}; the JWT is read from result.token.
Authorization must come from the credential store — not a node parameter
Rule: every n8n node calling p24-auth.radieu.workers.dev authenticates via an attached
n8n credential. On n8n Cloud that credential is p24-auth-automation-key
(g7S3Dsl2AQmTn5Yh, type httpHeaderAuth). Never type a Bearer … value into a node’s
“Send Headers” table.
A header literal is not merely untidy — it is invisible to rotation. P24_AUTOMATION_KEY can be
rotated everywhere it is managed and the node keeps sending the old value until someone opens
that node in the UI. Issue #4177 tracks the six nodes still in this state; #3800 is the same class
with a login/password in a tracked workflow JSON.
scripts/check-p24-workflows-connection.sh enforces this every 6 h and reports ERROR on a
hardcoded literal. Classification logic: scripts/lib/n8n_p24_auth_classify.py.
| Node configuration | Verdict |
|---|---|
| Attached credential, no manual Authorization row | OK |
Attached credential, manual row present but Send Headers off | OK (delete the leftover row) |
Hardcoded Bearer <literal> | ERROR — rotation cannot reach it |
| Hardcoded literal and an attached credential | ERROR — the manual header wins at runtime |
{{ expression }}, no credential | WARN — better than a literal, still not the credential store |
Empty / literal Bearer / unclosed {{ | ERROR |
⚠️ n8n Cloud parameters writes via the public REST API — status unclear, verify every time
Do not assume this either works or fails — verify with a read-back GET on every write,
every time. The behavior below was observed to flip between two dates on the same
workflow, so neither claim (“always silently drops” / “always persists”) can be trusted
without re-checking.
Original reproduction (2026-07-15, workflow vuZ1bgFHeiLr6JXp, n8n Cloud):
PUT /api/v1/workflows/vuZ1bgFHeiLr6JXpwith a modifiednodes[].parameters- Response:
200 OK, andupdatedAtis bumped GET /api/v1/workflows/vuZ1bgFHeiLr6JXp— the parameter value had reverted
Update (2026-07-30): the identical operation — PUT with modified nodes[].parameters
(disabling sendHeaders, deleting a header row) — against the same workflow ID
vuZ1bgFHeiLr6JXp (nodes p24-auth1/p24-auth2) succeeded and persisted, confirmed via
immediate GET read-back. The same pattern also succeeded that day against three other
Cloud workflows (ecotrans-hr-workflow, ai-documents-inbox-folders processing,
synchronize-kasa-with-googleSheet) and against a connections-object write (not just
parameters) on ecotrans-hr-workflow — see docs/ecotrans-hr-workflow-operations.md.
Root cause of the discrepancy is unknown (n8n platform-side fix between the two dates? Payload
shape difference? Not determined). Treat every write as unverified until read back, in
either direction:
- Never treat a
200fromPUT /workflows/{id}as success. Verify every change with a read-backGET— this holds regardless of which behavior is currently live. A workflow that reports “updated” and did not change is the same silent-pass failure mode as a monitoring probe that reads healthy while dead. - Given the 2026-07-30 result, a Cloud node may now be migratable via the API without the UI — attempt it, but always verify. Fall back to the UI (human, or Playwright once #4069 provisions it on the bms-4 secret-manager worker) only if the read-back shows no change.
- The bms-4 self-hosted instance reliably accepts
parameterswrites over REST. Its credential is a different object from the Cloud one — attaching Cloud’sg7S3Dsl2AQmTn5Yhto a bms-4 node silently breaks it.
Order of operations when migrating a node: attach the credential → verify a real execution →
only then disable “Send Headers”. Reversing this leaves the node briefly unauthenticated.
Snapshot the workflow JSON into n8n-backups/ before touching it.
Deployment
Automated (CI/CD — preferred)
.github/workflows/deploy-p24-auth-worker.yml deploys the Worker and binds all secrets
automatically. It runs on:
- push to
maintouchinginfra-src/p24-auth-worker/**(or the workflow itself) - manual:
gh workflow run deploy-p24-auth-worker.yml --repo radieu/p24-infra
The workflow installs deps, runs npm test, deploys via wrangler deploy, then binds the secrets —
each value decrypted from secrets/n8n-bms4.env.sops with the CI age key (AGE_KEY_GHA) and piped
to wrangler secret put (never echoed). Auth to Cloudflare uses the GH secrets
CLOUDFLARE_GLOBAL_API_KEY + CLOUDFLARE_EMAIL and account ID acc5b2c956dd488265003364e2c7f84a.
A smoke test confirms GET /health returns 200 after deploy. On failure it sends a Discord alert
(P24_DISCORD_INFRA_SCRIPTS_ERRORS_WEBHOOK_URL) and opens a GH issue with label bug.
Manual (fallback)
cd infra-src/p24-auth-worker
npm install
wrangler deployAfter first deploy, set the secrets (see below).
Rollback:
wrangler rollback # restores the previous CF Worker version instantlyKV namespace
The Worker caches the Pinbox24 JWT in KV binding P24_AUTH_KV (cache key pinbox24_jwt).
| Field | Value |
|---|---|
| Binding | P24_AUTH_KV |
| TTL written to KV | 7 h (KV_TTL_SECONDS) — Pinbox24 JWTs last ~8 h |
| Proactive refresh window | 30 min before TTL elapses (REFRESH_BEFORE_EXPIRY_SECONDS) |
| Namespace id | set in wrangler.toml ([[kv_namespaces]]) |
Create (one-time, only if the namespace ids in wrangler.toml are ever lost):
wrangler kv namespace create P24_AUTH_KV
wrangler kv namespace create P24_AUTH_KV --preview
# then copy the returned ids into wrangler.tomlInspect / clear the cached JWT (forces a fresh Pinbox24 auth on the next call):
wrangler kv key delete --binding P24_AUTH_KV pinbox24_jwtSecrets
All Worker secrets are set via wrangler secret put from infra-src/p24-auth-worker/. The deploy
workflow (deploy-p24-auth-worker.yml) binds P24_LOGIN, P24_MD5, P24_ALLOWED_EMAILS,
GOOGLE_CLIENT_ID and P24_AUTOMATION_KEY from secrets/pinbox24-w4-auth.env.sops — the
single canonical SOPS file for this credential family (migrated out of n8n-bms4.env.sops by
PR #3896 / confirmed the sole remaining copy by #3773 Phase 2, 2026-07-29). Never commit
plaintext values.
| Worker secret | Source key in SOPS | SOPS file | Notes |
|---|---|---|---|
P24_LOGIN | P24_LOGIN | secrets/pinbox24-w4-auth.env.sops | Pinbox24 login email |
P24_MD5 | P24_MD5 | secrets/pinbox24-w4-auth.env.sops | MD5 hash of the Pinbox24 password |
P24_ALLOWED_EMAILS | P24_ALLOWED_EMAILS | secrets/pinbox24-w4-auth.env.sops | Comma-separated Google emails allowed to call /token |
GOOGLE_CLIENT_ID | GOOGLE_CLIENT_ID | secrets/pinbox24-w4-auth.env.sops | Optional — enables aud validation; skipped if absent |
P24_AUTOMATION_KEY | P24_AUTOMATION_KEY | secrets/pinbox24-w4-auth.env.sops | Optional — pre-shared secret for n8n automation |
REDIS_BRIDGE_URL | (literal) | — | Optional — https://redis-bridge.bms-4.infra.zintegrowana.online (enables bridge write-through and /token-status). Not currently wired into deploy-p24-auth-worker.yml — set manually via wrangler secret put if enabling the bridge. |
REDIS_BRIDGE_KEY | (not yet in any SOPS file) | — | Optional — X-Bridge-Key sent to the bridge (required together with REDIS_BRIDGE_URL). Not currently wired into deploy-p24-auth-worker.yml — set manually. |
#3773 status (2026-07-29):
P24_LOGIN/P24_MD5in this file still hold the interimradieu@gmail.comcredentials from 3907, not yet cut over to a dedicated W4 service account (et-n8n@p24-infra.zintegrowana.online). Account creation is blocked — seedocs/secrets-rotation-log.mdand issue #3773 for the current blocker before attempting a cutover.
wrangler secret put P24_LOGIN
wrangler secret put P24_MD5
wrangler secret put P24_ALLOWED_EMAILS
wrangler secret put GOOGLE_CLIENT_ID # optional
wrangler secret put P24_AUTOMATION_KEY # optional
wrangler secret put REDIS_BRIDGE_URL # optional — enables /token-status
wrangler secret put REDIS_BRIDGE_KEY # optional — required if REDIS_BRIDGE_URL is setRotation
Pinbox24 password change
This is the whole point of the Worker — zero n8n changes required:
- Compute the new MD5 hash of the Pinbox24 password.
- Set it on the Worker:
wrangler secret put P24_MD5(ininfra-src/p24-auth-worker/). - Update
P24_MD5insecrets/n8n-bms4.env.sops— follow the Windows SOPS procedure indocs/playbooks/sops-windows-crlf.md. - Clear the cached JWT so the next call re-auths:
wrangler kv key delete --binding P24_AUTH_KV pinbox24_jwt. - Append a rotation entry to
docs/secrets-rotation-log.md.
Google OAuth credential
Auto-refreshing via the n8n Google OAuth2 credential — never needs manual rotation. Rotate the
Google OAuth client only on a security event (revoke + recreate the Google Cloud app, then update
GOOGLE_CLIENT_ID in SOPS + wrangler secret put).
Allowlist change
Edit P24_ALLOWED_EMAILS (comma-separated) in secrets/n8n-bms4.env.sops, then
wrangler secret put P24_ALLOWED_EMAILS. No code change needed.
Monitoring
Automated checks (bms-4 cron)
| Script | Schedule | What it checks | Alert |
|---|---|---|---|
scripts/check-p24-auth-worker.sh | every 15 min | Worker alive + real Pinbox24 JWT delivered | Discord RED ERROR + GH issue |
scripts/check-p24-workflows-connection.sh | every 6 h | n8n workflow nodes authenticate via an attached credential, not a header literal (#4177) | Discord RED ERROR + GH issue |
Both scripts are edge-triggered: they alert once on clean->fail transition and send a recovery notification once on fail->clean. Duplicate alerts are suppressed.
Cron entries (bms-4):
Deployed to /etc/cron.d/p24-auth-monitor or root crontab. Env loaded from
/root/.p24-auth-monitor.env (contains BMS4_N8N_API_KEY, P24_AUTOMATION_KEY,
N8N_CLOUD_API_KEY, GH_TOKEN, P24_DISCORD_INFRA_SCRIPTS_ERRORS_WEBHOOK_URL).
set -a is required because source/. does not export vars to subprocesses.
*/15 * * * * set -a && . /root/.p24-auth-monitor.env && set +a && /opt/p24-infra/scripts/check-p24-auth-worker.sh >> /var/log/check-p24-auth-worker.log 2>&1
0 */6 * * * set -a && . /root/.p24-auth-monitor.env && set +a && /opt/p24-infra/scripts/check-p24-workflows-connection.sh >> /var/log/check-p24-workflows-connection.log 2>&1check-p24-auth-worker.sh — worker health + token delivery
Check 1 — worker alive:
GET https://p24-auth.radieu.workers.dev/health
# expect: HTTP 200, body "ok"Check 2 — real JWT delivery:
GET https://p24-auth.radieu.workers.dev/token
Authorization: Bearer $P24_AUTOMATION_KEY
# expect: HTTP 200, JSON .token field with length > 100 chars
# the JWT value is NEVER echoed -- only its length is checkedRun manually (bms-4):
source /opt/p24-infra/bms-4/.env
/opt/p24-infra/scripts/check-p24-auth-worker.sh
echo "Exit: $?"Required env vars: P24_AUTOMATION_KEY, P24_DISCORD_INFRA_SCRIPTS_ERRORS_WEBHOOK_URL, GH_TOKEN
check-p24-workflows-connection.sh — n8n workflow auth checker + HTTP retry/version audit
Fetches all active workflows from both n8n instances (bms-4 + n8n cloud) in a single pass and
runs two checks (extended for issue #2641 Task 2+3 — see docs/n8n-workflow-inventory.md for
the full audit table and target retry config):
-
Auth-pattern check — finds HTTP Request nodes that call
p24-auth.radieu.workers.dev. For each node it checks:OK— Authorization header is set with a non-empty, non-placeholder valueWARN— header present but looks like an unresolved expression or workflow is inactiveERROR— no Authorization header, or empty, or literal"Bearer "with no token
This check alone drives the existing Discord/GH-issue alert path (unchanged, #4177).
-
HTTP retry/version audit (report-only, does not alert) — across ALL active
httpRequestnodes on both instances, flagsmissing_retryOnFail,waitBetweenTries < 30000ms, andtypeVersion < 4.2, withpriority: highfor any node callingapi.w4.pinbox24.comordownloadPdf. Seedocs/n8n-workflow-inventory.md§HTTP node retry/auth audit for the full findings table and the known audit-vs-target-config tension for CF-Worker auth nodes.
Output: one JSON record per node/check (tagged "check": "auth" or "check": "http_audit")
plus a human-readable summary for each.
Known workflows monitored:
- n8n cloud:
ai-documents-inbox-folders processing,ecotrans-hr-workflow,synchronize-kasa-with-googleSheet,ecotrans - docs 4 fibu 2025 - bms-4:
et-p24-cars,et-p24-emails-clasifier
Run manually (bms-4):
source /opt/p24-infra/bms-4/.env
/opt/p24-infra/scripts/check-p24-workflows-connection.sh
echo "Exit: $?"Required env vars: BMS4_N8N_API_KEY, BMS4_N8N_HOST, N8N_CLOUD_API_KEY,
N8N_CLOUD_BASE_URL, P24_DISCORD_INFRA_SCRIPTS_ERRORS_WEBHOOK_URL, GH_TOKEN
Live log streaming
wrangler tail # stream live Worker logs (auth decisions, cache hits, upstream errors)Key log lines (console.* in src/index.ts): Authorized email, Serving cached JWT,
JWT cached in KV, 401 unauthorized, 403 forbidden, 502 upstream_auth_failed.
Manual one-off health check:
curl -s https://p24-auth.radieu.workers.dev/health # expect: okToken refresh research
Research date: 2026-07-02
Tested against: https://api.w4.pinbox24.com
Method: obtained a live Pinbox24 JWT via the CF Worker /token endpoint (using
P24_AUTOMATION_KEY), then probed candidate refresh endpoints with the JWT in the Authorization
header. JWT value was never logged; only HTTP status codes are recorded.
Endpoints tested
| Method | Path | HTTP status | Result |
|---|---|---|---|
POST | /api/auth/refresh | 404 | Not Found |
GET | /api/auth/refresh | 404 | Not Found |
POST | /api/auth/extend | 404 | Not Found |
POST | /api/auth/token/refresh | 404 | Not Found |
Conclusion
Pinbox24 does not expose a token-refresh endpoint. There is no way to extend a live JWT
without re-authenticating with full credentials (P24_LOGIN + P24_MD5).
Current design is sufficient:
- Pinbox24 JWTs live ~8 h
- CF Worker KV TTL is 7 h with a proactive refresh 30 min before expiry
- Longest known workflow (
ai-documents-inbox-folders processing) runs 3-4 h - A fresh JWT is obtained automatically by the Worker on each KV miss — no workflow changes needed
Action: No code changes required. Full re-auth via POST /api/auth on cache expiry is the
correct and only available strategy.
n8n usage
Replace each p24-auth / p24-auth1 / p24-auth2 node with a single HTTP Request node:
GET https://p24-auth.radieu.workers.dev/token
Authorization: Bearer {{ $credentials.googleOAuth2.accessToken }}
Read token from the JSON response and use it as the Pinbox24 Authorization for downstream calls.
Troubleshooting
/token returns 401 for a valid-looking caller
- Bearer token is a Google access token (not an id_token) and is still valid — check expiry.
- If
GOOGLE_CLIENT_IDis set, the token’saudmust match it; a mismatch returns 401. Either bind the matching client id or clearGOOGLE_CLIENT_IDto disable audience validation.
/token returns 403
The caller’s Google email is not in P24_ALLOWED_EMAILS. Add it (comma-separated) and re-bind.
/token returns 502 upstream_auth_failed
Pinbox24 /api/auth rejected the credentials or changed its response shape. Verify P24_LOGIN /
P24_MD5 are correct (the MD5 must match the current Pinbox24 password) and that the JWT still
arrives in result.token. Use wrangler tail to see the logged upstream status/body.
Stale JWT after a Pinbox24 password change
The KV cache can serve a JWT for up to 7 h. After rotating P24_MD5, delete the cache key (see
Rotation step 4) to force an immediate re-auth.
Remaining human action (cutover — separate issue)
The Worker is deployed and self-contained, but the live n8n cutover is a human task:
- Create the Google Cloud OAuth2 app (client_id + client_secret) and the n8n Google OAuth2 credential (our own Google Cloud app — auto-refreshing).
- Confirm the 4 CF secrets are bound (the deploy workflow does this from SOPS).
Migrate the 6 n8n nodes…Done as of 2026-07-30 for the token-fetch node in each of the 6 originally-scoped workflows (cloud:ai-documents-inbox-folders processing,ecotrans-hr-workflow,synchronize-kasa-with-googleSheet,ecotrans - docs 4 fibu 2025; bms-4:et-p24-cars,et-p24-emails-clasifier) — each calls/tokenand authenticates via thep24-auth-automation-keycredential,sendHeadersdisabled, hardcoded rows deleted. #4177’s original inventory also missed two nodes in the sameecotrans - docs 4 fibu 2025workflow —p24-auth1andp24-auth2— which hadsendHeaders: truewith no credential attached at all (a live, unpatched literal-header exposure, not just a shadowed one). Found and fixed the same day. Correction (2026-08-01, #4699): “done” above only ever meant the token-fetch node itself. Inet-p24-carsthe 5 consumer nodes that callapi.w4.pinbox24.comdirectly —get_cars_all,get_1_car,upd_car,upd_car1,upd_car2— were never repointed to consume the livep24-authnode’s token and still held a static Pinbox24 JWT literal (dead, expired 2025-10-21). Fixed 2026-08-01: each node’sAuthorizationheader now reads={{ $('get token').item.json.value }}, the same field-name pattern (value, sourced from thep24-auth→store-token→get tokendata-table chain) already used by the workflow’s other correctly-wired consumer node (get_cars_p24_et). Verified via read-backGET— noAuthorizationheader in this workflow holds a raw JWT literal. If auditing this pattern again, check every node callingapi.w4.pinbox24.com(orp24-auth.radieu.workers.dev) directly in a workflow, not just the token-fetch node itself — a single workflow can have several downstream consumers that need repointing independently.
Redis bridge
Cloudflare Workers cannot open raw TCP sockets, so the Worker cannot talk to
the bms-4 Redis directly. infra-src/redis-bridge/ (Node.js + Hono) exposes
the two operations the Worker needs, gated by a pre-shared key and a hardcoded
key-prefix allowlist (p24auth:*). Introduced by issue #3832.
Endpoints (all under https://redis-bridge.bms-4.infra.zintegrowana.online):
| Method | Path | Auth | Response |
|---|---|---|---|
GET | /health | none | 200 ok |
POST | /kv/set | X-Bridge-Key | 204 on success |
GET | /kv/get?key=… | X-Bridge-Key | 200 {key, value} / 404 |
Keys must start with p24auth: — anything else returns 403 key_prefix_not_allowed.
This is intentional: the bridge is not a general Redis passthrough.
Deployment: built and run as service redis-bridge in bms-4/docker-compose.yml,
exposed via Traefik at redis-bridge.bms-4.infra.zintegrowana.online. Env vars:
REDIS_HOST=redis, REDIS_PORT=6379, REDIS_PASSWORD (from secrets/n8n-bms4.env.sops),
REDIS_BRIDGE_KEY (from secrets/pinbox24-w4-auth.env.sops, added by #3773).
# On bms-4, after a code change:
cd /opt/p24-infra/bms-4
docker compose build redis-bridge
docker compose up -d redis-bridge
docker compose logs -f redis-bridgeShared-Redis blind spot: the bridge writes to the same Redis that also
backs n8n’s Bull queue. A FLUSHALL during n8n maintenance also wipes the
p24auth:* keys. This is low-severity — the Worker self-heals on the next
/token call, and the KV cache stays authoritative. Callers relying on
/token-status for freshness should treat a 404 no_status_cached response
as “assume the mint path is fresh, don’t retry”.
Manual smoke test (bms-4):
# Extract keys silently — never echo values
KEY=$(sops -d --input-type dotenv --output-type dotenv secrets/pinbox24-w4-auth.env.sops \
| grep '^REDIS_BRIDGE_KEY=' | cut -d= -f2-)
curl -s https://redis-bridge.bms-4.infra.zintegrowana.online/health
# expect: ok
curl -s -X POST https://redis-bridge.bms-4.infra.zintegrowana.online/kv/set \
-H "X-Bridge-Key: $KEY" -H "Content-Type: application/json" \
-d '{"key":"p24auth:smoke","value":"hello","ttlSeconds":60}' -o /dev/null -w "%{http_code}\n"
# expect: 204
curl -s https://redis-bridge.bms-4.infra.zintegrowana.online/kv/get?key=p24auth:smoke \
-H "X-Bridge-Key: $KEY"
# expect: {"key":"p24auth:smoke","value":"hello"}
unset KEYRelated
infra-src/p24-auth-worker/— source codeinfra-src/p24-auth-worker/README.md— quick reference (endpoints, secrets, deploy)infra-src/redis-bridge/— HTTP↔Redis bridge on bms-4 (#3832).github/workflows/deploy-p24-auth-worker.yml— CI/CD deploy + secret bindingsecrets/n8n-bms4.env.sops— source for the 4 Worker secretssecrets/pinbox24-w4-auth.env.sops— source forREDIS_BRIDGE_KEY(#3773)docs/meta-dispatcher-operations.md— sibling CF Worker (same deploy/secret-binding pattern)