Skip to content
problem confidence: high

npm publish OIDC vs NPM_TOKEN: Why Long-Lived Tokens Are Getting Deprecated and What to Migrate To

Why npm is steering maintainers off long-lived NPM_TOKEN secrets toward OIDC trusted publishing, what the security difference actually is, and the exact migration path for a GitHub Actions repo.

Published May 13, 2026 · kw: npm publish OIDC vs NPM_TOKEN

Sources: S-001 S-002 S-004 S-005

NPM_TOKEN is the long-lived secret most maintainers added to GitHub Actions in 2019 and have not rotated since. npm’s trusted publishing replaces it with a short-lived OIDC token minted per workflow run. Here is the practical difference, why npm is steering maintainers off long-lived tokens, and the migration order that does not break CI.

TLDR

  • A NPM_TOKEN is a long-lived bearer token. Anyone with it can publish any of your packages until you rotate it.
  • An OIDC trusted-publisher run uses a short-lived (~10 minute) GitHub-signed token, scoped to a specific repo, workflow, and run.
  • If the token leaks, the blast radius is the duration of one job, not “until you remember to rotate.”
  • The migration is non-destructive: add OIDC, verify it works on one publish, then remove NPM_TOKEN.

Why the problem happens

Long-lived NPM_TOKEN values sit in GitHub Actions secrets, copied across forks, pasted into Slack threads, accidentally echo’d in logs. They have wide scope (publish any of your packages) and infinite lifetime until you rotate.

The npm registry team and GitHub Security have been explicit (S-001) that trusted publishing is the replacement story: short-lived tokens, scoped to the workflow that earned them, verifiable against a cryptographic chain. Trusted publishing was generally available in 2024 and the documentation now uses the long-lived-token flow only as a fallback.

Real-world maintainer pain in npm/cli issues (S-005) clusters around:

  • A token leaked in a CI log when an unrelated step ran env.
  • A token kept alive after a maintainer left the project.
  • A token granted to a third-party action that turned out to be malicious months later.

OIDC kills all three because the token only exists for the lifetime of the run, and id-token: write is a per-workflow grant you can audit.

What changes (and what does not)

ConcernNPM_TOKENOIDC trusted publishing
Token lifetimeMonths to years until rotatedSingle workflow run (~10 min)
ScopePublish any package owned by the token’s userOnly the packages whose trusted publisher matches this run
Audit trailNone on npm sideRun ID, workflow path, commit SHA in provenance attestation
RotationManualAutomatic per run
Leak blast radiusEntire account until rotatedOne job, then expired
ProvenanceOptional, often skippedStandard part of the trusted-publishing flow

What does not change:

  • You still need a npm account that owns the package.
  • You still publish via npm publish.
  • 2FA on your npm account is still recommended for the manual web actions (deleting packages, changing owners).

Step-by-step migration

1. Run the preflight before you change anything

Open the ForgeKite preflight checklist, paste your current package.json and your publish.yml, and confirm what is currently in place. If NPM_TOKEN is present, the checklist will flag it; if id-token: write is missing, it will flag that too.

2. Add the trusted publisher on npm

npmjs.com → your package → Settings → Trusted Publishers → Add → GitHub Actions. Fill the repo and the workflow filename. This is non-destructive — adding a trusted publisher does not break the existing NPM_TOKEN flow.

3. Update the workflow in a feature branch

Add permissions: { id-token: write, contents: read }. Switch to actions/setup-node@v4 with registry-url: "https://registry.npmjs.org". Add --provenance --access public to the publish command. Keep NPM_TOKEN set for one final publish so you can compare.

4. Publish a pre-release

git tag v1.2.3-rc.1 and let the workflow run. On npm, the package page should now show a provenance badge linked to the workflow run and the commit. The publish summary in the GitHub Actions run should mention oidc.

If you see the provenance badge, the OIDC path worked. If you do not, OIDC fell back to the legacy token — check the provenance failures post.

5. Remove NPM_TOKEN

In the workflow: delete any env.NPM_TOKEN line and any _authToken reference. In npm: revoke the token at npmjs.com → Account → Access Tokens. In GitHub: delete the NPM_TOKEN repository secret.

Publish one more pre-release without the token to confirm the OIDC path still works.

Concrete example: the diff

Before:

- run: npm publish
  env:
    NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

After:

permissions:
  id-token: write
  contents: read

steps:
  - uses: actions/setup-node@v4
    with:
      node-version: 20
      registry-url: "https://registry.npmjs.org"

  - run: npm publish --provenance --access public

No NODE_AUTH_TOKEN, no NPM_TOKEN. The OIDC handshake is implicit.

Common mistakes

  • Removing NPM_TOKEN before confirming OIDC works. Always run one publish on the new path with the old token still available to roll back to.
  • Forgetting registry-url. Without it, OIDC silently falls back to legacy auth — your publish works, but it works for the wrong reason.
  • Trusted publisher pointing at the wrong workflow filename. Trusted publishers are exact-match. If you rename publish.yml to release.yml, update the trusted publisher.
  • Leaving _authToken lines in .npmrc. The CLI prefers explicit _authToken over OIDC. Remove it from .npmrc and .npmrc.template.

FAQ

Will NPM_TOKEN stop working entirely?

Not announced. npm has not declared a hard deprecation date; trusted publishing is the recommended path and the documentation treats NPM_TOKEN as legacy. Treat it as deprecation-in-practice and migrate when convenient.

Does this work for GitLab CI or CircleCI?

Yes, the trusted publisher dropdown on npm includes GitLab and CircleCI. The OIDC token shape is different, but the principle is identical.

Is OIDC the same as Sigstore provenance?

Related but separate. OIDC is the auth mechanism that lets the workflow prove it is the repo it claims to be. Provenance is the signed attestation that the tarball came from that workflow. You can publish with OIDC and skip provenance — but you should not.

Next step

If you have not yet pasted your package.json into the preflight checklist, do it now — the migration is mostly a “delete the secret and add three lines” exercise once the preflight shows green. For the failure mode breakdown, read npm publish —provenance failed: 8 reasons and the first-publish setup guide.