Skip to content

Add Vercel OIDC auth and Presigned URLs#1056

Merged
elliotdauber merged 16 commits into
mainfrom
elliot/oidc-auth
May 18, 2026
Merged

Add Vercel OIDC auth and Presigned URLs#1056
elliotdauber merged 16 commits into
mainfrom
elliot/oidc-auth

Conversation

@elliotdauber
Copy link
Copy Markdown
Collaborator

@elliotdauber elliotdauber commented Apr 20, 2026

Adds OIDC auth for all methods in the SDK and adds support for Presigned Urls.

Adds a new uploadPresigned and handleUploadPresigned that used presigned urls for client uploads, which is needed as read-write tokens are removed.

Minor version bump.

@vercel
Copy link
Copy Markdown
Contributor

vercel Bot commented Apr 20, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
vercel-storage-next-integration-test-suite Ready Ready Preview May 18, 2026 8:35pm

Request Review

@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented Apr 20, 2026

🦋 Changeset detected

Latest commit: 9d6982a

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 2 packages
Name Type
@vercel/blob Minor
vercel-storage-integration-test-suite Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

Copy link
Copy Markdown
Collaborator Author

This stack of pull requests is managed by Graphite. Learn more about stacking.

falcoagustin added a commit to vercel/vercel that referenced this pull request Apr 30, 2026
vercel/storage#1056 makes the SDK's `options.token` read-write only
(parsed via underscore split for storeId). Passing the OIDC JWT
through it produces a malformed storeId. Instead pass only `storeId`
and rely on `getVercelOidcToken()` reading `VERCEL_OIDC_TOKEN` from
`process.env`; our resolver already hoists `.env.local` values there.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
BLOB_STORE_ID and the storeId option are accepted in either store_<id>
or <id> form (Vercel env pull writes the prefixed form), and may be
mixed-case. resolveBlobAuth was passing those through verbatim, so the
storeId in API headers and CDN host subdomains could be malformed —
e.g. blob.get against a private store with `store_WdsHBk1w9fDO4vPW`
built `https://store_WdsHBk1w9fDO4vPW.private.blob.vercel-storage.com/...`
and 404'd. The RW path was unaffected because parseStoreIdFromReadWriteToken
yields a bare lowercase id from the token's structure.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The first pass also lowercased — that breaks API requests, since the
Vercel Blob API is case-sensitive on the storeId (header and bearer
parsing). The CDN host accepts either case, so prefix-strip alone is
sufficient and works for both consumers. Verified end-to-end against
a private store: blob get and blob list both succeed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* presigned urls

* up

* normalize

* fix

* update to new signing string

