use Test::More; use strict; use IO::String; use MIME::Base64 qw/encode_base64url decode_base64url/; use JSON; require 't/test-lib.pm'; SKIP: { eval "use Authen::WebAuthn::Test; use Authen::WebAuthn;"; if ($@) { skip 'Authen::WebAuthn not found'; } my $ecdsa_key = <new( origin => "http://auth.example.com", rp_id => "auth.example.com", credential_id => $credential_id_1, aaguid => "00000000-0000-0000-0000-000000000000", key => $ecdsa_key, sign_count => 5, ); my $webauthn_tester_2 = Authen::WebAuthn::Test->new( origin => "http://auth.example.com", rp_id => "auth.example.com", credential_id => $credential_id_2, aaguid => "00000000-0000-0000-0000-000000000000", key => $ecdsa_key, sign_count => 18, ); #FIXME my $webauthn_tester = $webauthn_tester_1; my $res; my $client = LLNG::Manager::Test->new( { ini => { logLevel => 'error', useSafeJail => 1, webauthn2fSelfRegistration => 1, webauthn2fActivation => 1, webauthn2fUserCanRemoveKey => 1, webauthnDisplayNameAttr => 'cn', } } ); my $portal = $client->p; sub login_and_check_display { my ($client) = @_; my $res; ok( $res = $client->_post( '/', IO::String->new('user=dwho&password=dwho'), length => 23, ), 'Create Session' ); expectOK($res); my $id = expectCookie($res); # Display 2FA Manager ok( $res = $client->_get( '/2fregisters', cookie => "lemonldap=$id", accept => "test/html", ), 'Show 2FA Manager' ); expectRedirection( $res, 'http://auth.example.com//2fregisters/webauthn' ); # Display WebAuthn registration ok( $res = $client->_get( '/2fregisters/webauthn', cookie => "lemonldap=$id", accept => "test/html", ), 'Show WebAuthn registration' ); like( $res->[2]->[0], qr%[2]->[0], qr%
_post( '/2fregisters/webauthn/registrationchallenge', IO::String->new('{}'), cookie => "lemonldap=$id", length => 2, ), 'Registration challenge' ); my $reg_challenge = from_json $res->[2]->[0]; is( $reg_challenge->{request}->{rp}->{name}, "LemonLDAP::NG", "rp.name is set" ); is( $reg_challenge->{request}->{user}->{name}, "dwho", "user.name is set" ); is( length( $reg_challenge->{request}->{user}->{id} ), 86, "user.id is set" ); is( $reg_challenge->{request}->{user}->{displayName}, "Doctor Who", "user.displayName is set" ); my $state_id = $reg_challenge->{state_id}; my $challenge = $reg_challenge->{request}->{challenge}; ok( $state_id, "State ID is set" ); ok( $challenge, "Challenge is set" ); my $credential_response = $webauthn_tester->get_credential_response($reg_challenge); my $registration_response = buildForm( { credential => $webauthn_tester->encode_credential($credential_response), state_id => $state_id, keyName => $device_name, } ); # Post registration ok( $res = $client->_post( '/2fregisters/webauthn/registration', IO::String->new($registration_response), cookie => "lemonldap=$id", length => length($registration_response), ), 'Registration challenge' ); my $reg_response = from_json $res->[2]->[0]; if ($expected_error) { is( $reg_response->{result} // 0, 0, "Failed registration" ); is( $reg_response->{error}, $expected_error, "Failed registration" ); is( $res->[0], 400, "Expected failure http code" ); } else { is( $reg_response->{result}, 1, "Successful registration" ); } # return userHandle return $reg_challenge->{request}->{user}->{id}; } sub verify_device { my ( $client, $id, $webauthn_tester, $expected_credentials ) = @_; my $res; # Get verification parameters ok( $res = $client->_post( '/2fregisters/webauthn/verificationchallenge', IO::String->new('{}'), cookie => "lemonldap=$id", length => 2, ), 'Registration challenge' ); my $verif_challenge = from_json $res->[2]->[0]; is_deeply( $verif_challenge->{request}->{allowCredentials}, $expected_credentials ); my $state_id = $verif_challenge->{state_id}; my $challenge = $verif_challenge->{request}->{challenge}; ok( $state_id, "State ID is set" ); ok( $challenge, "Challenge is set" ); # Increment signature to avoid validation error $webauthn_tester->sign_count( $webauthn_tester->sign_count + 1 ); my $credential_response = $webauthn_tester->get_assertion_response($verif_challenge); my $verification_response = buildForm( { state_id => $state_id, credential => $webauthn_tester->encode_credential($credential_response), } ); # Verify registration ok( $res = $client->_post( '/2fregisters/webauthn/verification', IO::String->new($verification_response), cookie => "lemonldap=$id", length => length($verification_response), ), 'Registration challenge' ); my $verif_response = from_json $res->[2]->[0]; is( $verif_response->{result}, 1, "Successful verification" ); } sub check_psession { my ( $portal, $user_handle ) = @_; # Inspect Psession content my $psession = $portal->getPersistentSession("dwho"); # userHandle is stored is( $psession->{data}->{_webAuthnUserHandle}, $user_handle, "User handle saved" ); my $devices = from_json $psession->{data}->{_2fDevices}; is( @{$devices}, 2, "2 devices found" ); my $device1 = $devices->[0]; my $device2 = $devices->[1]; # Epoch will differ delete $device1->{epoch}; delete $device2->{epoch}; is_deeply( $device1, { '_credentialId' => encode_base64url($credential_id_1), '_credentialPublicKey' => 'pQECAyYgASFYIM_oQXEUzjPwEhM4gWmIbCuOXc4Ja8jPDKxbQaZckal7Ilgg_9a693_nkf7flk1S9AV2tjrtJPF6kg8TCGbFKoeD9Wc', '_signCount' => 5, 'name' => "MyFirstDevice", 'type' => 'WebAuthn' }, "Registration contains expected data" ); is_deeply( $device2, { '_credentialId' => encode_base64url($credential_id_2), '_credentialPublicKey' => 'pQECAyYgASFYIM_oQXEUzjPwEhM4gWmIbCuOXc4Ja8jPDKxbQaZckal7Ilgg_9a693_nkf7flk1S9AV2tjrtJPF6kg8TCGbFKoeD9Wc', '_signCount' => 18, 'name' => "MySecondDevice", 'type' => 'WebAuthn' }, "Registration contains expected data" ); } my $id = login_and_check_display($client); my $user_handle_1 = register_new_device( $client, $id, $webauthn_tester_1, "MyFirstDevice" ); # Register same device again, fails because credential ID is already taken register_new_device( $client, $id, $webauthn_tester_1, "MyAlreadyRegisteredDevice", "webauthnAlreadyRegistered" ); # Register a different device should succeed my $user_handle_2 = register_new_device( $client, $id, $webauthn_tester_2, "MySecondDevice" ); # userHandle was kept from first registration is( $user_handle_2, $user_handle_1, "userHandle was kept from first registration" ); check_psession( $portal, $user_handle_1 ); verify_device( $client, $id, $webauthn_tester_1, [ { 'id' => encode_base64url($credential_id_1), 'type' => 'public-key' }, { 'id' => encode_base64url($credential_id_2), 'type' => 'public-key' } ] ); } # TODO delete clean_sessions(); done_testing();