exswan_test creates valid WebAuthn responses for application tests. Use it when a test must
cross your HTTP, ceremony-store, persistence, or session code without a real browser.
The helpers run the real ExSwan verification code. They do not replace or mock the verifier.
Install the package
Add exswan_test only to the test environment:
def deps do
[
{:exswan, "~> 0.1.0"},
{:exswan_test, "~> 0.1.0", only: :test}
]
endDo not include this package in a production release. Its default private key is public and fixed. This makes tests repeatable, but makes the key unsafe for real credentials.
Test registration
Create an authenticator and keep it for the authentication test. Pass the challenge and RP ID from the ceremony that your application stored.
alias ExSwan.Test.Authenticator
challenge = :binary.copy(<<7>>, 32)
origin = "https://example.com"
rp_id = "example.com"
user_handle = <<1, 2, 3, 4>>
authenticator = Authenticator.new(user_handle: user_handle)
response =
Authenticator.registration_response(authenticator,
challenge: challenge,
origin: origin,
rp_id: rp_id
)
assert {:ok, registration} =
ExSwan.verify_registration_response(
response: response,
expected_challenge: challenge,
expected_origin: origin,
expected_rp_id: rp_id
)
credential = registration.credentialThe returned credential contains a CBOR-encoded public key. Persist it in the same form that your application uses in production.
Test authentication
Reuse the authenticator that created the credential. It owns the matching private key.
response =
Authenticator.authentication_response(authenticator,
challenge: challenge,
origin: origin,
rp_id: rp_id,
sign_count: credential.sign_count + 1
)
assert {:ok, authentication} =
ExSwan.verify_authentication_response(
response: response,
expected_challenge: challenge,
expected_origin: origin,
expected_rp_id: rp_id,
expected_user_handle: user_handle,
credential: credential
)
assert authentication.new_sign_count == credential.sign_count + 1After verification, test that your application persists new_sign_count and the returned
backup state. A later request must use the updated stored credential.
Create a stored credential directly
Some tests start at authentication and do not need registration. Use credential/2 to create
the stored value that matches an authenticator:
authenticator = Authenticator.new(user_handle: user_handle)
credential =
Authenticator.credential(authenticator,
sign_count: 10,
credential_device_type: :single_device,
credential_backed_up: false
)Use a second Authenticator.new/1 value with a different :credential_id to test an unknown
credential.
Test rejection paths
The builders sign the response after they apply their options. This lets you create responses that are cryptographically valid but fail a specific WebAuthn check.
# Wrong challenge
Authenticator.authentication_response(authenticator,
challenge: :binary.copy(<<9>>, 32),
origin: origin,
rp_id: rp_id
)
# Wrong origin
Authenticator.authentication_response(authenticator,
challenge: challenge,
origin: "https://evil.example",
rp_id: rp_id
)
# Wrong RP ID hash
Authenticator.authentication_response(authenticator,
challenge: challenge,
origin: origin,
rp_id: "other.example.com"
)
# Counter rollback: verify this response against a stored count of 10
Authenticator.authentication_response(authenticator,
challenge: challenge,
origin: origin,
rp_id: rp_id,
sign_count: 9
)
# User presence without user verification
Authenticator.authentication_response(authenticator,
challenge: challenge,
origin: origin,
rp_id: rp_id,
flags: 0x01
)Use tamper/2 when the corruption itself is the scenario:
invalid_response = Authenticator.tamper(response, :signature)
invalid_json = Authenticator.tamper(response, :client_data)
short_authenticator_data = Authenticator.tamper(response, :authenticator_data)Prefer builder options for semantic failures. Use tamper/2 for malformed or corrupted input.
This keeps each test clear about the condition it exercises.
Test HTTP endpoints
For an endpoint test, use the options returned by the begin endpoint to build the completion response:
- Call the registration or authentication begin endpoint.
- Read the challenge from its JSON response, or from the stored ceremony when the response is wrapped in an application-specific token.
- Build a response with the same challenge, origin, and RP ID.
- Send that response to the completion endpoint.
- Assert the HTTP result and all application effects.
Test effects that ExSwan cannot own, including:
- ceremony consumption and replay rejection;
- credential uniqueness and ownership rules;
- transaction rollback after persistence failures;
- signature counter and backup-state updates;
- session renewal after authentication;
- behavior for unknown credential IDs.
If a controller test must avoid cryptographic verification, add a small verifier module inside
your application and inject a test adapter there. Keep that seam local to the application. Use
exswan_test for endpoint and integration tests that should exercise real verification.
Authenticator options
Authenticator.new/1 accepts:
:credential_id— raw binary credential ID;:user_handle— raw binary user handle;:private_key— a 32-byte P-256 private key.
The response builders accept these common options:
:challenge,:origin, and:rp_id— required ceremony values;:credential_id— overrides the response credential ID;:flags— raw WebAuthn authenticator flags;:sign_count— authenticator signature counter;:authenticator_attachment— defaults to"platform";:client_extensions— defaults to an empty map.
Registration also accepts :aaguid and :transports. Authentication also accepts
:user_handle.