aspect-build / aspect-build/rules_js

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

Open
#2,547 2 comments 1 reaction 0 assignees View on GitHub
bug
Dominant language
Starlark
Stars
378
Forks
183
Avg merge
1d 9h
Merged PRs (30d)
32

Description

### 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](https://github.com/aspect-build/rules_js/blob/main/npm/extensions.bzl#L124-L132))
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=
```
3. Configure `npm_translate_lock` with `use_home_npmrc = True` in MODULE.bazel:
```python
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`:

```python
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()`:

```python
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](https://github.com/bazelbuild/rules_python/blob/main/python/private/auth.bzl)):

```python
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`:

```python
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:
```python
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

```shell

```

### Any other information?

_No response_

Contributor guide

Open the contributing guide

Research direction

Start with npm/extensions.bzl lines 124-132 to trace how ~/.npmrc is read, then follow the auth attributes into npm_import.bzl and its rctx.download call. Compare the fresh-auth pattern in rules_python/python/private/auth.bzl. Done means refreshed tokens in ~/.npmrc are used on subsequent downloads without requiring bazel clean --expunge, while the reproduction build succeeds after token refresh.

Written by the indexing model from the issue text.

Assessment

Tech stack
javascript, node.js
Domain
build-system, tooling
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Clearly specified
Newbie friendliness
48/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.