NAME

Uniform::HTTP::Auth - Framework-agnostic HTTP authentication for Perl

SYNOPSIS

use Uniform::HTTP::Auth;

my $auth = Uniform::HTTP::Auth->new(
    origin => 'https://example.com:443',
    credentials => {
        username => 'user',
        password => 'secret',
    },
);

my $result = $auth->prepare_authentication(
    challenge_headers => [
        'Digest realm="Members", nonce="abc", qop="auth", algorithm=SHA-256',
        'Basic realm="Members"',
    ],
    method         => 'GET',
    request_target => '/private',
);

# $result->{value} is the complete field value, for example:
# Digest username="user", ...
#
# The caller decides whether it belongs in Authorization or
# Proxy-Authorization and whether the request should be retried.

DESCRIPTION

Uniform::HTTP::Auth implements HTTP authentication mechanics without requiring an HTTP client, server, framework, event loop, request object, or transaction abstraction.

For ordinary applications, construct an auth object with an origin and a set of credentials. The object remembers those credentials and uses them later when that origin presents a supported authentication challenge.

For HTTP libraries or applications with dynamic credential stores, credentials may instead be a callback that receives the authentication context and returns credentials for that protection space.

The distribution implements Basic (RFC 7617), Bearer (RFC 6750), and Digest (RFC 7616). Unknown schemes are parsed and preserved for introspection but are not automatically used in version 0.02.

OWNERSHIP BOUNDARY

This module owns authentication mechanics: challenge parsing, scheme selection, credential lookup, Basic and Bearer construction, Digest calculation, and Digest nonce state.

The calling HTTP implementation owns receiving 401 or 407 responses, request replay, retries, connections, proxy routing, and the choice between Authorization and Proxy-Authorization.

prepare_authentication() performs no network I/O. It only prepares the authentication field value that the caller may use for a subsequent HTTP request.

CONSTRUCTOR

new

my $auth = Uniform::HTTP::Auth->new(
    origin => 'https://example.com:443',
    credentials => {
        username => 'user',
        password => 'secret',
    },
);

Supported options are:

origin

A normalized origin such as https://example.com:443. When static credentials are supplied, origin is required and binds those credentials to that origin. Calls to prepare_authentication() may then omit origin.

A callback-based credential source may omit origin and supply it per prepare_authentication() call instead. If an origin is supplied at construction, it is also treated as a binding and a different per-call origin is rejected.

credentials

Usually a hash reference containing credentials to retain for later use. Basic and Digest use:

credentials => {
    username => 'user',
    password => 'secret',
}

Bearer uses:

credentials => {
    token => $token,
}

A hash may contain both forms. Uniform automatically skips a scheme when the stored credentials do not contain the fields that scheme needs.

For dynamic lookup, credentials may instead be a coderef. See "DYNAMIC CREDENTIAL LOOKUP".

schemes

An array reference containing the enabled authentication schemes in preference order. Scheme names are case-insensitive. The default is:

[qw(digest bearer basic)]

The order is a convenience policy, not a universal security ranking. Supply an explicit order when an application has its own policy.

Unknown constructor options are rejected.

METHODS

schemes

my $schemes = $auth->schemes;

Returns a new array reference containing the configured normalized scheme names.

parse_challenges

my $challenges = $auth->parse_challenges(@header_values);

Parses one or more complete WWW-Authenticate or Proxy-Authenticate field values. Multiple challenges on one field line and multiple field occurrences are supported.

Returns an array reference in wire order. Each element is a plain hash reference with these keys:

{
    scheme    => 'digest',
    raw       => 'Digest realm="Members", ...',
    params    => { realm => 'Members', ... },
    token68   => undef,
    malformed => 0,
    error     => undef,
}

Scheme and parameter names are normalized to lowercase. Unknown schemes are retained. Malformed remote input is returned as data with malformed true and error set; malformed challenge input does not throw merely because it came from the network.

select

my $challenge = $auth->select($challenges);

Selects the best usable challenge according to the configured scheme order. The method does not obtain credentials. It returns undef if no supported well-formed challenge can be used.

When a field contains multiple Digest challenges, Digest-specific algorithm and qop selection remains the responsibility of Uniform::HTTP::Auth::Digest.

