Skip to content

feat: adds support for oidc publish - #8336

Merged
reggi merged 15 commits into
latestfrom
oidc
Jul 24, 2025
Merged

reggi merged 15 commits into
latestfrom
oidc

Conversation

@reggi

@reggi reggi commented May 29, 2025 •

Copy link
Copy Markdown
Contributor

🎉 Introducing OIDC Support for npm Publishing!

Further discussions not related directly this PR should happen here https://github.com/orgs/community/discussions/161015

We're thrilled to announce a new security feature that makes publishing npm packages from CI environments easier and more secure! This PR adds OpenID Connect (OIDC) token support for npm publishing, which eliminates the need to store long-lived access tokens in CI secrets.

With OIDC support, you can now publish packages from GitHub Actions and GitLab CI with improved security through short-lived, automatically generated tokens. This is a major step forward in securing the npm ecosystem and simplifying CI/CD workflows.

Technical Details

This implementation adds OIDC token support by:

  1. Detecting when npm is running in a supported CI environment (currently GitHub Actions and GitLab CI)
  2. Retrieving an OIDC token from the CI provider
  3. Exchanging this token with the npm registry for a short-lived publishing token
  4. Using this token for authentication during the publish process

The feature is designed to be non-invasive - it only activates in CI environments and gracefully falls back to traditional authentication methods when OIDC isn't available.

For Publishers

Updating Package Settings on npmjs.com

Warning

Not live yet, OIDC support is currently under development. The CLI, npmjs.com, and registry changes will be rolled out incrementally. Stay tuned for a public preview announcement. As of now, this documentation reflects features planned for a future release.

Important

In order to use OIDC publishing, a package must already exist on npmjs.com. This means the initial publish needs to be done through conventional means; further publishes, once configured, can use OIDC.

Before using OIDC for publishing, ensure your package settings on npmjs.com are configured to allow CI/CD workflows.

  1. Log in to your npm account and navigate to the Package Settings page for the package you want to publish.
  2. Add a new Connection for a Trusted Publisher and fill out the details.
  3. Save your changes to apply the new settings.

This step ensures that your package is ready to accept tokens generated via OIDC workflows.

GitHub Actions

To publish with OIDC from GitHub Actions:

  1. Add the id-token: write permission to your workflow:
permissions:
  id-token: write
  contents: read
  1. No need to set up NPM_TOKEN secrets anymore! Just run npm publish as usual:
- name: Publish to npm
  run: npm publish

GitLab CI

To publish with OIDC from GitLab CI:

  1. Configure your GitLab CI pipeline to provide the ID token via the NPM_ID_TOKEN environment variable:
publish:
  script:
    - npm publish
  id_tokens:
    NPM_ID_TOKEN:
      aud: npm:registry.npmjs.org

For other CLI's

If you're building a CLI tool that publishes to npm registries, you can implement OIDC support by:

  1. Detecting CI environments
  2. For GitHub Actions, requesting tokens via the ACTIONS_ID_TOKEN_REQUEST_URL endpoint with the proper audience format (npm:registry.hostname)
  3. For GitLab CI, using the NPM_ID_TOKEN environment variable if available
  4. Exchanging the OIDC token with the npm registry's token exchange endpoint

For other Registries

As a registry, you'll need a way for package publishers to create connections between OIDC Trusted Publishers and the registry, similar to how we allow connections to be added on the package settings page of npmjs.com.

To support OIDC token authentication in your npm-compatible registry:

  1. Implement an endpoint at /-/npm/v1/oidc/token/exchange/package/${escapedPackageName} that accepts POST requests (no body) with Authorization header / Bearer set to the jwt-token-from-ci-provider.

  2. Verify the OIDC token using standard JWT validation practices, checking the audience claim matches your expected format (npm:your.registry.hostname)

  3. Return a response with a short-lived npm token:

    {
      "token": "npm_short_lived_token"
    }

Technical Overview

  • The OIDC integration begins in the publish command.
  • This adds a new utility module oidc.js handles:
  • Full test coverage has been implemented in the publish test suite. ✅
  • The authentication flow follows this path:
    • publish command → libnpmpublish module → npm-registry-fetch

