[BUG] "npm login" does not work when registry uses ssl client authentication (mTLS)
Is there an existing issue for this?
- [x] I have searched the existing issues
This issue exists in the latest npm version
- [x] I am using the latest npm
Current Behavior
I'm using Verdaccio to run my own private npm registry and I have the registry behind ssl client authentication.
My .npmrc is correctly configured (npm installs do work [with cleared cache]):
registry="https://my-private-registry.com/"
//my-private-registry.com/:keyfile="/path/to/client.pkey"
//my-private-registry.com/:certfile="/path/to/client.cert"
npm notice Log in on https://my-private-registry.com/
Username: exory2024
Password:
npm error code E400
npm error 400 Bad Request - PUT https://my-private-registry.com/-/user/org.couchdb.user:exory2024/-rev/undefined
npm error A complete log of this run can be found in: /XXX.log
HTTP Status Code 400 is indicative of failed client auth. I have checked my server logs and can confirm that this particular request got denied.
I can also confirm that npm login starts to work again when I remove the ssl client auth on the server.
Stacktrace:
21 verbose stack HttpErrorGeneral: 400 Bad Request - PUT https://my-private-registry.com/-/user/org.couchdb.user:exory2024/-rev/undefined
21 verbose stack at /usr/local/lib/node_modules/npm/node_modules/npm-registry-fetch/lib/check-response.js:103:15
21 verbose stack at process.processTicksAndRejections (node:internal/process/task_queues:105:5)
21 verbose stack at async putCouch (/usr/local/lib/node_modules/npm/node_modules/npm-profile/lib/index.js:133:18)
21 verbose stack at async otplease (/usr/local/lib/node_modules/npm/lib/utils/auth.js:8:12)
21 verbose stack at async Object.login (/usr/local/lib/node_modules/npm/lib/utils/auth.js:91:11)
21 verbose stack at async Login.exec (/usr/local/lib/node_modules/npm/lib/commands/login.js:31:35)
21 verbose stack at async Npm.exec (/usr/local/lib/node_modules/npm/lib/npm.js:208:9)
21 verbose stack at async module.exports (/usr/local/lib/node_modules/npm/lib/cli/entry.js:67:5)
22 verbose statusCode 400
~~The bug occurs in this function.~~
The bug is triggered here.
I ran npm login while logging the return value of regFromURI:
npm notice Log in on https://my-private-registry.com/
URI {
uri: 'https://my-private-registry.com/-/v1/login',
regKey: '//my-private-registry.com/'
}
Username: exory2024
Password:
⠹URI {
uri: 'https://my-private-registry.com/-/user/org.couchdb.user:exory2024',
regKey: '//my-private-registry.com/'
}
⠸URI {
uri: 'https://my-private-registry.com/-/user/org.couchdb.user:exory2024',
regKey: '//my-private-registry.com/'
}
URI {
uri: 'https://my-private-registry.com/-/user/org.couchdb.user:exory2024/-rev/undefined',
regKey: false
}
As you can see the function fails to return the correct regKey for the uri https://my-private-registry.com/-/user/org.couchdb.user:exory2024/-rev/undefined'.
Expected Behavior
Successful login.
Steps To Reproduce
~~I suppose to recreate the bug you just have to call regFromURI with the URI above.~~
Environment
- npm: 11.5.1
- Node.js: 23.11.0
- OS Name: Apple Sequoia 15.5
- System Model Name: MacBook Pro M1 Max
- npm config:
; "project" config from /path/to/my/project/.npmrc
//my-private-registry.com/:certfile = (protected)
//my-private-registry.com/:keyfile = (protected)
registry = "https://my-private-registry.com/"
; node bin location = /usr/local/bin/node
; node version = v23.11.0
; npm local prefix = /path/to/my/project
; npm version = 11.5.1
; cwd = /path/to/my/project
; HOME = /Users/exory2024
; Run `npm config ls -l` to show all defaults.
Ok doing some more investigation shows me that the bug isn't in regFromURI but somewhere else:
Logging the second parameter that is passed down to regFromURI:
const { regKey, authKey } = regFromURI(uri, forceAuth || opts)
console.log({regKey}, forceAuth || opts)
{ regKey: false }
{ username: 'exory2024', password: 'XXX', otp: null }
Yeah, the issue is that forceAuth overwrites opts which then causes regFromURI to return false:
const forceAuthOrOpts = forceAuth || opts
console.log("forceAuthOrOpts", "//my-private-registry.com/:keyfile" in forceAuthOrOpts)
const { regKey, authKey } = regFromURI(uri, forceAuthOrOpts)
npm notice Log in on https://my-private-registry.com/
forceAuthOrOpts true
Username: exory2024
Password:
⠇forceAuthOrOpts true
⠙forceAuthOrOpts true
⠹forceAuthOrOpts false <--- forceAuth is "{ username: 'exory2024', password: 'XXX', otp: null }" here
npm error code E400
npm error 400 Bad Request - PUT https://my-private-registry.com/-/user/org.couchdb.user:exory2024/-rev/undefined
npm error A complete log of this run can be found in: /path/to/log
could it be because of the user name is undefined here ?
https://my-private-registry.com/-/user/org.couchdb.user:exory2024/-rev/undefined
Ok I managed to track down where forceAuth is created / passed down:
return putCouch(`/-rev/${body._rev}`, username, body, {
...opts,
forceAuth: {
username,
password: Buffer.from(password, 'utf8').toString('base64'),
otp: opts.otp,
},
})
https://github.com/npm/npm-profile/blob/main/lib/index.js#L202
Checking getAuth from npm-registry-fetch we can see that forceAuth can have the properties keyfile and certfile but they aren't passed down from npm-profile's putCouch call:
const { regKey, authKey } = regFromURI(uri, forceAuth || opts)
// we are only allowed to use what's in forceAuth if specified
if (forceAuth && !regKey) { // eXory2024: <- this condition is TRUE in my case but forceAuth doesn't have "keyfile" or "certfile"
return new Auth({
// if we force auth we don't want to refer back to anything in config
regKey: false,
authKey: null,
scopeAuthKey: null,
token: forceAuth._authToken || forceAuth.token,
username: forceAuth.username,
password: forceAuth._password || forceAuth.password,
auth: forceAuth._auth || forceAuth.auth,
certfile: forceAuth.certfile, // eXory2024: <- basically never set in my case
keyfile: forceAuth.keyfile, // eXory2024: <- basically never set in my case
})
}
To me this looks like the author of this code anticipated certfile and keyfile to be set but they are never passed down.
Doing a quick grep revealed to me that there aren't really any other places where forceAuth is created.
When I hardcore keyfile and certfile in loginCouch then npm login succeds:
return putCouch(`/-rev/${body._rev}`, username, body, {
...opts,
forceAuth: {
username,
password: Buffer.from(password, 'utf8').toString('base64'),
otp: opts.otp,
keyfile: opts['//my-private-registry.com/:keyfile'],
certfile: opts['//my-private-registry.com/:certfile']
},
})
So I really think passing down the appropriate keyfile and certfile propeties in loginCouch is all that is needed to fix this bug?
@eXory2024 yeah your assessment sounds right to me. When I implemented certfile/keyfile, I remembered thinking I should trace back where forceAuth comes from to see if anything needed to change, but forgot to actually do it 🤦
There might be a little complexity around fixing this, since the logic to find the registry-scoped auth settings (e.g. _authToken or keyfile) lives inside npm-registry-fetch. So either we'd have to pull that out so that npm-profile can use it, or pass along registry-scoped options in forceAuth and change the logic to detect/use them