Skip to content

[Bug]: OAuth token for hex.pm is attached to custom repositories, suppressing their netrc/per-repo auth (401 on mix deps.get) #1216

Description

@paulnicholson

Product or component

Hex CLI

Summary

When a user is authenticated to hex.pm (mix hex.user auth), Hex sends that hex.pm OAuth token — as Authorization: Bearer <token> — to requests for custom repositories, even ones with no configured auth_key that rely on ~/.netrc for HTTP Basic auth. This has two effects:

  1. The hex.pm Bearer token is sent to a server it means nothing to, which rejects it.
  2. Because an Authorization header is now present, Hex.HTTP.add_basic_auth_via_netrc/2 no-ops, so the repo's ~/.netrc credentials are never applied.

Net result: mix deps.get (and mix hex.repo add --fetch-public-key) fail with 401 against a custom repo that authenticates fine outside Hex. mix hex.user deauth fixes it immediately.

Regression introduced in 2.5.0 (see root cause). Works on 2.4.2.

What happened?

401.

What did you expect?

netrc credentials are used for myrepo; fetch succeeds.

Steps to reproduce

  1. Add a custom repo whose server requires HTTP Basic auth, with credentials only in ~/.netrc:
    machine myrepo.example.com
      login someuser
      password somepass
    
    mix hex.repo add myrepo https://myrepo.example.com --public-key ./key.pem
  2. mix hex.user auth
  3. mix deps.get for a project depending on a package from myrepo.

Environment / context

  • Hex 2.5.1, Elixir 1.19.5 / Erlang-OTP 28
  • Custom repo behind a CDN requiring HTTP Basic auth; credentials only in ~/.netrc; added with no auth_key.

Logs / screenshots

No response

Additional context

Root cause

In 2.5.0, Hex.Repo.get_package/3 began wrapping the hex_core call in Hex.Auth.with_repo/3. Verbatim:

# 2.4.2 and earlier — no auth wrapper
def get_package(repo, package, etag) do
  repo_config = get_repo(repo)
  config = build_hex_core_config(repo_config, repo, etag)
  :mix_hex_repo.get_package(config, package)
end

# 2.5.0+ — wrapped
def get_package(repo, package, etag) do
  repo_config = get_repo(repo)
  config = build_hex_core_config(repo_config, repo, etag)
  Hex.Auth.with_repo(config, &:mix_hex_repo.get_package(&1, package), optional: true)
end

Hex.Auth.with_repo:mix_hex_cli_auth.with_repo uses Hex.Auth.get_oauth_tokens/0, which reads the global token (Hex.State.get(:oauth_token), set by mix hex.user auth) and attaches it as Authorization: Bearer <token> to the request — regardless of the target repo. The repo in question has auth_key: nil and oauth_exchange: false, so it should not be receiving hexpm credentials.

The netrc step only runs when no Authorization header is present (lib/hex/http.ex):

defp add_basic_auth_via_netrc(%{"authorization" => _} = headers, _url), do: headers

So the Bearer header both fails auth and blocks the netrc fallback.

Evidence

Same custom-repo URL, same session:

Condition Result
Hex.HTTP.request with no authorization header 200 (netrc applied)
Hex.HTTP.request with an authorization header 401 (netrc skipped)
Real get_package while logged into hex.pm 401
Real get_package after mix hex.user deauth 200

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