Repository: pypa/pip

Issue:
Authentication prioritize .netrc credentials over ones from url.
### Description

I have private repository in Google Artifacts Registry.
I am able to install a package using following command without the issue:
```
pip install simple-package -i "https://oauth2accesstoken:$(gcloud auth print-access-token)@us-python.pkg.dev/$PROJECT/my-pypi-repo/simple"
```

Once I create `.netrc` file with another token (for example an invalid one) and run the command again pip is able to find tar.gz link using credentials from url, but it uses ~/.netrc credentials to download it (in this example since token is invalid it fails).

Anonymized -vvv output of installation attempt is in Output section.



### Expected behavior

Credentials in url should be preffered over .netrc ones.

### pip version

22.0.4

### Python version

3.7.1

### OS

Linux

### How to Reproduce

1. Create repository with required authentication and add package into it
2. Run `pip install --no-input --no-cache-dir --index-url "https://mylogin:realpassword@repo.url my-package`
3. Watch it installs successfully.
4. Uninstall the package `pip uninstall my-package`
5. Create `~/.netrc` file with following content: `machine repo.url login mylogin password fakepassword
6. Run `pip install --no-input --no-cache-dir --index-url "https://mylogin:realpassword@repo.url my-package` and see it fails


### Output

```sh-session
$ pip install  simple-package --no-input -vvv -i "https://oauth2accesstoken:$(gcloud auth print-access-token)@us-python.pkg.dev/$PROJECT/my-pypi-repo/simple"
Using pip 22.0.4 from $HOME/.pyenv/versions/3.7.1/envs/workspace3.9/lib/python3.7/site-packages/pip (python 3.7)
Non-user install because user site-packages disabled
Created temporary directory: /tmp/pip-ephem-wheel-cache-qpruibfz
Created temporary directory: /tmp/pip-req-tracker-4_0o7qbx
Initialized build tracking at /tmp/pip-req-tracker-4_0o7qbx
Created build tracker: /tmp/pip-req-tracker-4_0o7qbx
Entered build tracker: /tmp/pip-req-tracker-4_0o7qbx
Created temporary directory: /tmp/pip-install-yd20cgdf
Looking in indexes: https://oauth2accesstoken:****@us-python.pkg.dev/$PROJECT/my-pypi-repo/simple
1 location(s) to search for versions of simple-package:
* https://oauth2accesstoken:****@us-python.pkg.dev/$PROJECT/my-pypi-repo/simple/simple-package/
Fetching project page and analyzing links: https://oauth2accesstoken:****@us-python.pkg.dev/$PROJECT/my-pypi-repo/simple/simple-package/
Getting page https://oauth2accesstoken:****@us-python.pkg.dev/$PROJECT/my-pypi-repo/simple/simple-package/
Found credentials in url for us-python.pkg.dev
Looking up "https://us-python.pkg.dev/$PROJECT/my-pypi-repo/simple/simple-package/" in the cache
Request header has "max_age" as 0, cache bypassed
Starting new HTTPS connection (1): us-python.pkg.dev:443
https://us-python.pkg.dev:443 "GET /$PROJECT/my-pypi-repo/simple/simple-package/ HTTP/1.1" 200 242
Updating cache with response from "https://us-python.pkg.dev/$PROJECT/my-pypi-repo/simple/simple-package/"
  Found link https://us-python.pkg.dev/$PROJECT/my-pypi-repo/simple-package/simple_package-0.1.tar.gz (from https://us-python.pkg.dev/$PROJECT/my-pypi-repo/simple/simple-package/), version: 0.1
Skipping link: not a file: https://oauth2accesstoken:****@us-python.pkg.dev/$PROJECT/my-pypi-repo/simple/simple-package/
Given no hashes to check 1 links for project 'simple-package': discarding no candidates
Collecting simple_package
  Created temporary directory: /tmp/pip-unpack-h_1_vpv2
  Found credentials in netrc for us-python.pkg.dev
  Looking up "https://us-python.pkg.dev/$PROJECT/my-pypi-repo/simple-package/simple_package-0.1.tar.gz" in the cache
  No cache entry available
  https://us-python.pkg.dev:443 "GET /$PROJECT/my-pypi-repo/simple-package/simple_package-0.1.tar.gz HTTP/1.1" 401 60
  ERROR: HTTP error 401 while getting https://us-python.pkg.dev/$PROJECT/my-pypi-repo/simple-package/simple_package-0.1.tar.gz (from https://us-python.pkg.dev/$PROJECT/my-pypi-repo/simple/simple-package/)
ERROR: Could not install requirement simple_package from https://us-python.pkg.dev/$PROJECT/my-pypi-repo/simple-package/simple_package-0.1.tar.gz because of HTTP error 401 Client Error: Unauthorized for url: https://us-python.pkg.dev/$PROJECT/my-pypi-repo/simple-package/simple_package-0.1.tar.gz for URL https://us-python.pkg.dev/$PROJECT/my-pypi-repo/simple-package/simple_package-0.1.tar.gz (from https://us-python.pkg.dev/$PROJECT/my-pypi-repo/simple/simple-package/)
Exception information:
Traceback (most recent call last):
  File "$HOME/.pyenv/versions/3.7.1/envs/workspace3.9/lib/python3.7/site-packages/pip/_internal/operations/prepare.py", line 538, in _prepare_linked_requirement
    hashes,
  File "$HOME/.pyenv/versions/3.7.1/envs/workspace3.9/lib/python3.7/site-packages/pip/_internal/operations/prepare.py", line 218, in unpack_url
    hashes=hashes,
  File "$HOME/.pyenv/versions/3.7.1/envs/workspace3.9/lib/python3.7/site-packages/pip/_internal/operations/prepare.py", line 94, in get_http_url
    from_path, content_type = download(link, temp_dir.path)
  File "$HOME/.pyenv/versions/3.7.1/envs/workspace3.9/lib/python3.7/site-packages/pip/_internal/network/download.py", line 133, in __call__
    resp = _http_get_download(self._session, link)
  File "$HOME/.pyenv/versions/3.7.1/envs/workspace3.9/lib/python3.7/site-packages/pip/_internal/network/download.py", line 117, in _http_get_download
    raise_for_status(resp)
  File "$HOME/.pyenv/versions/3.7.1/envs/workspace3.9/lib/python3.7/site-packages/pip/_internal/network/utils.py", line 54, in raise_for_status
    raise NetworkConnectionError(http_error_msg, response=resp)
pip._internal.exceptions.NetworkConnectionError: 401 Client Error: Unauthorized for url: https://us-python.pkg.dev/$PROJECT/my-pypi-repo/simple-package/simple_package-0.1.tar.gz

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "$HOME/.pyenv/versions/3.7.1/envs/workspace3.9/lib/python3.7/site-packages/pip/_internal/cli/base_command.py", line 167, in exc_logging_wrapper
    status = run_func(*args)
  File "$HOME/.pyenv/versions/3.7.1/envs/workspace3.9/lib/python3.7/site-packages/pip/_internal/cli/req_command.py", line 205, in wrapper
    return func(self, options, args)
  File "$HOME/.pyenv/versions/3.7.1/envs/workspace3.9/lib/python3.7/site-packages/pip/_internal/commands/install.py", line 340, in run
    reqs, check_supported_wheels=not options.target_dir
  File "$HOME/.pyenv/versions/3.7.1/envs/workspace3.9/lib/python3.7/site-packages/pip/_internal/resolution/resolvelib/resolver.py", line 95, in resolve
    collected.requirements, max_rounds=try_to_avoid_resolution_too_deep
  File "$HOME/.pyenv/versions/3.7.1/envs/workspace3.9/lib/python3.7/site-packages/pip/_vendor/resolvelib/resolvers.py", line 481, in resolve
    state = resolution.resolve(requirements, max_rounds=max_rounds)
  File "$HOME/.pyenv/versions/3.7.1/envs/workspace3.9/lib/python3.7/site-packages/pip/_vendor/resolvelib/resolvers.py", line 348, in resolve
    self._add_to_criteria(self.state.criteria, r, parent=None)
  File "$HOME/.pyenv/versions/3.7.1/envs/workspace3.9/lib/python3.7/site-packages/pip/_vendor/resolvelib/resolvers.py", line 172, in _add_to_criteria
    if not criterion.candidates:
  File "$HOME/.pyenv/versions/3.7.1/envs/workspace3.9/lib/python3.7/site-packages/pip/_vendor/resolvelib/structs.py", line 151, in __bool__
    return bool(self._sequence)
  File "$HOME/.pyenv/versions/3.7.1/envs/workspace3.9/lib/python3.7/site-packages/pip/_internal/resolution/resolvelib/found_candidates.py", line 155, in __bool__
    return any(self)
  File "$HOME/.pyenv/versions/3.7.1/envs/workspace3.9/lib/python3.7/site-packages/pip/_internal/resolution/resolvelib/found_candidates.py", line 143, in <genexpr>
    return (c for c in iterator if id(c) not in self._incompatible_ids)
  File "$HOME/.pyenv/versions/3.7.1/envs/workspace3.9/lib/python3.7/site-packages/pip/_internal/resolution/resolvelib/found_candidates.py", line 47, in _iter_built
    candidate = func()
  File "$HOME/.pyenv/versions/3.7.1/envs/workspace3.9/lib/python3.7/site-packages/pip/_internal/resolution/resolvelib/factory.py", line 220, in _make_candidate_from_link
    version=version,
  File "$HOME/.pyenv/versions/3.7.1/envs/workspace3.9/lib/python3.7/site-packages/pip/_internal/resolution/resolvelib/candidates.py", line 294, in __init__
    version=version,
  File "$HOME/.pyenv/versions/3.7.1/envs/workspace3.9/lib/python3.7/site-packages/pip/_internal/resolution/resolvelib/candidates.py", line 158, in __init__
    self.dist = self._prepare()
  File "$HOME/.pyenv/versions/3.7.1/envs/workspace3.9/lib/python3.7/site-packages/pip/_internal/resolution/resolvelib/candidates.py", line 227, in _prepare
    dist = self._prepare_distribution()
  File "$HOME/.pyenv/versions/3.7.1/envs/workspace3.9/lib/python3.7/site-packages/pip/_internal/resolution/resolvelib/candidates.py", line 299, in _prepare_distribution
    return preparer.prepare_linked_requirement(self._ireq, parallel_builds=True)
  File "$HOME/.pyenv/versions/3.7.1/envs/workspace3.9/lib/python3.7/site-packages/pip/_internal/operations/prepare.py", line 487, in prepare_linked_requirement
    return self._prepare_linked_requirement(req, parallel_builds)
  File "$HOME/.pyenv/versions/3.7.1/envs/workspace3.9/lib/python3.7/site-packages/pip/_internal/operations/prepare.py", line 543, in _prepare_linked_requirement
    "error {} for URL {}".format(req, exc, link)
pip._internal.exceptions.InstallationError: Could not install requirement simple_package from https://us-python.pkg.dev/$PROJECT/my-pypi-repo/simple-package/simple_package-0.1.tar.gz because of HTTP error 401 Client Error: Unauthorized for url: https://us-python.pkg.dev/$PROJECT/my-pypi-repo/simple-package/simple_package-0.1.tar.gz for URL https://us-python.pkg.dev/$PROJECT/my-pypi-repo/simple-package/simple_package-0.1.tar.gz (from https://us-python.pkg.dev/$PROJECT/my-pypi-repo/simple/simple-package/)
Removed build tracker: '/tmp/pip-req-tracker-4_0o7qbx'
```


