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
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions