NAME
Web::Authn - Server-side WebAuthn / passkeys
SYNOPSIS
use Web::Authn;
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',
user_display_name => 'Bob',
) || die( $authn->error );
my $json = $authn->options_to_json( $opts ); # send to the browser
my $reg = $authn->verify_registration_response(
credential => $browser_json,
expected_challenge => $opts->{challenge},
) || die( $authn->error );
# persist $reg->{credential_id}, $reg->{credential_public_key}, $reg->{sign_count}
my $assert_opts = $authn->generate_authentication_options(
allow_credentials => [
{ type => 'public-key', id => $reg->{credential_id} },
],
) || die( $authn->error );
my $ok = $authn->verify_authentication_response(
credential => $browser_json,
expected_challenge => $assert_opts->{challenge},
credential_public_key => $reg->{credential_public_key},
credential_current_sign_count => $reg->{sign_count},
) || die( $authn->error );
$authn->fatal(1); # subsequent errors die() as exception objects
Functional wrappers still exist (they build a temporary object):
use Web::Authn qw( generate_registration_options options_to_json );
VERSION
v0.2.2
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
Named parameters may be passed either as a flat list or as a single hash reference. Both of the following are equivalent:
$authn->generate_registration_options( rp_id => $id, user_name => $name );
$authn->generate_registration_options({ rp_id => $id, user_name => $name });
An odd number of list elements throws Web::Authn::Exception.
String arguments may be plain Perl scalars or blessed objects that overload stringification (overload::Method( $obj, '""' )), for example Module::Generic::Scalar. They are converted with $obj . '' before use.
Array arguments (such as origins, algorithm lists, credential descriptors) may be a native array reference or a blessed array whose reftype is ARRAY, for example Module::Generic::Array.
new
my $authn = Web::Authn->new(
rp_id => 'example.com',
rp_name => 'Example Co',
expected_origin => 'https://example.com',
expected_rp_id => 'example.com', # defaults to rp_id
attestation => 'none',
timeout => 60_000,
user_verification => 'preferred',
fatal => 0,
);
my $authn = Web::Authn->new({
rp_id => 'example.com',
rp_name => 'Example Co',
expected_origin => 'https://example.com',
}) || die( Web::Authn->error );
my $authn = Web::Authn->new(
rp_id => 'example.com',
rp_name => 'Angels, Inc',
origins => [ 'https://www.angels-inc.com' ],
timeout => 120_000,
debug => 1,
) || die( Web::Authn->error );
Stores Relying Party defaults used by later methods. If new is called on an existing instance, a failure stores the error on that instance so the caller can write $authn->new( %args ) || die( $authn->error ).
Supported options are:
attestation-
Optional. String. One of
none,indirect,direct, orenterprise. Defaultnone. Conveyance preference copied onto options produced by "generate_registration_options". debug-
Optional. Boolean. Default false. Stored on the object for the caller; it does not change WebAuthn verification.
expected_origin-
Optional at construction, required later by "verify_registration_response" and "verify_authentication_response" unless passed there. String (a full origin such as
https://example.com) or array of such strings. Compared toclientDataJSON.origin. expected_origins-
Optional. Alias of
expected_origin. Same type. expected_rp_id-
Optional. String. Effective RP ID used during verification. Defaults to
rp_id. fatal-
Optional. Boolean. Default false. When true, "error" throws the exception instead of returning
undef. See "fatal". origin-
Optional. Alias of
expected_origin. Same type. origins-
Optional. Alias of
expected_origin. Same type. Useful when the application config key is plural. rp_id-
Optional at construction, required (here or on the call) by "generate_registration_options" and "generate_authentication_options". String. Registrable domain of the Relying Party, for example
example.com. Must match the RP ID hash inside authenticator data. rp_name-
Optional at construction, required (here or on the call) by "generate_registration_options". String. Human-readable Relying Party name shown by the authenticator UI.
timeout-
Optional. Integer. Milliseconds. Default
60000. Hint copied onto generated options. user_verification-
Optional. String. One of
required,preferred, ordiscouraged. Defaultpreferred. Copied onto generated options.
base64url_to_bytes
my $raw = $authn->base64url_to_bytes( $credential->{id} ) ||
die( $authn->error );
Decodes unpadded base64url to raw bytes. The only argument is the string to decode (characters A-Za-z0-9_-). You may also pass an object that overloads stringification, such as Module::Generic::Scalar.
bytes_to_base64url
my $id = $authn->bytes_to_base64url( $reg->{credential_id} );
Encodes raw bytes as unpadded base64url. The only argument is the byte string to encode. You may also pass an object that overloads stringification.
error
my $authn = Web::Authn->new( %bad_args );
if( !defined( $authn ) )
{
my $err = Web::Authn->error;
warn "Error: $err";
}
my $err = $authn->error;
warn $err->message, ' at ', $err->file, ' line ', $err->line;
$authn->error( 'something went wrong' );
$authn->error({ class => 'Web::Authn::Exception::InvalidRegistration', message => 'bad' });
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).
class-
This key is optional. It is a string naming the exception class to bless into, and it defaults to
Web::Authn::Exception. message-
This key is optional when you pass a hash (the sole positional string is treated as the message). It is the human-readable error text.
skip_frames-
This key is optional. It is an integer: how many extra
callerframes to skip when recording file and line.
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.
}
Called with a boolean, this method sets whether subsequent "error" calls throw. Called without arguments, it returns the current flag.
generate_registration_options
my $opts = $authn->generate_registration_options(
user_name => 'bob@example.com',
user_id => $handle, # raw bytes; optional
user_display_name => 'Bob',
challenge => $bytes, # optional
timeout => 60_000,
attestation => 'none',
exclude_credentials => [ { type => 'public-key', id => $cid } ],
supported_pub_key_algs => [ -8, -7, -257 ],
authenticator_selection => {
resident_key => 'preferred',
user_verification => 'preferred',
},
hints => [ 'client-device' ],
) || die( $authn->error );
my $opts = $authn->generate_registration_options({
user_name => 'bob@example.com',
user_id => $handle,
}) || die( $authn->error );
Returns a Perl hash describing PublicKeyCredentialCreationOptions.
Pass it through "options_to_json" before sending it to the browser.
An empty rp_id, rp_name, or user_name is rejected via "error".
attestation-
This argument is optional. It is a string, one of
none,indirect,direct, orenterprise. It defaults to the value stored by "new", or tonone. It is the conveyance preference placed on the creation options. authenticator_selection-
This argument is optional. It is a hash. Recognised keys are
authenticator_attachment(the stringplatformorcross-platform),resident_key(discouraged,preferred, orrequired), anduser_verification(required,preferred, ordiscouraged). Ifresident_keyisrequired,require_resident_keyis also set to true on the options. challenge-
This argument is optional. It is a raw byte string: the cryptographic challenge issued to the authenticator. It defaults to 64 CSPRNG bytes from "generate_challenge". Store this value until you call "verify_registration_response".
exclude_credentials-
This argument is optional. It is an array of descriptor hashes of the form
{ type ='public-key', id => $raw_bytes, transports => [ ... ] }>, whereidis raw bytes. It prevents re-registration of authenticators the account already has. hints-
This argument is optional. It is an array of strings: WebAuthn Level 3 UI hints, one or more of
security-key,client-device, andhybrid. rp_id-
This argument is required unless it was already set on the object by "new". It is a string: the registrable domain of the Relying Party.
rp_name-
This argument is required unless it was already set on the object by "new". It is a string: the human-readable Relying Party name.
supported_pub_key_algs-
This argument is optional. It is an array of integers (COSE algorithm identifiers). It defaults to EdDSA (
-8), ES256 (-7), and RS256 (-257). Constants live in Web::Authn::COSE. timeout-
This argument is optional. It is an integer number of milliseconds. It defaults to the value stored by "new", or to
60000. It is a client-side hint only. user_display_name-
This argument is optional. It is a string shown in the OS passkey UI. It defaults to
user_name. user_id-
This argument is optional. It is raw bytes identifying the account to the authenticator, and it must not be an email or other personal information. It defaults to 64 random bytes from "generate_user_handle". Keep this value stable for the life of the account.
user_name-
This argument is required. It is a string: the account identifier shown to the user, often an email or login.
generate_authentication_options
my $opts = $authn->generate_authentication_options(
allow_credentials => [ { type => 'public-key', id => $cid } ],
user_verification => 'preferred',
timeout => 60_000,
challenge => $bytes, # optional
) || die( $authn->error );
# usernameless / discoverable:
my $opts = $authn->generate_authentication_options;
my $opts = $authn->generate_authentication_options({
allow_credentials => [ { type => 'public-key', id => $cid } ],
}) || die( $authn->error );
Returns PublicKeyCredentialRequestOptions. Pass it through "options_to_json" before sending it to the browser. An empty rp_id is rejected via "error".
allow_credentials-
This argument is optional. It is an array of descriptor hashes of the form
{ type ='public-key', id => $raw_bytes, transports => [ ... ] }>, whereidis raw bytes. Omit it or pass[]for usernameless (discoverable) credentials. challenge-
This argument is optional. It is a raw byte string: the cryptographic challenge issued to the authenticator. It defaults to 64 CSPRNG bytes from "generate_challenge". Store this value until you call "verify_authentication_response".
rp_id-
This argument is required unless it was already set on the object by "new". It is a string: the registrable domain of the Relying Party.
timeout-
This argument is optional. It is an integer number of milliseconds. It defaults to the value stored by "new", or to
60000. It is a client-side hint only. user_verification-
This argument is optional. It is a string, one of
required,preferred, ordiscouraged. It defaults to the value stored by "new", or topreferred.
generate_challenge
my $chal = $authn->generate_challenge; # 64 bytes
my $chal = $authn->generate_challenge(16);
Returns raw challenge bytes from Bytes::Random::Secure. You may pass an optional integer length; it defaults to 64.
generate_user_handle
my $handle = $authn->generate_user_handle; # 64 random bytes
Returns 64 raw random bytes suitable as a WebAuthn user.id. No arguments.
options_to_json
print $authn->options_to_json( $opts );
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. The only argument is the hash returned by "generate_registration_options" or "generate_authentication_options".
options_to_json_dict
my $href = $authn->options_to_json_dict( $opts );
Same conversion as "options_to_json", but returns a hash instead of a JSON string. The only argument is the same options hash.
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.
verify_registration_response
my $reg = $authn->verify_registration_response(
credential => $browser_json,
expected_challenge => $opts->{challenge},
expected_rp_id => 'example.com', # default: new()
expected_origin => 'https://example.com',
require_user_presence => 1,
require_user_verification => 0,
supported_pub_key_algs => [ -8, -7, -257 ],
pem_root_certs_bytes_by_fmt => { packed => [ $pem ] },
) || die( $authn->error );
my $reg = $authn->verify_registration_response({
credential => $browser_json,
expected_challenge => $opts->{challenge},
expected_rp_id => 'example.com',
expected_origin => 'https://example.com',
}) || die( $authn->error );
Implements WebAuthn §7.1.
On success returns a hash of fields to persist (see below).
On failure stores Web::Authn::Exception::InvalidRegistration, and returns undef in scalar context or an empty list in list context, unless "fatal" is true.
Supported options are:
credential-
This argument is required. It is either a JSON string, or a hash in the SimpleWebAuthn / py_webauthn shape (
id,rawId,type,response.clientDataJSON,response.attestationObjectas unpadded base64url). This is the value returned bynavigator.credentials.create(). expected_challenge-
This argument is required. It is a raw byte string: the challenge previously issued by "generate_registration_options". It must match
clientDataJSON.challenge. expected_origin-
This argument is required unless it was already set on the object by "new". It is either a string containing a full origin such as
https://example.com, or an array of such strings. It must matchclientDataJSON.origin. expected_origins-
This argument is optional. It is an alias of
expected_originand accepts the same values. expected_rp_id-
This argument is required unless it was already set on the object by "new" (as
expected_rp_idorrp_id). It is a string: the registrable domain. Its SHA-256 must equal the RP ID hash in authenticator data. origin-
This argument is optional. It is an alias of
expected_originand accepts the same values. origins-
This argument is optional. It is an alias of
expected_originand accepts the same values. pem_root_certs_bytes_by_fmt-
This argument is optional. It is a hash mapping an attestation format name (for example
packedorfido-u2f) to an array of PEM or DER root certificates (strings or raw bytes). It is used when verifying packed or FIDO U2F statements that carry anx5c. It is unused forfmt=none. require_user_presence-
This argument is optional. It is a boolean and defaults to true. When it is true, authenticator data must have the User Present (UP) flag set.
require_user_verification-
This argument is optional. It is a boolean and defaults to false. When it is true, authenticator data must have the User Verified (UV) flag set.
supported_pub_key_algs-
This argument is optional. It is an array of integers (COSE algorithm identifiers). It restricts which credential public-key algorithms are accepted. The default includes the Web::Authn::COSE defaults plus ES384, ES512, RS384, RS512, PS256, PS384 and PS512.
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
verify_authentication_response
my $ok = $authn->verify_authentication_response(
credential => $browser_json,
expected_challenge => $opts->{challenge},
expected_rp_id => 'example.com',
expected_origin => 'https://example.com',
credential_public_key => $row->{public_key},
credential_current_sign_count => $row->{sign_count},
require_user_verification => 0,
) || die( $authn->error );
my $ok = $authn->verify_authentication_response({
credential => $browser_json,
expected_challenge => $opts->{challenge},
credential_public_key => $row->{public_key},
credential_current_sign_count => $row->{sign_count},
}) || die( $authn->error );
Implements WebAuthn §7.2. On success returns a hash (see below). On failure returns undef and stores Web::Authn::Exception::InvalidAuthentication unless "fatal" is true.
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.
credential-
This argument is required. It is either a JSON string, or a hash in the SimpleWebAuthn / py_webauthn shape (
id,rawId,type,response.clientDataJSON,response.authenticatorData,response.signatureas unpadded base64url). This is the value returned bynavigator.credentials.get(). credential_current_sign_count-
This argument is required. It is an integer: the
sign_countlast persisted for this credential. It is compared to the counter in authenticator data. credential_public_key-
This argument is required. It is raw bytes: the
credential_public_keyCOSE_Key blob returned by "verify_registration_response" and stored as-is. expected_challenge-
This argument is required. It is a raw byte string: the challenge previously issued by "generate_authentication_options". It must match
clientDataJSON.challenge. expected_origin-
This argument is required unless it was already set on the object by "new". It is either a string containing a full origin such as
https://example.com, or an array of such strings. It must matchclientDataJSON.origin. expected_origins-
This argument is optional. It is an alias of
expected_originand accepts the same values. expected_rp_id-
This argument is required unless it was already set on the object by "new" (as
expected_rp_idorrp_id). It is a string: the registrable domain. Its SHA-256 must equal the RP ID hash in authenticator data. origin-
This argument is optional. It is an alias of
expected_originand accepts the same values. origins-
This argument is optional. It is an alias of
expected_originand accepts the same values. require_user_verification-
This argument is optional. It is a boolean and defaults to false. When it is true, authenticator data must have the User Verified (UV) flag set.
Returns:
credential_id raw bytes
new_sign_count persist this
credential_device_type
credential_backed_up
user_verified
user_handle bytes or undef
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.
none-
Statement must be empty. Typical for consumer passkeys.
packed-
Signature over
authData || SHA-256(clientDataJSON). Self-attestation uses the credential public key;x5cuses the leaf certificate (optional root pinning). fido-u2f-
U2F
verificationData, P-256, AAGUID all zeros. apple-
x5c, nonce in OID1.2.840.113635.100.8.2, subject public key matches the credential key. Pass Apple's WebAuthn root viapem_root_certs_bytes_by_fmt. tpm-
x5c, signature overcertInfo,extraDatacontains a hash ofattToBeSigned. android-safetynet-
JWT signature,
ctsProfileMatch, nonce. android-key-
x5cplus packed-style signature.
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
- Level 3 Recommendation (25 August 2026) — current
-
https://www.w3.org/TR/webauthn-3/
Pinned: https://www.w3.org/TR/2026/REC-webauthn-3-20260825/
RP registration procedure: https://www.w3.org/TR/webauthn-3/#sctn-registering-a-new-credential (§7.1)
RP authentication procedure: https://www.w3.org/TR/webauthn-3/#sctn-verifying-assertion (§7.2)
Attestation statement formats: https://www.w3.org/TR/webauthn-3/#sctn-defined-attestation-formats (§8)
Editor's Draft (next level): https://w3c.github.io/webauthn/
Working Group repo: https://github.com/w3c/webauthn
- Level 2 Recommendation (8 April 2021)
- Level 1 Recommendation (4 March 2019)
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.
- CTAP 2.2 Proposed Standard (28 February 2025)
- CTAP 2.1 Proposed Standard
- CTAP 2.0 (27 February 2018)
- FIDO Alliance specifications index
- ITU-T X.1278 (CTAP aligned)
Encoding and algorithms
- RFC 8949 — CBOR
- RFC 8610 — CDDL
- RFC 9052 / 9053 — COSE
- RFC 8812 — COSE/JOSE registrations used by WebAuthn (RS256 etc.)
- IANA COSE Algorithms registry
- IANA COSE Key Types / Elliptic Curves
- RFC 5280 — X.509 (attestation
x5c) - RFC 4648 §5 — base64url
Related
THREAD & PROCESS SAFETY
Web::Authn is designed to be fully thread-safe and process-safe, ensuring data integrity across Perl ithreads and mod_perl’s threaded Multi-Processing Modules (MPMs) such as Worker or Event, provided each thread constructs and uses its own objects. Do not pass a Web::Authn instance, a CryptX key object, or a Bytes::Random::Secure generator across threads->create.
Perl ithreads clone the interpreter. Package variables such as $Web::Authn::ERROR and $JSON_CLASS are copied, then independent. They are not :shared. That is the intended model: each thread has its own last-error slot.
What is not safe to share:
A blessed
Web::Authnobject created in another thread. Create it inside the worker (Web::Authn->new( ... )afterthreads->create, or in a prefork child afterfork).CryptX public-key objects returned by Web::Authn::Crypto. Persist COSE public-key bytes in the database and rebuild the key in the thread that verifies.
A cached
Bytes::Random::Secureobject. "_random" in Web::Authn::Parse therefore constructs a new generator for every challenge or user handle. Do not add a process-wide singleton.
JSON backends (Cpanel::JSON::XS, JSON::XS, JSON::PP) are used by creating a new coder per call ($JSON_CLASS->new). Do not stash a coder object in a package global.
Typical prefork servers (Starman, Hypnotoad, Apache prefork + mod_perl) start workers after compile. Each worker is a separate process, not an ithread; no extra care is required beyond not sharing memory on purpose.
Typical ithreads pattern:
use threads;
my $thr = threads->create(sub
{
my $authn = Web::Authn->new(
rp_id => 'example.com',
expected_origin => 'https://example.com',
);
my $opts = $authn->generate_registration_options(
user_name => 'bob',
) || die( $authn->error );
return( $opts->{challenge} );
});
my $challenge = $thr->join;
fork() without exec: avoid calling generate/verify in the parent and the child with the same in-memory CryptX object. Rebuild from stored bytes in the child.
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.