### Code of Conduct

- [X] I agree to follow the [PSF Code of Conduct](https://www.python.org/psf/conduct/).


Discussion:
Are you sure this is a Pip issue and not a requests issue? What behavior does requests have for the same thing?

Mostly Pip is just delegating to requests on how to handle this.
> ```
> pip._internal.exceptions.NetworkConnectionError: 401 Client Error: Unauthorized for url: https://us-python.pkg.dev/$PROJECT/my-pypi-repo/simple-package/simple_package-0.1.tar.gz
> ```

This is the error: Your server is giving you a 401 Client Error on this. You'll have to investigate what that is.

I'll bundle improving this error message into #10421.
@pradyunsg I think you are misunderstanding the issue reported here.

The credentials are wrong because they are the ones read out of the .netrc file not the ones read out of the URL.

But I believe this behavior comes from requests not pip. And as such there are two things I can suggest:

1. Improve the specificity of the netrc file to only the hosts which require the credentials
2. Set the NETRC env variable to /dev/null or a separate NETRC file with the correct credentials when running pip commands, assuming you are using pip that has vendored requests 2.25.0 or higher

(I see a similar issue quite a lot in my corporate environment)
Indeed, and thanks for catching that @notatallshaw! :)
I have deliberately put an incorrect password in .netrc file to trigger a 401 error to illustrate that netrc credentials are used instead of ones in url.

