NAME

Web::Authn - Server-side WebAuthn / passkeys

SYNOPSIS

use Web::Authn qw(
    generate_registration_options
    verify_registration_response
    generate_authentication_options
    verify_authentication_response
    options_to_json
    base64url_to_bytes
);

my $opts = generate_registration_options(
    rp_id     => 'example.com',
    rp_name   => 'Example Co',
    user_name => 'bob',
);
my $json = options_to_json($opts);   # send to the browser

my $reg = verify_registration_response(
    credential          => $browser_json,          # JSON or hash
    expected_challenge  => $opts->{challenge},
    expected_rp_id      => 'example.com',
    expected_origin     => 'https://example.com',
);
# persist $reg->{credential_id}, $reg->{credential_public_key}, $reg->{sign_count}

my $assert_opts = generate_authentication_options(
    rp_id => 'example.com',
    allow_credentials => [
        { type => 'public-key', id => $reg->{credential_id}, _bytes => 1 },
    ],
);

my $ok = verify_authentication_response(
    credential                   => $browser_json,
    expected_challenge           => $assert_opts->{challenge},
    expected_rp_id               => 'example.com',
    expected_origin              => 'https://example.com',
    credential_public_key        => $reg->{credential_public_key},
    credential_current_sign_count => $reg->{sign_count},
);

Or, using the OO interface:

my $authn = Web::Authn->new(
    rp_id           => 'example.com',
    rp_name         => 'Example Co',
    expected_origin => 'https://example.com',
);

my $opts = $authn->generate_registration_options( user_name => 'bob' ) ||
    die( $authn->error );

my $reg = $authn->verify_registration_response(
    credential         => $browser_json,
    expected_challenge => $opts->{challenge},
) || die( $authn->error );

$authn->fatal(1);   # subsequent errors die() as exception objects

VERSION

v0.2.0

DESCRIPTION

Web::Authn is a Perl port of Duo Labs' py_webauthn. It implements the Relying Party operations of the W3C WebAuthn Level 2/3 specifications: option generation and cryptographic verification of registration and authentication ceremonies.

Methods return, undef in scalar context or an empty list in list context, on failure and store a Web::Authn::Exception on the object and in Web::Authn->error. They die only when "fatal" is true.

Functional wrappers remain exported on request and dispatch to a temporary object.

See Web::Authn::Cookbook for integration.

It is not a user/session framework. You store challenges, credential IDs, COSE public keys and signature counters yourself.

The usual integration is four JSON endpoints: register/begin, register/complete, login/begin, login/complete. See the distribution README.md for a full walk-through, storage schema, and frontend notes.

EXPORTS

Nothing is exported by default. Request what you need:

use Web::Authn qw(
    generate_registration_options
    verify_registration_response
    generate_authentication_options
    verify_authentication_response
    options_to_json
    options_to_json_dict
    base64url_to_bytes
    bytes_to_base64url
    generate_challenge
    generate_user_handle
);

METHODS

All generation/verification functions take a flat named-parameter hash.

generate_registration_options(%args)

Returns a Perl hash describing PublicKeyCredentialCreationOptions. Pass it through "options_to_json" before sending it to the browser.

Required: rp_id, rp_name, user_name.

Optional:

Dies via Carp if rp_id, rp_name, or user_name is empty.

generate_authentication_options(%args)

Returns PublicKeyCredentialRequestOptions.

Required: rp_id.

Optional: challenge, timeout (default 60000), allow_credentials (array of descriptors; omit or [] for usernameless / discoverable credentials), user_verification (default preferred).

options_to_json($options)

Serialises registration or authentication options to a JSON string suitable for Content-Type: application/json. Byte fields (challenge, user.id, descriptor id) become unpadded base64url. Keys are camelCase as the WebAuthn spec uses them.

options_to_json_dict($options)

Same conversion, but returns a hash instead of a JSON string.

verify_registration_response(%args)

Implements WebAuthn §7.1. credential may be a JSON string or a hash in the SimpleWebAuthn / py_webauthn shape (id, rawId, type, response.clientDataJSON, response.attestationObject as base64url).

Required: credential, expected_challenge (raw bytes you issued), expected_rp_id, expected_origin (full origin string, or an array of allowed origins).

Optional: require_user_presence (default true), require_user_verification (default false), supported_pub_key_algs, pem_root_certs_bytes_by_fmt (hash of attestation format name to array of PEM/DER root certificates).

Returns a hash:

credential_id            raw bytes
credential_public_key    COSE_Key bytes (store as-is)
sign_count               integer
aaguid                   UUID string
fmt                      attestation format
credential_type          "public-key"
user_verified            0/1
attestation_object       raw bytes
credential_device_type   "singleDevice" | "multiDevice"
credential_backed_up     0/1

Dies with Web::Authn::Exception::InvalidRegistration on any check failure.

