Skip to content

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

@jreaves-ui

Description

@jreaves-ui

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions