npmjs: security theater galore
TL;DR
NPM is removing direct publishing tokens due to the increase in supply chain attacks where people export environment variables in their environment without (and with) 2FA. They are doing this without offering workable solutions for many different workflows. It is done in, typical Microsoft fashion, our way or the highway. And to be fair to Microsoft, it is the way of the tech giants: Force your workflow upon others.
Deprecating it in Jan 2027
Via Upcoming changes to npm 2FA-bypass granular access tokens (GATs) the npm team announced they are deprecating Granular Access Tokens which bypass 2FA. For those who don’t know, npmjs, requires a 2FA method to be set. The fun kicker is, they only allow hardware keys.

They removed alternative multifactor in February this year:
We understand the frustration - local TOTP generators are a reasonable step up from no 2FA, and we appreciate that many users rely on them today.
That said, TOTP as a protocol is vulnerable to phishing and real-time relay attacks in ways that passkeys/WebAuthn are not, regardless of whether the generator itself is compromised. The security improvement from moving to passkeys is meaningful, and our direction there hasn’t changed.
What has changed is that we’re being deliberate about the timeline. We want trusted publishing and proof of presence support to be broadly available and well-adopted before we deprecate existing TOTP configurations - so no one is left without a secure publishing path.
Source
The fun ways of trying to login when you aren’t at your desk. Now, I have multiple machines and multiple Yubikeys, but my Mac doesn’t have regular USB ports, so I had to use GAT without 2FA in order to push a release to npm. You could argue either Apple should ship Macs with regular USB ports or I should order additional Yubikeys, but you could also argue NPM has lost the absolute plot in regards in how they manage account access. For example, if you don’t have your Yubikey with you and you rely on a backup code, you are not merely burning a backup key, you are also losing the ability to to make changes to your account:

Please note the “so no one is left without a secure publishing path”. They aren’t fixing any of the problem they’ve introduced themselves.
September 29th 2026 is the new January 2027
I was working on a mobile app, made a small change to a library I maintain and wanted to publish it. Instead I got greeted with this error message:
29 verbose stack Error: This command requires you to be logged in to https://registry.npmjs.org/
29 verbose stack at #publish (/usr/lib/node_modules/npm/lib/commands/publish.js:166:29)
29 verbose stack at async Publish.exec (/usr/lib/node_modules/npm/lib/commands/publish.js:51:5)
29 verbose stack at async Npm.exec (/usr/lib/node_modules/npm/lib/npm.js:193:9)
29 verbose stack at async module.exports (/usr/lib/node_modules/npm/lib/cli/entry.js:67:5)
30 error code ENEEDAUTH
31 error need auth This command requires you to be logged in to https://registry.npmjs.org/
32 error need auth You need to authorize this machine using `npm adduser`
33 verbose cwd /mnt/code/opn/javascript/react-native/events/.build/5ca5db5dcb4fe5a7
34 verbose os Linux 7.2.6+deb14-amd64
35 verbose node v26.10.0
36 verbose npm v11.19.1
37 verbose exit 1
38 verbose code 1
39 error A complete log of this run can be found in: /home/wesleys/.npm/_logs/2026-09-29T20_01_03_698Z-debug-0.log
Weird, we are 3 months out, clearly this is a mistake, but let’s do what npm asks me to do:
$ npm adduser
npm notice Log in on https://registry.npmjs.org/
npm notice npm tokens that bypass 2FA are being restricted for account changes and direct publishing. Learn how to prepare: https://gh.io/npm-gat-bypass2fa-deprecation
Create your account at:
https://www.npmjs.com/login?next=/login/cli/xyz
Press ENTER to open in the browser...
Logged in on https://registry.npmjs.org/.
Now, here NPM fucks up, they leave a clear text token in your .npmrc, I
noticed that later on after I tried to release again and got hit with this
message:
npm notice
npm notice Publishing to https://registry.npmjs.org/ with tag latest and public access
npm error code EOTP
npm error This operation requires a one-time password.
npm error Open this URL in your browser to authenticate:
npm error https://www.npmjs.com/auth/cli/***
npm error
npm error After authenticating, your token can be retrieved from:
npm error https://registry.npmjs.org/-/v1/done?authId=***
npm error A complete log of this run can be found in: /home/wesleys/.npm/_logs/2026-09-29T20_03_06_220Z-debug-0.log
What? I just did everything you asked me to and you still won’t let me
publish? I looked at my token, compared it to the one in my account, went back,
looked at it again… Only to see the token got replaced by the npm adduser
command. Thanks npm, not.
GAT with 2FA enabled
And it gets worse, even with a GAT that does NOT bypass 2FA you are still greeted with this error:
npm notice Publishing to https://registry.npmjs.org/ with tag latest and public access
npm error code EOTP
npm error This operation requires a one-time password.
npm error Open this URL in your browser to authenticate:
npm error https://www.npmjs.com/auth/cli/***
npm error
npm error After authenticating, your token can be retrieved from:
npm error https://registry.npmjs.org/-/v1/done?authId=***
npm error A complete log of this run can be found in: /home/wesleys/.npm/_logs/2026-09-29T20_27_01_412Z-debug-0.log
They are rolling out releases which break consumers, and promises.
GAT with staging
A token with only staging does work, one doesn’t need 2FA. But the thing is, I now need to login again to the npmjs site. And remember, if you use a recovery code, your account gets locked out, meaning npm just because utterly useless for you. Not only do you have to wait for an “automatic review”, you need your effing 2FA again, even tho you used it several times in the same session. Security adds friction, and that’s ok, but this level of friction is how early humans discovered how to make fire.
NPM isn’t for normal people anymore, only for CI
but only from these three
So, as a human doing releases, you are only left with staged publishing, a horrible experience for anyone that ships code. The only workaround is to ship releases via CI. Something I for one refuse to do.
And even if you use CI, you have to hope your CI provider is supported. CI from Bitbucket, Codeberg, or basically, anything other than Github, Gitlab and CircleCI isn’t supported yet. Trying to publish a new package: Trusted publishing doesn’t support that (yet – and timelines are not really communicated either). Remember they said that everyone gets secure releases, but only if everyone is on three CI providers only. And your out of luck on private infrastructure, according to the docs “Self-hosted runners are not currently supported but are planned for future releases.”
Instead they should improve npm so actual 2FA gets supported from the command line, instead they focus on the more corporate use-case with trusted publishing. Absolute bonkers.
The alternative
I saw there is an alternative to npmjs, called The Javascript Registry, or jsr.io in short. And I’m now really considering using it. I need to figure out how to change my release tooling so I can release to jsr.io. They focus on ESM and Typescript and I’m not sure how I can ship React Native mobile repo’s, which use CommonJS, and I need to ensure my tooling knows how to work with it. Another way would maybe setup a private registry somewhere..
I’m waiting till next year to see what I’m going to do, I’ll update all my tooling to support jsr regardless. It costs nothing extra from where I’m sitting and buys me a whole lot more than NPM currently offers me.