Skip to documentation
Docs Build a Nook bot
Browse documentation
Apps & Bots · Security

Secure callbacks, tokens, and scopes

Verify raw requests, prevent replay and duplicate side effects, rotate secrets safely, and preserve workspace consent boundaries.

Verify before parsing

Every Nook callback carries:

X-Nook-Timestamp: <unix-seconds>
X-Nook-Signature: v1=<hex-hmac-sha256>

The signed input is exactly:

v1:<timestamp>:<raw-request-body>

Capture the raw bytes/string before any JSON parser, whitespace normalization, body reconstruction, or schema coercion. Reject a missing header, timestamp more than ±300 seconds from your clock, and every signature mismatch.

import { createHmac, timingSafeEqual } from 'node:crypto';

function expectedSignature(secret: string, timestamp: string, rawBody: string) {
  const hex = createHmac('sha256', secret)
    .update(`v1:${timestamp}:${rawBody}`, 'utf8')
    .digest('hex');
  return `v1=${hex}`;
}

function constantTimeEqual(left: string, right: string) {
  const a = Buffer.from(left, 'utf8');
  const b = Buffer.from(right, 'utf8');
  return a.length === b.length && timingSafeEqual(a, b);
}

X-Nook-Signature may contain two comma-separated v1= entries during the 24-hour signing-secret rotation overlap. Accept only when any supplied v1 signature matches any currently configured current/previous secret. Never fall back to ordinary string equality.

Enforce body and time boundaries

  • Set a bounded request-body limit before buffering (the public starter uses 1 MiB).
  • Keep host time synchronized; a broken clock must fail requests rather than widen the replay window.
  • Accept only POST on the two configured callback paths.
  • Return 404 for unknown paths rather than building a general-purpose public API accidentally.
  • Validate the parsed body against the documented, versioned callback shape after signature verification; reject unknown or malformed required fields before business logic.
  • Log only safe type/identifier/error-code metadata, never raw bodies by default.

Distinguish retries from new work

Nook uses at-least-once background delivery. Duplicates are normal.

For lifecycle/events, persist and deduplicate the signed meta.idempotencyKey. Do not dedupe on X-Nook-Delivery-Id; that ID changes for every retry attempt.

app.installed is the one special bootstrap path:

  1. if a usable token already exists for the installation, ignore later install callbacks;
  2. if the same tokenExchange.attemptId was processed, skip that HTTP retry;
  3. otherwise exchange the newest code;
  4. do not suppress it merely because meta.idempotencyKey was seen—each delivery attempt carries a fresh code and attempt ID.

Persist dedupe state for the maximum retry/side-effect window your business action needs. An in-memory set is suitable only for a local prototype.

Token classes are not interchangeable

TokenAccepted atMust not be used for
Client secret/api/v1/apps/token-exchange request bodyBot API, callback verification
Signing secretLocal HMAC verification onlyAny Nook API request
Bot token/api/v1/app/* for one installationUser/admin APIs or another installation
Response tokenThe named interaction’s response/follow-up pathsBot API or another interaction
Incoming webhook URL tokenThat standalone webhook endpointApp/bot authentication

The bot token is shown once and stored by Nook only as a hash. A response token is a 15-minute JWT for one interaction and allows at most five follow-ups. Never persist it as a replacement bot token.

Store secrets professionally

  • Inject current and previous signing secrets, client secret, and bot tokens from a secret manager.
  • Encrypt durable bot-token storage and key it by installation ID.
  • Keep local fallback files mode 0600, outside source control and web roots.
  • Redact nookcs_, nooksig_, nookbot_, response JWTs, exchange codes, and webhook URLs from logs, tracing, errors, analytics, crash dumps, and support attachments.
  • Never put secrets in query strings, command arguments, image layers, frontend bundles, or GitHub Actions output.
  • Scope service/runtime access so interaction workers cannot enumerate unrelated installations.

Scope and channel policy

Authorization is the intersection of:

valid bot token
∩ active installation and published app
∩ token scopes
∩ current installation scopes
∩ current channel policy (for channel-bound calls)

Scope removal takes effect immediately. A later scope addition does not widen existing tokens; rotate the token after the administrator consents to an upgrade. Giving the bot user a workspace role does not expand API authority.

Channel-policy denial is deliberately non-enumerating: a hidden/disallowed channel behaves like an unknown channel. Do not probe identifiers or turn 404 into “request broader access.”

Interaction deadlines and response tokens

Return an immediate response within three seconds:

  • ack — no immediate content;
  • ephemeral — visible only to the invoking member in a channel;
  • message — a channel or consented DM message under the relevant scopes;
  • modal — immediate modal open when modals:open is granted.

For slow work, acknowledge first and run the job after the HTTP response. Use the callback’s response token for a delayed response/follow-up. A delayed modal is rejected. Revalidate current installation, scope, channel/DM, and consent policy when the delayed call executes.

dms:write never grants cold outreach. A member must interact with the installed app while:

  • dms:write is currently granted;
  • the member’s global App DMs preference is enabled;
  • the installation is active.

Consent is not backfilled from an older interaction. A user revocation remains sticky until the user explicitly re-enables the app. Every unknown/missing/not-member/no-consent/preference-off target returns the same 403 APP_DM_NOT_ALLOWED; treat it as “stop until a new permitted interaction,” not as an enumeration oracle.

Rotate without downtime

Signing secret

  1. Create the new secret in Developer Portal.
  2. Keep old secret configured as NOOK_PREVIOUS_SIGNING_SECRET.
  3. Deploy the new current secret before the 24-hour overlap ends.
  4. Verify callbacks containing two signatures.
  5. Remove the old secret after the overlap and a successful callback smoke.

Client secret

The previous client secret remains valid for a 24-hour overlap. Deploy the new value to every token exchange worker, then remove the old value from the secret manager after proof.

Bot token

An installation supports at most two active tokens. Create new → deploy and verify → revoke old. Revoking the installation kills every token immediately.

Required negative tests

  • missing signature/timestamp → 401, no body handler side effect;
  • valid signature over different raw bytes → 401;
  • timestamp outside ±300 seconds → 401;
  • duplicate event key → one business side effect;
  • same delivery ID with different idempotency key → separate logical event;
  • bot token on user/admin/external-messaging endpoints → reject;
  • response token on bot API → reject;
  • revoked/disabled/suspended installation → current runtime gate rejects;
  • missing scope and disallowed/private channel → non-enumerating denial;
  • stale token after scope addition → still lacks the new scope;
  • DM without current consent/preference → uniform APP_DM_NOT_ALLOWED;
  • secret rotation overlap → current or previous secret accepted, unrelated secret rejected.

Was this guide clear?

Keep commands tied to your Customer Portal values and never paste secrets into support requests.