prepare_authentication

For an object with a bound origin:

my $result = $auth->prepare_authentication(
    challenge_headers => \@authenticate_values,
    method            => 'GET',
    request_target    => '/private?x=1',
    entity_body       => $body,
);

A callback-based object without a bound origin supplies one per call:

my $result = $auth->prepare_authentication(
    challenge_headers => \@authenticate_values,
    origin            => 'https://example.com:443',
    method            => 'GET',
    request_target    => '/private?x=1',
);

Performs parsing, scheme selection, credential lookup, and authentication value construction. It tries configured schemes in order and can fall through to a later scheme when suitable credentials are unavailable. It does not send a request or otherwise perform network I/O.

method and request_target are required only when Digest is selected. entity_body is used only for Digest qop=auth-int and must be a plain scalar when supplied.

Instead of those three values, a caller may supply any object implementing the Uniform::HTTP::Request contract:

my $result = $auth->prepare_authentication(
    challenge_headers => \@authenticate_values,
    request           => $request,
);

Explicit method, request_target, or entity_body arguments take precedence over values from request. The body is read only when the request reports true from has_buffered_body(); authentication never consumes an incremental body source.

On success, the method returns:

{
    scheme    => 'digest',
    value     => 'Digest username="...", ...',
    challenge => $challenge,
}

value is the complete authentication field value without a header name. Returns undef when no supported challenge can be satisfied.

STATIC CREDENTIALS

Static credentials are the normal application API. They are copied into the auth object at construction and bound to the configured origin.

Username/password credentials can satisfy Basic or Digest challenges:

my $auth = Uniform::HTTP::Auth->new(
    origin => 'https://example.com:443',
    credentials => {
        username => 'user',
        password => 'secret',
    },
);

A token can satisfy Bearer challenges:

my $auth = Uniform::HTTP::Auth->new(
    origin => 'https://api.example.com:443',
    credentials => {
        token => $token,
    },
);

Static credentials are never used for a different origin. To manage many origins with one object, use dynamic credential lookup instead.

DYNAMIC CREDENTIAL LOOKUP

HTTP libraries and applications with credential stores can supply a callback:

my $auth = Uniform::HTTP::Auth->new(
    credentials => sub {
        my ($context) = @_;

        return $store->lookup(
            $context->{origin},
            $context->{realm},
            $context->{scheme},
        );
    },
);

The callback receives one plain hash reference:

{
    scheme    => 'digest',
    origin    => 'https://example.com:443',
    realm     => 'Members',
    challenge => $challenge,
}

Return undef when credentials are unavailable for that protection space. Return a hash reference otherwise.

Basic and Digest expect:

{ username => 'user', password => 'secret' }

Bearer expects:

{ token => 'token-value' }

The callback supplies credentials; it does not verify them. Missing fields or invalid return types are programmer errors and throw exceptions.

ERROR MODEL

Programmer errors, such as invalid constructor options, invalid argument types, or malformed credential callback results, throw exceptions with croak.

Malformed remote challenge data is represented in the returned challenge data and ignored by automatic selection. A credential callback returning undef is not an error.

SECURITY NOTES

Static credentials are bound to one normalized origin. This prevents an auth object created for one service from silently offering those credentials to a different origin.

Basic credentials are only Base64 encoded and should normally be sent over a secure transport such as TLS.

Bearer tokens are treated as opaque credentials. This distribution does not validate JWTs, refresh OAuth tokens, or determine token permissions.

Digest supports legacy MD5 for interoperability as well as SHA-256 and SHA-512/256 families. Applications can restrict the enabled authentication schemes at construction time.

SEE ALSO

Uniform::HTTP::Auth::Basic, Uniform::HTTP::Auth::Bearer, Uniform::HTTP::Auth::Digest, RFC 9110, RFC 7617, RFC 7616, RFC 6750.

The distribution also includes docs/AUTH-SPEC.md with the version 0.02 Auth contract and docs/MESSAGE-SPEC.md with the shared request contract.

AUTHOR

Joshua S. Day, <HAX@cpan.org>

LICENSE

This software is released under the MIT License.