* [wip] presigned url version of handleUpload (#1059)

* [wip] presigned url version of handleUpload

* up

* up

* up

* add presigned urls for mpu

* update options shape

* webhook signature

* update example

* up

* up

* up

* change to operation

* up

* up

* verify webhook signature

* up

* callbackUrl

* non-null

* presigned url opts

* new delegation token + url opts

* dont lowercase store id

* validUntil instead of ttlSeconds

* up

* up

* Add 'delete' to DelegationOperation for presigned DELETE URLs (#1061)

Mirrors the API-side change that adds `delete` to the issue_signed_token
allowed operations. `presignUrl` now signs canonical
`operation=delete\npathname=...` against the public blob object URL
(same host shape as `get`/`head`, just used with HTTP DELETE).

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>

* change upload to put

* delete

* up

* presigned url payload

* update app

* fix

* docs

* refactor presign url

* up

* up

* up

* add head

* up

* add params to url

* add keys first

* token payload

* add buildPresignedGetUrl

* up

* presignUrl returns url, not accepted in sdk methods

* up

* up

* feat(blob): presigned HEAD & DELETE URLs (#1064)

* feat(blob): presigned DELETE URLs

Wires the `'delete'` DelegationOperation (added in #1061, since rebased
out of elliot/presigned-urls) through `presignUrl`. A token issued with
`operations: ['delete']` can now mint a presigned `DELETE /?pathname=…`
against the control-plane API.

- `PresignDeleteUrlOptions` accepts `pathname`, optional `validUntil`,
  optional `ifMatch`. Upload-only fields are rejected at the type level.
- `presign()` gates on the delegation scope including `'delete'`.
- `buildPresignedDeleteUrl()` mirrors the PUT URL shape; the HTTP method
  is the discriminator (canonical signing string carries `operation=delete`).
- `buildPresignCanonicalQueryEntries()` for `delete` emits only
  `validUntil` (when below the delegation ceiling) and `ifMatch`.
- E2E test route + delete button on the presigned-upload demo page.

Based on `elliot/presigned-urls`, not `main`.

* feat(blob): presigned HEAD URLs (#1065)

`HEAD` mirrors `GET` against the blob object host
(`<storeId>.<access>.blob.vercel-storage.com/<pathname>`); the URL shape
is identical and the HTTP method is the discriminator. `operation=head`
goes into the canonical signing string so a GET-signed URL cannot be
replayed as a HEAD (and vice versa).

- `'head'` added to `DelegationOperation`.
- `PresignHeadUrlOptions` — same shape as `PresignGetUrlOptions`
  (`pathname`, optional `validUntil`).
- `presign()` gates on the delegation scope including `'head'`.
- `presignUrl()` reuses `buildPresignedGetUrl()` for `operation: 'head'`.
- `buildPresignCanonicalQueryEntries()` for `head` emits only
  `validUntil` (when below the delegation ceiling) — same as `get`.
- E2E test route + HEAD button on the presigned-upload demo page.

Stacked on `falcoagustin/presigned-delete-impl` (#1064).

* environment error

* up

* cleanup

---------

Co-authored-by: Agustin Falco <agusfalco_11@hotmail.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
@elliotdauber elliotdauber changed the title Add Vercel OIDC auth Add Vercel OIDC auth and Presigned URLs May 18, 2026
@elliotdauber elliotdauber marked this pull request as ready for review May 18, 2026 17:54
Copy link
Copy Markdown
Collaborator

@falcoagustin falcoagustin left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A few real bugs worth fixing before merging — inline.

Comment thread packages/blob/src/client.ts
Comment thread packages/blob/src/signed-token.ts
Comment thread packages/blob/src/helpers.ts Outdated
Copy link
Copy Markdown
Collaborator

@falcoagustin falcoagustin left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM — all three findings from the prior round are resolved.

  • uploadPresigned.extraChecks now correctly rejects allowOverwrite and the error message names the right function.
  • publicBlobObjectUrl removed, so the case-folding footgun has no caller.
  • Explicit oidcToken without a resolvable storeId now throws instead of silently downgrading to BLOB_READ_WRITE_TOKEN.

Minor nits to consider in a follow-up (non-blocking):

  • packages/blob/src/signed-token.ts:248-252 — the local lowercasing normalizeStoreId is unreferenced now; worth deleting so it can't be picked back up by accident.
  • Add a test for the new oidcToken-without-storeId throw branch in helpers.ts.

@elliotdauber elliotdauber merged commit 20eeaff into main May 18, 2026
9 checks passed
@elliotdauber elliotdauber deleted the elliot/oidc-auth branch May 18, 2026 20:37
falcoagustin added a commit to vercel/vercel that referenced this pull request May 20, 2026
# Problem

`vercel blob` only authenticates via `BLOB_READ_WRITE_TOKEN` (flag, env,
or `.env.local`). With OIDC landing in `@vercel/blob`, the CLI needs to:

1. Speak OIDC at all (env-pair already in `.env.local` from `vercel env
pull`).
2. Let users pass OIDC creds explicitly without env state, the way
`--rw-token` already works.
3. Refuse to silently downgrade identity when OIDC config is half-set —
the AWS credential-provider posture, not the "fall through to whatever
works" one.

# Solution

`getBlobRWToken` returns a discriminated union (`kind: 'rw' | 'oidc'`)
and resolves in this order:

1. **Explicit flags (exclusive):**
   - `--rw-token <token>` → RW.
- `--oidc-token <jwt> --store-id <id>` → OIDC. Both required when either
is given; partial → hard error.
2. **Env-derived auth.** `process.env` ∪ `.env.local` (process.env wins;
`.env.local` fills gaps), then:
- **Partial OIDC (`VERCEL_OIDC_TOKEN` xor `BLOB_STORE_ID`) → hard
error.** No silent fall-through to RW. Mirrors the explicit-flag policy
and AWS's behavior when `AWS_WEB_IDENTITY_TOKEN_FILE` is set without
`AWS_ROLE_ARN`.
- Both OIDC vars → OIDC (hoisted onto `process.env` so the SDK's
`getVercelOidcToken()` finds them).
   - `BLOB_READ_WRITE_TOKEN` → RW.
   - Otherwise → no-creds error.

Two helpers consume the union: `blobOpts(auth)` builds the SDK option
object (`{ token }` for RW, `{ storeId }` for OIDC — never passes the
OIDC JWT through `options.token`, which would be parsed as an RW token),
and `getStoreIdFromAuth(auth)` resolves `store_<id>` for
store-management subcommands.

`findFlagValue(argv, flag)` is a small linear scanner that reads the
three auth flags directly, avoiding a duplicate `parseArguments` over
the whole argv. The resolver no longer needs to know about argv layout,
flag specifications, or `permissive: true`.

# What's different from the previous priority list

| Before | After |
|---|---|
| `--rw-token` only explicit-flag path | `--rw-token` + `--oidc-token` /
`--store-id` pair |
| Env vars resolved per-source (process.env, then `.env.local`), each
requiring both OIDC vars in the same source | Sources combined; vars can
be split (e.g. token in shell, store id in `.env.local`) |
| Partial OIDC config silently fell back to RW (identity confusion) |
Partial OIDC hard-errors with explicit message |
| Resolver re-ran `parseArguments` to read three known flags | Resolver
scans argv directly for the three flags |

# Dependency

Bumps `@vercel/blob` to the snapshot containing
vercel/storage#1056 (OIDC support + `store_`
prefix normalization in `resolveBlobAuth`). Without that, `blob get`
against a private store via OIDC would still 404 because the SDK would
build a CDN host with `store_…` in the subdomain.

# Test plan

- [x] `vercel blob list/get` with `--oidc-token` + `--store-id` from a
directory with no `.env.local` — succeeds.
- [x] `--oidc-token` alone or `--store-id` alone → partial-flags hard
error.
- [x] `.env.local` with `VERCEL_OIDC_TOKEN` set but `BLOB_STORE_ID`
missing, plus a working `BLOB_READ_WRITE_TOKEN` — used to silently use
RW; now hard-errors.
- [x] `.env.local` with only `BLOB_READ_WRITE_TOKEN` (no OIDC vars at
all) — RW path still works.
- [x] Both OIDC vars present in `.env.local` — OIDC path works,
including `blob get` against a private store (paired with the SDK fix
above).
- [x] `--rw-token` flag — unchanged behavior, still wins over env.
- [x] Equals form for the new flags (`--oidc-token=… --store-id=…`) —
works.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Jeff See <jeffsee.55@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants