npm / npm/cli

OTP validator on publish/unpublish/deprecate rejects valid recovery codes — length and pattern enforcement inconsistent

Open
#9,326 2 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
JavaScript
Stars
10.1k
Forks
4.7k
Avg merge
2d 2h
Merged PRs (30d)
19

Description

npm version: 10.9.4
Node version: 22.21.1
Platform: macOS Darwin 25.3.0

Summary

The publish/unpublish/deprecate endpoints' server-side validator for the otp field requires the value to be (a) all digits matching /^\d+$/ AND (b) exactly 64 characters long. This rejects every valid recovery code format that npm itself issues to users (16-char alphanumeric, 48-char hex) but inconsistently accepts 64-char hex strings despite their containing a-f letters.

Reproduction

Account: granular access token in ~/.npmrc, account 2FA enabled with security key only (no TOTP authenticator), org 2FA enforcement on.

# 1. 16-char alphanumeric recovery code (the format npm issues)
npm publish --access public --provenance=false --otp=63a1ae13e3023820
# → 400 Bad Request: "child 'otp' fails because ['otp' with value
#   '63a1ae13e3023820' fails to match the required pattern: /^\d+$/,
#   'otp' length must be 64 characters long]"

# 2. 48-char hex recovery code (also a format some npm accounts get)
npm publish --otp=82fa6fa8d54a0b21c29d57f501aab450a13adf47d4db17e0
# → same 400 with both pattern + length errors

# 3. 64-char hex string — accepted, despite containing a-f (violates /^\d+$/)
npm publish --otp=9fd5371d56172164b950daf1ff84a71a4559a00e832038d09ad25055b26003f0
# → SUCCESS

What's wrong

The error message advertises a /^\d+$/ pattern requirement, but the actual server-side enforcement appears to be:

  • Pattern (/^\d+$/): advisory or unenforced — 64-char hex strings pass
  • Length (== 64): strictly enforced
  • Result: only 64-character strings work, regardless of content

But recovery codes from https://www.npmjs.com/settings/<user>/tfa → Manage Recovery Codes are typically 16 or 48 characters, never 64. TOTP codes are 6 digits. Neither matches the working format.

Impact

Accounts that:

  • Have security-key-only 2FA (no TOTP authenticator), AND
  • Belong to organizations with 2FA enforcement enabled

…cannot publish at all using the recovery codes npm issued them. They're locked out of the documented --otp=<code> path.

Additional CLI quirk

npm unpublish --otp=<value> fails with Usage: npm unpublish [<package-spec>] Options: [--dry-run] [-f|--force] — the --otp flag is not declared in npm unpublish's usage in 10.9.4, but the underlying API call still requires OTP. Workaround:

npm_config_otp=<value> npm unpublish <pkg>@<version> --force

Same broken validator applies once the env-var path bypasses the unrecognized-flag error.

Suggested fix

Either:

  1. Update the server-side validator to accept the recovery-code formats actually issued (16-char alphanumeric, 48-char hex), OR
  2. Update the npm web UI to issue only 64-char digit-only codes that match the validator, AND update the validator error message to remove the misleading /^\d+$/ claim, OR
  3. Document the working OTP format publicly — currently neither npm docs nor the error message reveal that 64 chars is the only working length

Workaround used

Locating an old TOTP shared-secret string (64 hex chars) that happens to satisfy the length check, used in place of a recovery code. This is brittle, undocumented, and degrades over time as TOTP secrets are typically rotated.

Related

Filing companion discussion at npm/feedback for the underlying UI gap that forces affected users into this state in the first place: TOTP authenticator cannot be added on accounts with security-key-only 2FA + org enforcement.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with the publish, unpublish, and deprecate endpoint validation paths and the npm unpublish option handling described here. Reproduce the listed 16-character, 48-character, and 64-character values; done means issued recovery codes are accepted consistently and unpublish exposes the required OTP path.

Written by the indexing model from the issue text.

Assessment

Tech stack
javascript, nodejs
Domain
authentication, cli, security
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
45/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.