Skip to content

[Bug]: use_home_npmrc caches auth tokens statically, causing 401 errors when tokens expire #2547

@ravi-pplx

Description

@ravi-pplx

What happened?

Description

When using use_home_npmrc = True with npm_translate_lock, authentication tokens from ~/.npmrc are read once during the module extension evaluation and baked into the generated npm_import_rule repository rules. When these tokens expire (e.g., AWS CodeArtifact tokens expire after 12 hours), Bazel continues using the stale cached tokens, resulting in 401 Unauthorized errors.

Expected Behavior

Similar to how rules_python handles .netrc credentials, authentication tokens should be read fresh from ~/.npmrc on every download attempt, not cached statically. This ensures that when tokens are refreshed in ~/.npmrc, Bazel automatically uses the new credentials without requiring cache invalidation.

Current Behavior

  1. npm_translate_lock reads ~/.npmrc once during module extension evaluation (extensions.bzl:124-132)
  2. Auth tokens are extracted and stored in npm_auth dictionary
  3. These static auth values are passed to npm_import_rule attributes
  4. When tokens expire, the stale tokens remain in Bazel's repository cache
  5. Downloads fail with 401 Unauthorized until bazel clean --expunge is run

Reproduction Steps

  1. Configure a private npm registry that uses short-lived tokens (e.g., AWS CodeArtifact with 12-hour tokens)
  2. Set up ~/.npmrc with fresh auth token:
    //registry.example.com/:_authToken=<TOKEN>
    
  3. Configure npm_translate_lock with use_home_npmrc = True in MODULE.bazel:
    npm.npm_translate_lock(
        name = "npm",
        pnpm_lock = "pnpm-lock.yaml",
        npmrc = ".npmrc",
        use_home_npmrc = True,
    )
  4. Run bazel build //... successfully
  5. Wait for token to expire (or manually expire it)
  6. Refresh token in ~/.npmrc by running auth command
  7. Run bazel build //... again
  8. Result: 401 Unauthorized errors despite having fresh token in ~/.npmrc:
    ERROR: An error occurred during the fetch of repository 'aspect_rules_js++npm+npm__package__1.0.0':
    Error in download: java.io.IOException: Error downloading [https://registry.example.com/package/-/package-1.0.0.tgz] 
    to /path/to/cache/package.tgz: GET returned 401 Unauthorized
    

Workaround

Run bazel clean --expunge after refreshing tokens to clear the repository cache.

Root Cause Analysis

The issue occurs in npm/extensions.bzl:

if attr.use_home_npmrc:
    home_directory = repo_utils.get_home_directory(module_ctx)
    if home_directory:
        home_npmrc_path = "{}/{}".format(home_directory, ".npmrc")
        home_npmrc = parse_npmrc(module_ctx.read(home_npmrc_path))  # ← READ ONCE
        
        (registries2, npm_auth2) = npm_translate_lock_helpers.get_npm_auth(home_npmrc, home_npmrc_path, module_ctx.os.environ)
        registries.update(registries2)
        npm_auth.update(npm_auth2)  # ← STORED STATICALLY

The auth tokens are then passed to npm_import.bzl which uses them in rctx.download():

rctx.download(
    output = _TARBALL_FILENAME,
    url = download_url,
    integrity = rctx.attr.integrity,
    auth = auth,  # ← USES STALE AUTH
    canonical_id = download_url,
)

Comparison with rules_python

rules_python correctly handles this by reading .netrc on every download (python/private/auth.bzl):

def get_auth(ctx, urls, ctx_attr = None):
    """Utility for retrieving netrc-based authentication parameters"""
    # ...
    if ctx_attr.netrc:
        netrc = read_netrc(ctx, ctx_attr.netrc)
    elif "NETRC" in ctx.os.environ:
        netrc = read_netrc(ctx, ctx.getenv("NETRC"))
    else:
        netrc = read_user_netrc(ctx)  # ← READ FRESH EVERY TIME
    
    return use_netrc(netrc, urls, ctx_attr.auth_patterns)

This get_auth() function is called for every rctx.download() call in whl_library.bzl:

result = rctx.download(
    url = urls,
    output = filename,
    sha256 = rctx.attr.sha256,
    auth = get_auth(rctx, urls),  # ← FRESH AUTH EVERY TIME
)

Proposed Solution

Implement a similar pattern to rules_python:

  1. Create an npm_auth.bzl file similar to rules_python/python/private/auth.bzl
  2. Implement get_npm_auth() that reads ~/.npmrc dynamically using rctx.read() on every call
  3. Modify npm_import.bzl to call this function instead of using static auth attributes:
    rctx.download(
        output = _TARBALL_FILENAME,
        url = download_url,
        integrity = rctx.attr.integrity,
        auth = get_npm_auth(rctx, [download_url]),  # ← READ FRESH
        canonical_id = download_url,
    )
  4. Remove the npm_auth, npm_auth_basic, npm_auth_username, npm_auth_password attributes from npm_import_rule as they would no longer be needed

This would ensure credentials are always fresh and match the behavior of standard HTTP tools.

Version

Development (host) and target OS/architectures:

Output of bazel --version:
bazel 8.3.0

Version of the Aspect rules, or other relevant rules from your
WORKSPACE or MODULE.bazel file:
2.6.0

Language(s) and/or frameworks involved:

How to reproduce

Any other information?

No response

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions