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.
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_TOKENis 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)
| Concern | NPM_TOKEN | OIDC trusted publishing |
|---|---|---|
| Token lifetime | Months to years until rotated | Single workflow run (~10 min) |
| Scope | Publish any package owned by the token’s user | Only the packages whose trusted publisher matches this run |
| Audit trail | None on npm side | Run ID, workflow path, commit SHA in provenance attestation |
| Rotation | Manual | Automatic per run |
| Leak blast radius | Entire account until rotated | One job, then expired |
| Provenance | Optional, often skipped | Standard 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_TOKENbefore 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.ymltorelease.yml, update the trusted publisher. - Leaving
_authTokenlines in.npmrc. The CLI prefers explicit_authTokenover OIDC. Remove it from.npmrcand.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.