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

MethodPathAuthResponse
GET/healthnone200 ok — confirms the Worker script is live
GET/tokenAuthorization: Bearer <google_access_token>200 {"token":"<pinbox24_jwt>","expiresAt":"<ISO8601>"}
GET/token-statussame as /token200 {"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:

StatusBodyCause
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
404Not FoundUnknown 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 configurationVerdict
Attached credential, no manual Authorization rowOK
Attached credential, manual row present but Send Headers offOK (delete the leftover row)
Hardcoded Bearer <literal>ERROR — rotation cannot reach it
Hardcoded literal and an attached credentialERROR — the manual header wins at runtime
{{ expression }}, no credentialWARN — 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):

  1. PUT /api/v1/workflows/vuZ1bgFHeiLr6JXp with a modified nodes[].parameters
  2. Response: 200 OK, and updatedAt is bumped
  3. GET /api/v1/workflows/vuZ1bgFHeiLr6JXpthe 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 200 from PUT /workflows/{id} as success. Verify every change with a read-back GET — 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 parameters writes over REST. Its credential is a different object from the Cloud one — attaching Cloud’s g7S3Dsl2AQmTn5Yh to 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 main touching infra-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 deploy

After first deploy, set the secrets (see below).

Rollback:

wrangler rollback        # restores the previous CF Worker version instantly

KV namespace

The Worker caches the Pinbox24 JWT in KV binding P24_AUTH_KV (cache key pinbox24_jwt).

FieldValue
BindingP24_AUTH_KV
TTL written to KV7 h (KV_TTL_SECONDS) — Pinbox24 JWTs last ~8 h
Proactive refresh window30 min before TTL elapses (REFRESH_BEFORE_EXPIRY_SECONDS)
Namespace idset 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.toml

Inspect / clear the cached JWT (forces a fresh Pinbox24 auth on the next call):

wrangler kv key delete --binding P24_AUTH_KV pinbox24_jwt

Secrets

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 secretSource key in SOPSSOPS fileNotes
P24_LOGINP24_LOGINsecrets/pinbox24-w4-auth.env.sopsPinbox24 login email
P24_MD5P24_MD5secrets/pinbox24-w4-auth.env.sopsMD5 hash of the Pinbox24 password
P24_ALLOWED_EMAILSP24_ALLOWED_EMAILSsecrets/pinbox24-w4-auth.env.sopsComma-separated Google emails allowed to call /token
GOOGLE_CLIENT_IDGOOGLE_CLIENT_IDsecrets/pinbox24-w4-auth.env.sopsOptional — enables aud validation; skipped if absent
P24_AUTOMATION_KEYP24_AUTOMATION_KEYsecrets/pinbox24-w4-auth.env.sopsOptional — 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_MD5 in this file still hold the interim radieu@gmail.com credentials from 3907, not yet cut over to a dedicated W4 service account (et-n8n@p24-infra.zintegrowana.online). Account creation is blocked — see docs/secrets-rotation-log.md and 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 set

Rotation

Pinbox24 password change

This is the whole point of the Worker — zero n8n changes required:

  1. Compute the new MD5 hash of the Pinbox24 password.
  2. Set it on the Worker: wrangler secret put P24_MD5 (in infra-src/p24-auth-worker/).
  3. Update P24_MD5 in secrets/n8n-bms4.env.sops — follow the Windows SOPS procedure in docs/playbooks/sops-windows-crlf.md.
  4. Clear the cached JWT so the next call re-auths: wrangler kv key delete --binding P24_AUTH_KV pinbox24_jwt.
  5. 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)

ScriptScheduleWhat it checksAlert
scripts/check-p24-auth-worker.shevery 15 minWorker alive + real Pinbox24 JWT deliveredDiscord RED ERROR + GH issue
scripts/check-p24-workflows-connection.shevery 6 hn8n 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>&1

check-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 checked

Run 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):

  1. 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 value
    • WARN — header present but looks like an unresolved expression or workflow is inactive
    • ERROR — no Authorization header, or empty, or literal "Bearer " with no token

    This check alone drives the existing Discord/GH-issue alert path (unchanged, #4177).

  2. HTTP retry/version audit (report-only, does not alert) — across ALL active httpRequest nodes on both instances, flags missing_retryOnFail, waitBetweenTries < 30000ms, and typeVersion < 4.2, with priority: high for any node calling api.w4.pinbox24.com or downloadPdf. See docs/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: ok

Token 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

MethodPathHTTP statusResult
POST/api/auth/refresh404Not Found
GET/api/auth/refresh404Not Found
POST/api/auth/extend404Not Found
POST/api/auth/token/refresh404Not 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_ID is set, the token’s aud must match it; a mismatch returns 401. Either bind the matching client id or clear GOOGLE_CLIENT_ID to 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:

  1. Create the Google Cloud OAuth2 app (client_id + client_secret) and the n8n Google OAuth2 credential (our own Google Cloud app — auto-refreshing).
  2. Confirm the 4 CF secrets are bound (the deploy workflow does this from SOPS).
  3. 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 /token and authenticates via the p24-auth-automation-key credential, sendHeaders disabled, hardcoded rows deleted. #4177’s original inventory also missed two nodes in the same ecotrans - docs 4 fibu 2025 workflow — p24-auth1 and p24-auth2 — which had sendHeaders: true with 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. In et-p24-cars the 5 consumer nodes that call api.w4.pinbox24.com directly — get_cars_all, get_1_car, upd_car, upd_car1, upd_car2 — were never repointed to consume the live p24-auth node’s token and still held a static Pinbox24 JWT literal (dead, expired 2025-10-21). Fixed 2026-08-01: each node’s Authorization header now reads ={{ $('get token').item.json.value }}, the same field-name pattern (value, sourced from the p24-authstore-tokenget token data-table chain) already used by the workflow’s other correctly-wired consumer node (get_cars_p24_et). Verified via read-back GET — no Authorization header in this workflow holds a raw JWT literal. If auditing this pattern again, check every node calling api.w4.pinbox24.com (or p24-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):

MethodPathAuthResponse
GET/healthnone200 ok
POST/kv/setX-Bridge-Key204 on success
GET/kv/get?key=…X-Bridge-Key200 {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-bridge

Shared-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 KEY

  • infra-src/p24-auth-worker/ — source code
  • infra-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 binding
  • secrets/n8n-bms4.env.sops — source for the 4 Worker secrets
  • secrets/pinbox24-w4-auth.env.sops — source for REDIS_BRIDGE_KEY (#3773)
  • docs/meta-dispatcher-operations.md — sibling CF Worker (same deploy/secret-binding pattern)