Key touchpoints:

This initial implementation is focused on the publish workflow only. Currently OIDC token support is limited to the publish command.

@reggi
reggi requested a review from a team as a code owner May 29, 2025 18:31
@ljharb

ljharb commented May 29, 2025

Copy link
Copy Markdown
Contributor
  1. Is there a config option or command line argument that can prevent OIDC from kicking in? (it seems like "write" is a magic id-token string for GHA, so i'm not sure what i'd put there or in an env var to guarantee that it can't kick in)
  2. if i wanted to publish using OIDC on my local machine, for testing or for funsies, what would I need to set up and how would I need to invoke it?

@reggi

reggi commented May 29, 2025 •

Copy link
Copy Markdown
Contributor Author

@ljharb 👋

Is there a config option or command line argument that can prevent OIDC from kicking in? (It seems like "write" is a magic id-token string for GHA, so I'm not sure what I'd put there or in an env var to guarantee that it can't kick in.)

I've thought about this a lot. I also want this, but I question its necessity. If you have id: write and don't configure the trusted connection on npmjs.com, the token-exchange endpoint will return a 404 and you won't get a token, and the publish flow will continue as normal. So, you're already opted out by not configuring it—though, yes, there is still an extra network request.

If I wanted to publish using OIDC on my local machine, for testing or for fun, what would I need to set up and how would I need to invoke it?

Not really possible. The whole point of OIDC is to have a trusted publisher (like GitLab or GitHub) as the issuer of a token that the registry trusts. A local machine isn't a trusted issuer, so we wouldn't be able to validate any token you could provide.

@ljharb

ljharb commented May 29, 2025

Copy link
Copy Markdown
Contributor

Right, but what if an attacker configures OIDC on npmjs.com unbeknownst to me? I'd still want to ensure CI can't publish with it.

So to clarify, the reason it's not possible is because the npm servers only have a finite hardcoded list of "trusted OIDC publishers", and i'm not on it?

Comment thread lib/utils/oidc.js Resolved
Comment thread lib/utils/oidc.js Resolved
@reggi
reggi force-pushed the oidc branch 2 times, most recently from 80c39ba to 3a930c9 Compare May 30, 2025 16:45
wraithgar
wraithgar previously approved these changes May 30, 2025

@wraithgar wraithgar left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

So clean, so boilerplate. Let's leave this unmerged for at least a little while to see if any more community input happens.

@reggi

reggi commented May 30, 2025 •

Copy link
Copy Markdown
Contributor Author

@ljharb

Right, but what if an attacker configures OIDC on npmjs.com unbeknownst to me? I'd still want to ensure CI can't publish with it.

If an attacker has access to your npmjs.com account, they can just make gat tokens, I think OIDC would be the least of your worries 🤔 . Further expanding on this idea if a malicious actor got access to your account and added a trusted connection to one of THEIR repos, with their own workflow a --no-oidc flag isn't gonna help you at that point, they write the workflow, and the npm publish script in the workflow, they can just omit that flag.

So to clarify, the reason it's not possible is because the npm servers only have a finite hardcoded list of "trusted OIDC publishers", and i'm not on it?

Yeah, kinda

@leobalter

Copy link
Copy Markdown
Contributor

Just to chime in, I'm onboard and supportive w/ the decision to avoid a cli parameter here for the sake of feature streamlining. Security wise, the malicious access goes similarly to creating access tokens, as mentioned above.

So to clarify, the reason it's not possible is because the npm servers only have a finite hardcoded list of "trusted OIDC publishers", and i'm not on it?

By finite we are kick starting this feature w/ 2 trusted publishers: GitHub Actions and GitLab.

This is an MVP and we want to understand the usage feedback for the next iterations on this feature.


If possible, I'd really love if we can shift other feature requests and feedback at the community discussion post as it makes it easier for me to track feedback received and we keep this PR limited to new changes added to the repo.

Comment thread lib/utils/oidc.js Resolved
Comment thread lib/utils/oidc.js Resolved
Comment thread lib/commands/publish.js Resolved
Comment thread lib/utils/oidc.js Resolved
reggi and others added 2 commits June 27, 2025 16:02
#8399)

Co-authored-by: Chris Sidi <hashtagchris@github.com>
I don't believe we need to destructure the objects as we we're doing.
@jakebailey

Copy link
Copy Markdown

I feel like this logic should not be in the npm CLI, but rather a part of setup-node or some other third party lib. Otherwise, this code will end up continuing to include more and more special cases for more CI providers, and have to be reimplemented by every other package manager or tool that needs the npm token directly (like the DefinitelyTyped infrastructure that publishes directly via the API, which I would love to use this with).

Comment thread lib/utils/oidc.js Resolved
Comment thread lib/utils/oidc.js Resolved
reggi added 4 commits July 9, 2025 10:15
This PR adds "auto" or "default" provenance to publishes that use OIDC
within github and gitlab. It does this by checking the OIDC id token
payload and checking if the current repo's visibility is public or
private if it's public we do the equivalent of adding the `--provenance`
flag.
small update to allow gitlab to NOT REQUIRE provenance
lovasoa added a commit to sql-js/sql.js that referenced this pull request Aug 14, 2026
The runner's bundled npm (10.9.8, shipped with Node 22) predates
npm/cli#8336 which added OIDC token exchange support (npm 11.4.2),
so --provenance was silently ignored and publish failed with
ENEEDAUTH. Upgrade npm before publishing.
rama6636 added a commit to nRouterAI/nrouter-sdk that referenced this pull request Aug 28, 2026
Prerequisite for trusted publishing, landed separately and ahead of it on
purpose. The runner's Node 22.23.2 bundles npm 10.9.8 — measured in run
33201098721 — and OIDC publish support arrived in npm 11.5.0 (npm/cli#8336,
2025-07-24).

The ordering is the point. Registering a trusted publisher on npmjs.com while
the runner still had npm 10 would look like the REGISTRATION had failed: npm
10 does not attempt OIDC at all, so it would quietly authenticate with
NODE_AUTH_TOKEN and publish green by the old path, with nothing in the log
saying which mechanism ran. Raising the floor first makes the switch a
one-line change instead of a debugging session.

Nothing changes today: with a token present and no trusted publisher
registered, npm 11 authenticates exactly as npm 10 did. This push exercises
the new step for free — 1.1.1 is already on the registry, so `already
published?` short-circuits and the run is a green no-op.

Why this matters now: bypass-2FA granular tokens lose direct publish in
January 2027 (github.blog changelog 2026-07-31), and this repo's token
expires 2026-11-26. Trusted publishing removes the credential rather than
rotating it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
HOONY-LEE added a commit to HOONY-LEE/softium-ui that referenced this pull request Aug 31, 2026
Adds id-token: write to the release workflow and forces npm to a version
>=11.5.1 right after actions/setup-node, since pnpm publish shells out to
whatever npm is first on PATH (see pnpm's exec/run-npm) rather than bundling
its own, and OIDC trusted publishing requires that npm version.

This is additive, not a cutover: npm's OIDC detection gracefully falls back to
NODE_AUTH_TOKEN/NPM_TOKEN when a package has no trusted publisher registered
(confirmed against the source PR, npm/cli#8336), so every package keeps
publishing exactly as before until its trusted publisher is configured.

RELEASE.md documents the one part that needs a human: registering a trusted
publisher per package on npmjs.com (repo/workflow-filename/org — can't be done
via API, needs an npm web session), plus how to verify OIDC actually took over
per package, and the known pnpm+OIDC intermittency
(pnpm/pnpm#11513) this setup works around.

Also fixes a pre-existing README bug while touching this section: `pnpm
version` collides with pnpm's own built-in version-bump command, so the
documented command silently ran the wrong thing. Needs `pnpm run version`
(release.yml already did this correctly).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
janakhpon added a commit to janakhpon/monocr-onnx that referenced this pull request Sep 3, 2026
The trusted publisher on npm was already configured correctly. The retagged
js/v0.3.0 run got past every other step -- including the real test-ordering
fix -- and failed here, on a response body that plainly carried a real,
freshly issued token:

  {"created":1788421934,"expires":1788422834,"token":"***","token_type":"oidc"}

That is a success shape, and the step reported it as npm refusing the
exchange, because it checked `[ "$code" != "200" ]` and the registry did not
return exactly 200.

npm's own CLI (npm/cli#8336) settles what the real contract is: it does not
check the HTTP status code for this exchange at all. It decides success or
failure purely by whether the parsed JSON body has a `token` field. This step
now does the same, rather than a workflow here guessing which 2xx code the
registry uses for "created" a token.

Verified against both bodies actually seen from this registry: the real
success body above returns "yes", and a real refusal body,
{"error":"invalid-publisher"}, returns "no". YAML and embedded shell both
syntax-checked.
KHAEntertainment pushed a commit to KHAEntertainment/ofa that referenced this pull request Oct 24, 2026
The v0.2.1 release run signed the provenance statement successfully
('Provenance statement published to transparency log') and then failed
on the publish PUT with:

  npm error code E404
  npm error 404 Not Found - PUT https://registry.npmjs.org/@khaentertainment%2fofa

Root cause: CI was running npm CLI 10.8.2. npm/cli#8336 added OIDC
publish support in npm CLI 11.5.0, and npm's docs require 11.5.1+ on
Node 22.14.0+. An older CLI signs provenance but then sends
setup-node's placeholder NODE_AUTH_TOKEN on the publish PUT; the
registry answers a bad token on PUT with a bare 404 rather than 401,
so it reads like a missing package instead of an auth failure. npm
even shipped npm/cli#8441 ('Makes 404 errors less scary without
revealing existence') acknowledging exactly this shape of error.

Node 22 ships npm 10.9.9 — still too old. Node 24 ships npm 11.19.1.

Changes:

  - setup-node node-version: '20' -> '24'.
  - package-manager-cache: false, per npm's guidance to never cache
    the package manager in a release build.
  - New 'Assert npm supports OIDC publish' step fails the release
    loudly if the npm CLI is ever too old, instead of shipping a 404
    that reads like a missing package.
  - Action majors bumped across both workflows: checkout v4->v7,
    setup-go v5->v7, setup-node v4->v7, upload-artifact v4->v7,
    download-artifact v4->v8. The old majors target Node 20, which
    GitHub now forces to run on Node 24 and warns about on every run.

Note: the v0.2.1 GitHub Release succeeded with all four binaries and
checksums.txt; only the npm publish failed. npm never received
0.2.1, so the tag is being re-cut rather than bumped.
KHAEntertainment pushed a commit to KHAEntertainment/ofa that referenced this pull request Oct 24, 2026
The trusted publisher is deliberately configured WITHOUT the 'publish'
permission. npm's docs on that checkbox: 'Not recommended. For stronger
security, leave this unchecked to require staged publishing for new
versions.' A publisher created after 2026-09-03 is granted
`npm stage publish` unconditionally, and direct publishing stays
off.

So CI stages the package and a maintainer approves it with 2FA:

  CI          npm stage publish --access public --provenance --tag latest
              -> package sits in npm's staging area, NOT public
  Maintainer  npm stage approve <stage-id>   (or the Staged Packages
              tab on npmjs.com) -- both require 2FA
              -> version becomes public

Staging intentionally bypasses 2FA in the CLI (proof of presence is
deferred to the approve step), so the CI step needs only OIDC.

npm CLI version floors, all now enforced or satisfied:
  11.5.0  OIDC publish support (npm/cli#8336)
  11.15.0 npm stage publish
  11.21.0 OIDC npm dist-tag
Node 24 ships npm 11.19.1, which clears the first two but not the
third, and no Node release bundles 11.21.0 yet -- so the CLI is now
upgraded explicitly with 'npm install -g npm@^11.21.0'. The assert
step was retargeted to 11.21.0.

The stage id is parsed from npm's output and written to the step
summary with the two ways to approve it.

Also renames the job to 'Stage @khaentertainment/ofa' so the workflow
UI does not imply the version went live.

The GitHub Release still publishes automatically on the tag push. That
is a deliberate split: release binaries are content-addressed and
anyone with the link can fetch them, whereas an npm package is
installed by anyone. If the staged publish is rejected, cut a new
version rather than re-cutting the tag.
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.

7 participants