hex_cli_auth (hex_core v0.19.0)
View SourceAuthentication handling with callback functions for build-tool-specific operations.
This module provides generic authentication handling that allows both rebar3 and Elixir Hex (and future build tools) to share the common auth logic while customizing prompting, persistence, and configuration retrieval.
Callbacks
Callbacks are provided via the cli_auth_callbacks key in the config map. All callbacks below are required unless marked optional:
#{
%% Auth configuration for a specific repo
get_auth_config => fun((RepoName :: binary()) ->
#{api_key => binary(),
auth_key => binary(),
oauth_exchange => boolean(),
oauth_exchange_url => binary()} | undefined),
%% Global OAuth tokens - storage and retrieval
get_oauth_tokens => fun(() -> {ok, #{access_token := binary(),
refresh_token => binary(),
expires_at := integer()}} | error),
persist_oauth_tokens => fun((Scope :: global | binary(),
AccessToken :: binary(),
RefreshToken :: binary() | undefined,
ExpiresAt :: integer()) -> ok),
%% Invalidate the stored global OAuth token after it expired and could
%% not be refreshed (optional). Lets the build tool drop the unusable
%% token so concurrent and subsequent callers stop retrying the doomed
%% refresh, and warn the user. Invoked at most once per resolution, while
%% holding the token-refresh lock.
clear_oauth_tokens => fun(() -> ok),
%% User interaction
prompt_otp => fun((Message :: binary()) -> {ok, OtpCode :: binary()} | cancelled),
should_authenticate => fun((Reason :: no_credentials | token_refresh_failed) -> boolean()),
%% OAuth client configuration
get_client_id => fun(() -> binary())
}Auth Resolution Order
For API calls:
- Per-repo
api_keyfrom config (with optional OAuth exchange for hex.pm) - Parent repo
api_key(for "hexpm:org" organizations) - Global OAuth token (refreshed if expired)
- Device auth flow (for write operations only)
For repo calls:
- Per-repo
auth_keywith optional OAuth exchange (default true for hex.pm) - Parent repo
auth_key - Global OAuth token
OAuth Exchange
For hex.pm URLs, api_key and auth_key are exchanged for short-lived OAuth tokens via the client credentials grant. This behavior can be controlled per-repo via the oauth_exchange option in the repo config (defaults to true for hex.pm).
Auth Context
Internally, authentication resolution tracks context via auth_context():
source- Where the credentials came from (env,config, oroauth)has_refresh_token- Whether token refresh is possible on 401
Token Format
OAuth access tokens are automatically prefixed with <<"Bearer ">> when used as api_key or repo_key in the config.
Summary
Functions
Execute a function with API authentication.
Execute a function with API authentication.
Execute a function with repository authentication.
Execute a function with repository authentication.
Types
-type auth_context() :: #{source => env | config | oauth, has_refresh_token => boolean()}.
-type auth_error() :: {auth_error, no_credentials} | {auth_error, auth_declined} | {auth_error, otp_cancelled} | {auth_error, otp_max_retries} | {auth_error, token_refresh_failed} | {auth_error, device_auth_timeout} | {auth_error, device_auth_denied} | {auth_error, oauth_exchange_failed} | {auth_error, term()}.
-type auth_prompt_reason() :: no_credentials | token_refresh_failed.
-type callbacks() :: #{get_auth_config := fun((RepoName :: binary()) -> repo_auth_config() | undefined), get_oauth_tokens := fun(() -> {ok, oauth_tokens()} | error), persist_oauth_tokens := fun((Scope :: global | binary(), AccessToken :: binary(), RefreshToken :: binary() | undefined, ExpiresAt :: integer()) -> ok), clear_oauth_tokens => fun(() -> ok), prompt_otp := fun((Message :: binary()) -> {ok, OtpCode :: binary()} | cancelled), should_authenticate := fun((Reason :: auth_prompt_reason()) -> boolean()), get_client_id := fun(() -> binary())}.
-type permission() :: read | write.
-type repo_auth_config() :: #{api_key => binary(), repo_key => binary(), auth_key => binary(), oauth_token => oauth_tokens()}.
Functions
-spec with_api(permission(), hex_core:config(), fun((hex_core:config()) -> Result)) -> Result | {error, auth_error()} when Result :: term().
Execute a function with API authentication.
Equivalent to with_api(Permission, Config, Fun, []).
See also: with_api/4.
-spec with_api(permission(), hex_core:config(), fun((hex_core:config()) -> Result), opts()) -> Result | {error, auth_error()} when Result :: term().
Execute a function with API authentication.
Resolves credentials in this order:
- Per-repo
api_keyfrom config (with optional OAuth exchange for hex.pm) - Parent repo
api_key(for "hexpm:org" organizations) - Global OAuth token (refreshed if expired)
- Device auth flow (when
should_authenticatecallback returns true)
On 401 responses, handles OTP prompts and token refresh automatically.
The repository name is taken from the config (repo_name or repo_organization).
Callbacks are taken from the cli_auth_callbacks key in the config map.
Options:
optional- Whentrue, if no credentials are found, executes the function without authentication first. If the server returns 401, triggers auth (respectingauth_inline). Whenfalse(default), missing credentials immediately triggers theshould_authenticatecallback.auth_inline- Whentrue(default), prompts the user viashould_authenticatecallback when authentication is needed. Whenfalse, returns{error, {auth_error, no_credentials}}instead of prompting.oauth_open_browser- Whentrue(default), automatically opens the browser during device auth flow. Whenfalse, only prints the URL for the user.
Example:
hex_cli_auth:with_api(write, Config, fun(C) ->
hex_api_release:publish(C, Tarball)
end, [{optional, false}, {auth_inline, true}]).
-spec with_repo(hex_core:config(), fun((hex_core:config()) -> Result)) -> Result | {error, auth_error()} when Result :: term().
Execute a function with repository authentication.
Equivalent to with_repo(Config, Fun, []).
See also: with_repo/3.
-spec with_repo(hex_core:config(), fun((hex_core:config()) -> Result), opts()) -> Result | {error, auth_error()} when Result :: term().
Execute a function with repository authentication.
Resolves credentials in this order:
repo_keyin config - passthroughrepo_keyfromget_auth_configcallback - passthroughauth_keyfromget_auth_configwhentrustedis true andoauth_exchangeis true - exchange for OAuth tokenauth_keyfromget_auth_configwhentrustedis true - use directly- Global OAuth token from
get_oauth_tokenscallback - No auth when
optionalis true (with retry on 401) - Prompt via
should_authenticatewhenauth_inlineis true
The repository name is taken from the config (repo_name or repo_organization).
Callbacks are taken from the cli_auth_callbacks key in the config map.
Options:
optional- Whentrue(default), proceeds without auth if none found; retries with auth on 401.auth_inline- Whentrue, prompts user viashould_authenticatecallback. Default isfalse.oauth_open_browser- Whentrue(default), automatically opens the browser during device auth flow. Whenfalse, only prints the URL for the user.
Example:
hex_cli_auth:with_repo(Config, fun(C) ->
hex_repo:get_tarball(C, <<"ecto">>, <<"3.0.0">>)
end).