322 lines
9.9 KiB
Perl
322 lines
9.9 KiB
Perl
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 = <<ENDKEY;
|
|
-----BEGIN EC PRIVATE KEY-----
|
|
MIIBUQIBAQQgWEGujn2kkOVckTIKhIJDSqH99bxydPGloXvbeaq9swiggeMwgeAC
|
|
AQEwLAYHKoZIzj0BAQIhAP////8AAAABAAAAAAAAAAAAAAAA////////////////
|
|
MEQEIP////8AAAABAAAAAAAAAAAAAAAA///////////////8BCBaxjXYqjqT57Pr
|
|
vVV2mIa8ZR0GsMxTsPY7zjw+J9JgSwRBBGsX0fLhLEJH+Lzm5WOkQPJ3A32BLesz
|
|
oPShOUXYmMKWT+NC4v4af5uO5+tKfA+eFivOM1drMV7Oy7ZAaDe/UfUCIQD/////
|
|
AAAAAP//////////vOb6racXnoTzucrC/GMlUQIBAaFEA0IABM/oQXEUzjPwEhM4
|
|
gWmIbCuOXc4Ja8jPDKxbQaZckal7/9a693/nkf7flk1S9AV2tjrtJPF6kg8TCGbF
|
|
KoeD9Wc=
|
|
-----END EC PRIVATE KEY-----
|
|
ENDKEY
|
|
|
|
my $credential_id_1 = "lZYltP9MtoRNuXK8f8tWf";
|
|
my $credential_id_2 = "d2ViYXV0aG5fdGVzdGVyXzI";
|
|
|
|
my $webauthn_tester_1 = Authen::WebAuthn::Test->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%<img src="/static/bootstrap/webauthn.png"%,
|
|
"WebAuthn logo found"
|
|
);
|
|
like(
|
|
$res->[2]->[0],
|
|
qr%<div id="u2fPermission" trspan="u2fPermission"%,
|
|
"Security help message found"
|
|
);
|
|
return $id;
|
|
}
|
|
|
|
sub register_new_device {
|
|
my ( $client, $id, $webauthn_tester, $device_name, $expected_error ) =
|
|
@_;
|
|
my $res;
|
|
|
|
ok(
|
|
$res = $client->_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();
|