verify_authentication_response(%args)

Implements WebAuthn §7.2.

Required: credential, expected_challenge, expected_rp_id, expected_origin, credential_public_key (COSE bytes stored at registration), credential_current_sign_count.

Optional: require_user_verification (default false).

Returns:

credential_id            raw bytes
new_sign_count           persist this
credential_device_type
credential_backed_up
user_verified
user_handle              bytes or undef

The signature counter must strictly increase when either the stored or reported count is non-zero; otherwise the response is rejected as a possible cloned authenticator.

Dies with Web::Authn::Exception::InvalidAuthentication.

base64url_to_bytes($str)

bytes_to_base64url($bytes)

Unpadded base64url, as WebAuthn uses it.

generate_challenge($len)

CSPRNG bytes via Bytes::Random::Secure. $len defaults to 64 (WebAuthn only requires 16).

generate_user_handle()

64 CSPRNG bytes for PublicKeyCredentialUserEntity.id.

error

my $authn = Web::Authn->new( %bad_args );
if( !defined( $authn ) )
{
    my $err = Web::Authn->error;
    warn "Error: $err";
}

Instance and class method. When called with a message, constructs a Web::Authn::Exception object, stores it internally, and either warns (if fatal mode is off) or dies (if fatal mode is on). Returns undef in scalar context, an empty list in list context.

When called without arguments, returns the most recent error object (or undef if no error has occurred).

fatal

$authn->fatal(1); # Enable fatal exceptions
$authn->fatal(0); # Disable fatal exceptions
my $bool = $authn->fatal;

Sets or get the boolean value, whether to die upon exception, or not. If set to true, then instead of setting an exception object, this module will die with an exception object. You can catch the exception object then after using try. For example:

use v.5.34; # to be able to use try-catch blocks in perl
use experimental 'try';
no warnings 'experimental';
try
{
    my $authn = Web::Authn->new( fatal => 1 );
    # Forgot the 'rp_id':
    my $bad = $authn->generate_registration_options( rp_id => '', rp_name => 'x', user_name => 'y' );
}
catch( $e )
{
    say "Error occurred: ", $e->message;
    # Error occurred: No value for width was provided.
}

pass_error

sub my_method
{
    my $self = shift( @_ );
    my $authn = Web::Authn->new( %bad_args ) ||
        return( $self->pass_error( Web::Authn->error ) );
    ...
}

Propagates the error stored in another object (or the class-level error) into the current object's error slot, without constructing a new exception. Used internally when a lower-level call fails and the caller wants to surface the same error to its own caller.

ATTESTATION FORMATS

"parse_attestation_object" in Web::Authn::Parse decodes the CBOR attestationObject into fmt, authData and attStmt. "verify" in Web::Authn::Attestation then dispatches on the fmt argument (none, packed, fido-u2f, apple, tpm, android-safetynet, android-key). That package has no attestationObject field; fmt is a named parameter.

Certificate chains against RP-supplied roots are validated in process (CryptX verifies each certificate signature). There is no openssl(1) subprocess. If you pass no roots, pinning is skipped (same as py_webauthn).

COSE ALGORITHMS

Verified with CryptX: ES256/384/512, Ed25519 (EdDSA), RS256/384/512, PS256/384/512.

ML-DSA (-48/-49/-50) keys can be decoded; verification is not implemented.

EXCEPTIONS

See Web::Authn::Exception. Generation of empty rp_id etc. uses "croak" in Carp instead.

DEPENDENCIES

CryptX, Bytes::Random::Secure, and core Digest::SHA, JSON::PP, MIME::Base64.

REFERENCES

Normative documents this module is written against. Dated REC links are pinned; undated /TR/webauthn-3/ always resolves to the latest published Level 3.

WebAuthn (W3C) — browser API and RP procedures

verify_registration_response implements §7.1. verify_authentication_response implements §7.2.

CTAP / FIDO2 — authenticator protocol

WebAuthn is the web API. CTAP is how the platform talks to the authenticator (USB, NFC, BLE, hybrid). Together they are FIDO2.

Encoding and algorithms

CREDITS

Duo Labs / Matthew Miller for his work on the Python's py_webauthn.

AUTHOR

Jacques Deguest <jack@deguest.jp>

SEE ALSO

Web::Authn::Cookbook, Web::Authn::Parse, Web::Authn::Attestation, Web::Authn::Crypto, Web::Authn::COSE, Web::Authn::CBOR, Web::Authn::Exception, Authen::WebAuthn

https://github.com/duo-labs/py_webauthn, https://www.w3.org/TR/webauthn-3/, Authen::WebAuthn

COPYRIGHT & LICENSE

Copyright(c) 2026 DEGUEST Pte. Ltd.

All rights reserved.

This program is free software; you can redistribute it and/or modify it under the same terms as Perl itself.