Thank you for providing workarounds, but the issue appears in [environment](https://cloud.google.com/functions/docs/writing/specifying-dependencies-python#using_private_dependencies) where netrc is generated automatically if user don't provide one and user does not have control over env, so they need to remember to generate .netrc to use desired token instead of generating requirements.txt file.
Workaround exists, but it would be appreciated if pip behaves as documented: https://pip.pypa.io/en/latest/topics/authentication/#netrc-support

You can see in the output of the command there are 2 requests:
```
Starting new HTTPS connection (1): us-python.pkg.dev:443
https://us-python.pkg.dev:443 "GET /$PROJECT/my-pypi-repo/simple/simple-package/ HTTP/1.1" 200 242
```
and
```
https://us-python.pkg.dev:443 "GET /$PROJECT/my-pypi-repo/simple-package/simple_package-0.1.tar.gz HTTP/1.1" 401 60
  ERROR: HTTP error 401 while getting https://us-python.pkg.dev/$PROJECT/my-pypi-repo/simple-package/simple_package-0.1.tar.gz (from https://us-python.pkg.dev/$PROJECT/my-pypi-repo/simple/simple-package/)
```

As you see the first one was successful which means it used credentials from url correctly, while the second one failed so it used netrc ones. This shows that pip is sending these requests in different ways, hence I believe it is pip and not requests issue.
Are there any other tools that override explicitly provided credentials with netrc credentials?
> As you see the first one was successful which means it used credentials from url correctly, while the second one failed so it used netrc ones. This shows that pip is sending these requests in different ways, hence I believe it is pip and not requests issue.

You could be correct but I'm not sure based on the information you've provided so far, is still seems speculative.

Poking around in Pips code for netrc I could only find 1 location that is a possible culprit: `_get_new_credentials` (which is called by `_get_url_and_credentials` which is called by `__call__` which is used to establish auth for a network session): https://github.com/pypa/pip/blob/22.0.4/src/pip/_internal/network/auth.py#L109

But `_get_new_credentials` gets the credentials in the order you would expect, url scheme is first and netrc is much later.
OK, I just re-read this issue and... I completely misread this the last two times. Sincere apologies to @kramarz and @notatallshaw.

This is definitely a bug at some point in our network authentication handling code.

Resolving patch:
diff --git a/src/pip/_internal/network/auth.py b/src/pip/_internal/network/auth.py
--- a/src/pip/_internal/network/auth.py
+++ b/src/pip/_internal/network/auth.py
@@ -109,7 +109,8 @@ def _get_index_url(self, url: str) -> Optional[str]:
     def _get_new_credentials(
         self,
         original_url: str,
-        allow_netrc: bool = True,
+        *,
+        allow_netrc: bool = False,
         allow_keyring: bool = False,
     ) -> AuthInfo:
         """Find and return credentials for the specified URL."""
@@ -260,7 +261,7 @@ def handle_401(self, resp: Response, **kwargs: Any) -> Response:
         # Query the keyring for credentials:
         username, password = self._get_new_credentials(
             resp.url,
-            allow_netrc=False,
+            allow_netrc=True,
             allow_keyring=True,
         )
 


Test patch:
diff --git a/tests/functional/test_install_config.py b/tests/functional/test_install_config.py
--- a/tests/functional/test_install_config.py
+++ b/tests/functional/test_install_config.py
@@ -415,3 +415,46 @@ def get_credential(url, username):
         assert "get_credential was called" in result.stderr
     else:
         assert "get_credential was called" not in result.stderr
+
+
+def test_prioritize_url_credentials_over_netrc(
+    script: PipTestEnvironment,
+    data: TestData,
+    cert_factory: CertFactory,
+) -> None:
+    cert_path = cert_factory()
+    ctx = ssl.SSLContext(ssl.PROTOCOL_SSLv23)
+    ctx.load_cert_chain(cert_path, cert_path)
+    ctx.load_verify_locations(cafile=cert_path)
+    ctx.verify_mode = ssl.CERT_REQUIRED
+
+    server = make_mock_server(ssl_context=ctx)
+    server.mock.side_effect = [
+        package_page(
+            {
+                "simple-3.0.tar.gz": "/files/simple-3.0.tar.gz",
+            }
+        ),
+        authorization_response(str(data.packages / "simple-3.0.tar.gz")),
+    ]
+
+    url = f"https://USERNAME:PASSWORD@{server.host}:{server.port}/simple"
+
+    netrc = script.scratch_path / ".netrc"
+    netrc.write_text(
+        f"machine {server.host} login wrongusername password wrongpassword"
+    )
+    with server_running(server):
+        script.environ["NETRC"] = netrc
+        script.pip(
+            "install",
+            "--no-cache-dir",
+            "--index-url",
+            url,
+            "--cert",
+            cert_path,
+            "--client-cert",
+            cert_path,
+            "simple",
+        )
+        script.assert_installed(simple="3.0")