When I ask developers how they authenticate to the npm registry, the answer is almost always "there's a token in my .npmrc". That is true and it is also the shallow end of a surprisingly deep pool. The npm registry supports basic auth, legacy tokens, granular access tokens, OIDC trusted publishers, and a handful of proprietary auth flows for enterprise mirrors. Each has different security properties, and choosing incorrectly is how you end up with an incident.
I've spent much of the last year auditing how organizations authenticate to the npm registry and its common mirrors (Verdaccio, Nexus, Artifactory, GitHub Packages). The patterns repeat. Here is the landscape, current as of April 2024, with the details I wish someone had written down when I started.
The Four Flows
The npm registry (the one at registry.npmjs.org) supports four authentication mechanisms.
The first is HTTP Basic auth. This is npm adduser over HTTPS sending a username and password. The password is hashed client-side with bcrypt before transmission (as of 2018), but the server still holds it and uses it to issue a session token. Basic auth is what backs npm login when you don't have 2FA enabled.
The second is classic access tokens. These are long strings that begin with npm_ (since npm 7). They're opaque bearer tokens: whoever has the string can act as the user. Classic tokens have two sub-types: publish tokens (which can push packages) and read-only tokens (which can only install). The npm token create command creates them.
The third is granular access tokens, introduced in December 2022 and made generally available in early 2023. These also begin with npm_ but carry scoped permissions: you specify the packages or scopes the token can publish to, an expiration date (required, up to one year), and whether the token can read private packages. This is the flow you should be using now; the rest of this post explains why.
The fourth is OIDC trusted publishers, rolled out as a preview in June 2023 and generally available starting April 2024. OIDC lets a CI system (GitHub Actions, initially) publish to a configured package without any long-lived token. The CI workflow presents a short-lived OIDC token to npm, npm validates it against a trusted-publisher configuration attached to the package, and if it matches, npm issues a one-shot publish credential. This is the most secure flow available and the one I push all new projects toward.
What A Token Actually Looks Like
An npm token, post-npm 7, has the shape npm_ followed by 36 base58-adjacent characters. The npm_ prefix is detectable by scanning tools, which is why GitHub's secret scanning and Trufflehog both catch them reliably. Before the prefix standardization in 2021, tokens were raw UUIDs, which were not detectable by content shape alone. If you have legacy tokens from that era still in circulation, they should be rotated immediately; the detection infrastructure does not help you with them.
Granular access tokens also use the npm_ prefix, but their server-side representation carries the scope and expiration metadata. You cannot tell a classic token from a granular token just by looking at the string. The only way to inspect a token's scope is through the npm web UI or via npm token list, which reports classic tokens but as of my last check in March 2024 still does not enumerate granular tokens reliably via CLI. You need the web UI for granular token management.
The .npmrc Problem
The standard way to carry a token is an entry in ~/.npmrc or a project .npmrc that looks like //registry.npmjs.org/:_authToken=npm_abc.... This file is plaintext. Every single credential leak I have investigated in the last three years has started either with a committed .npmrc or a .npmrc baked into a Docker image layer.
The mitigations are well-known and rarely followed. Never commit .npmrc with a token. Use environment variable substitution: //registry.npmjs.org/:_authToken=${NPM_TOKEN}. When building Docker images, use BuildKit secrets (--mount=type=secret,id=npmrc) so the token never lands in a layer. For developer machines, use a credential helper; macOS Keychain via npm-credential-helper-keychain is the only one I trust, because it avoids ever writing the token to disk.
Public Mirrors And Config Scoping
The per-scope registry configuration is where a lot of teams get subtle bugs. You can configure @mycompany:registry=https://npm.mycompany.com/ and point only the @mycompany scope at your private registry. But if you don't also scope the _authToken config with the same URL, you can end up sending your private registry's token to the public npm registry. I watched this happen during a dependency confusion investigation in 2023: a misconfigured .npmrc sent the enterprise token along with every install request, including to the public registry, for about six weeks before a security scan caught it.
The correct form:
@mycompany:registry=https://npm.mycompany.com/
//npm.mycompany.com/:_authToken=${COMPANY_TOKEN}
//registry.npmjs.org/:_authToken=${PUBLIC_TOKEN}
Each registry URL has its own auth entry. The tokens never cross.
2FA And Publish
Npm has required 2FA for publish on high-impact packages since 2022. The top thousand packages by downloads are in the "high-impact" bucket. For those packages, publishing requires either a 2FA prompt or a "automation" token that has been specifically marked as bypassing 2FA for CI use.
The automation-bypass token is the weakest part of the current model. It is, in effect, a password equivalent: anyone who has the token can publish. Granular tokens with expirations and package-scoped permissions mitigate the blast radius, but the underlying bypass still exists.
OIDC trusted publishers eliminate this entirely. A GitHub Actions workflow configured as a trusted publisher for a package can publish without any token at all. The OIDC identity (the repo, workflow, and ref) is bound to the package configuration at the registry side. An attacker who compromises a developer's laptop gets no publish capability; they would need to compromise the GitHub Actions workflow, which is a different and substantially harder target.
The Provenance Link
When a publish happens through OIDC, npm can record a Sigstore provenance attestation that binds the published tarball to the exact GitHub Actions workflow run that produced it. Since April 2023 this data is visible on the npm website and queryable via npm audit signatures. In my experience, consumers don't yet check it often, but the data is there, and it is the foundation of any "verified build" story for the npm ecosystem.
Migration Advice
If you're on classic tokens, move to granular tokens now. The migration is straightforward: create a new granular token scoped to the packages you actually publish, deploy it, delete the classic token. Set an expiration. If you can move to OIDC trusted publishers, do that instead; it eliminates the class of "long-lived token stolen from CI" attacks entirely.
Audit .npmrc files across your fleet. A simple grep for _authToken=npm_ outside of files listed in .gitignore is enough to catch most issues.
How Safeguard Helps
Safeguard inventories every npm token active across your organization, tagging each as classic, granular, or OIDC, and flagging tokens without expirations or without package scope restrictions. For repos using GitHub Actions, we check whether the workflow publishes via OIDC trusted publishers or via a long-lived secret, and surface the ones still on the old model. Our scanner also catches committed .npmrc tokens during SCM ingestion, not just at merge time, so a token committed to a feature branch does not stay active for the weeks it takes to merge. If you're planning a token migration, Safeguard generates the sequenced rotation plan so you don't lose publish capability mid-flight.