Skip to content

A presigned URL returns 403

2 min read · main part only; folded sections are optional

After this you can answer: What should you check when an S3 presigned URL returns 403?

Troubleshooting scenario: a ticket image or poster upload returns S3 403 Forbidden with “Request has expired”, ExpiredToken, SignatureDoesNotMatch or AccessDenied. Read the S3 error code before treating every 403 as the same problem.

S3 received the request, so Reach succeeded. Under Reach / Allowed / Password, expiry or signature failure is authentication, not a database password. AccessDenied points toward Allowed: check the signing API task role, object prefix and conditions. A missing allow differs from an explicit deny. Generating a signature locally proves neither permission nor object existence.

flowchart TB
A["S3 403"] --> B{"Error code or message?"}
B -->|"expired"| C["Get fresh link with valid task credentials"]
B -->|"signature mismatch"| D["Compare method, path, query and headers"]
B -->|"access denied"| E["Check signer grants and explicit denies"]
B -->|"browser hides response"| F["Check preflight and CORS separately"]

Ranked for MiniTickets’ short-lived links; the actual error overrides the order.

  1. The 120-second link expired, or its temporary signing credentials expired sooner.
  2. A PUT changed the signed content type, byte length or request URL; a GET link was used for a different method. Poster PUT also needs the returned encryption header.
  3. The API role lost permission, the key is outside an allowed prefix, or a policy denies access.

Privately inspect browser Network: S3 error, method and headers; never share the signed URL. Compare the API task role’s S3 grants with S3 → bucket → Permissions. A rejected OPTIONS preflight belongs to the CORS case.

Request a fresh link through the authorized API. Preserve the exact signed request and returned headers. Repair narrowly scoped signer permissions when needed; retain the bucket’s TLS-only deny. Do not store links as durable data: store object keys.

Keep the 120-second lifetime and private/no-store responses. Test real SDK signing, upload headers and expired-link recovery without logging bearer links.

apps/api/src/queries/assets.ts → ASSET_URL_SECONDS; apps/api/src/queries/posters.ts → signPosterPut; Inspect private assets →.

Advanced: A bearer link is a bounded permission

Whoever has a presigned URL can use it until it expires, subject to the signing principal’s permissions. The URL cannot outlive temporary credentials used to sign it. AWS documents expiry, permission and signature troubleshooting. Poster uploads sign the file’s content type and byte length; the browser supplies Content-Length for the File body. JavaScript must not set that forbidden header itself. This bucket uses SSE-S3, not a customer-managed KMS key; investigate KMS only if that design has actually changed. AccessDenied can also conceal a nonexistent object when the caller lacks bucket-list permission; privately confirm the canonical key before broadening grants. apps/web/src/features/events/hooks/use-upload-poster.ts → useUploadPoster.

Read this book online: https://book.theversehub.in/