cli icon indicating copy to clipboard operation
cli copied to clipboard

[BUG] "npm login" does not work when registry uses ssl client authentication (mTLS)

Open eXory2024 opened this issue 5 months ago • 5 comments

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.

eXory2024 avatar Jul 29 '25 14:07 eXory2024

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 }

eXory2024 avatar Jul 29 '25 14:07 eXory2024

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

eXory2024 avatar Jul 29 '25 14:07 eXory2024

could it be because of the user name is undefined here ? https://my-private-registry.com/-/user/org.couchdb.user:exory2024/-rev/undefined

milaninfy avatar Jul 29 '25 15:07 milaninfy

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 avatar Jul 29 '25 15:07 eXory2024

@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

jenseng avatar Sep 19 '25 15:09 jenseng