NAME

Net::Nostr::HttpAuth - NIP-98 HTTP auth

SYNOPSIS

use Net::Nostr::HttpAuth qw(
    create_auth_event create_auth_header
    parse_auth_header validate_auth_event
);

# Client: create an Authorization header for a GET request
my $header = create_auth_header(
    key    => $key,
    url    => 'https://api.example.com/data',
    method => 'GET',
);
# "Nostr eyJpZCI6Ii..."

# Client: POST with payload hash
my $body = '{"name":"test"}';
my $header = create_auth_header(
    key     => $key,
    url     => 'https://api.example.com/upload',
    method  => 'POST',
    payload => $body,
);

# Server: parse and validate
my $event = parse_auth_header($header);
validate_auth_event($event,
    url    => 'https://api.example.com/data',
    method => 'GET',
);

DESCRIPTION

Implements NIP-98 HTTP auth. Uses kind 27235 ephemeral nostr events to authorize HTTP requests. The client signs an event containing the request URL and method, base64-encodes it, and sends it in the Authorization HTTP header with the Nostr scheme. The server decodes the event and validates the kind, timestamp, URL, and method.

When the request has a body (POST, PUT, PATCH), the client SHOULD include a payload tag containing the SHA-256 hex hash of the body. The server MAY check this tag to verify the body is authorized.

FUNCTIONS

All functions are exportable. None are exported by default.

create_auth_event

my $event = create_auth_event(
    pubkey     => $hex_pubkey,
    url        => $absolute_url,
    method     => $http_method,
    payload    => $body,        # optional
    created_at => time(),       # optional, defaults to now
);

Creates a kind 27235 Net::Nostr::Event with u and method tags. If payload is provided, adds a payload tag with the SHA-256 hex hash of the body. The event content is always empty. Croaks if pubkey, url, or method is missing. Croaks if pubkey is not 64-character lowercase hex.

The returned event is unsigned. Use "sign_event" in Net::Nostr::Key to sign it before encoding.

my $event = create_auth_event(
    pubkey => 'aa' x 32,
    url    => 'https://api.example.com/data',
    method => 'GET',
);

create_auth_header

my $header = create_auth_header(
    key        => $nostr_key,
    url        => $absolute_url,
    method     => $http_method,
    payload    => $body,           # optional
    created_at => time(),          # optional, defaults to now
);

Creates, signs, and base64-encodes a kind 27235 event, returning a complete Authorization header value in the format Nostr <base64>. The key must be a Net::Nostr::Key object with a private key for signing. Croaks if key, url, or method is missing.

my $header = create_auth_header(
    key    => $key,
    url    => 'https://api.example.com/data',
    method => 'GET',
);
# "Nostr eyJpZCI6Ii..."

parse_auth_header

my $event = parse_auth_header($header_value);

Parses an Authorization header value. Validates the Nostr scheme, decodes the base64 payload, and parses the JSON into a Net::Nostr::Event using "from_wire" in Net::Nostr::Event. Croaks if the header is missing, uses the wrong scheme, or contains invalid base64 or JSON.

my $event = parse_auth_header('Nostr eyJpZCI6Ii...');

validate_auth_event

validate_auth_event($event,
    url         => $absolute_url,
    method      => $http_method,
    payload     => $body,         # optional
    time_window => 60,            # optional, seconds, default 60
);

Performs the server-side validation checks specified by NIP-98:

0. The event ID and Schnorr signature are cryptographically verified via "validate" in Net::Nostr::Event. This ensures the event was actually signed by the claimed pubkey and has not been tampered with.
1. The kind MUST be 27235.
2. The created_at timestamp MUST be within time_window seconds of the current time (default 60 seconds).
3. The u tag MUST exactly match the request URL (including query parameters).
4. The method tag MUST match the HTTP method used.

If payload is provided and the event has a payload tag, the tag value is checked against the SHA-256 hex hash of the body. If the server does not provide payload, the tag is not checked.

Returns 1 on success. Croaks with a descriptive message on failure. Servers SHOULD respond with HTTP 401 when validation fails.

eval { validate_auth_event($event, url => $url, method => 'GET') };
if ($@) {
    # respond with 401 Unauthorized
}

SEE ALSO

NIP-98, Net::Nostr, Net::Nostr::Event, Net::Nostr::Key