diff --git a/lemonldap-ng-portal/MANIFEST b/lemonldap-ng-portal/MANIFEST index bc984b888..58bf51c51 100644 --- a/lemonldap-ng-portal/MANIFEST +++ b/lemonldap-ng-portal/MANIFEST @@ -58,6 +58,7 @@ lib/Lemonldap/NG/Portal/Issuer/OpenID.pm lib/Lemonldap/NG/Portal/Issuer/OpenIDConnect.pm lib/Lemonldap/NG/Portal/Issuer/SAML.pm lib/Lemonldap/NG/Portal/Lib/_tokenRule.pm +lib/Lemonldap/NG/Portal/Lib/2fDevices.pm lib/Lemonldap/NG/Portal/Lib/Captcha.pm lib/Lemonldap/NG/Portal/Lib/CAS.pm lib/Lemonldap/NG/Portal/Lib/Choice.pm diff --git a/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/2F/Engines/Default.pm b/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/2F/Engines/Default.pm index 034f2aa7c..24be564c6 100644 --- a/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/2F/Engines/Default.pm +++ b/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/2F/Engines/Default.pm @@ -509,14 +509,6 @@ sub _displayRegister { } } - return [ 302, [ Location => $self->conf->{portal} . $am[0]->{URL} ], [] ] - if ( - @am == 1 - and not( $req->userData->{_2fDevices} - && $req->userData->{_2fDevices} =~ /\w+/ - or $req->data->{sfRegRequired} ) - ); - # Retrieve user all second factors my $_2fDevices = []; unless ( $self->canUpdateSfa($req) ) { @@ -534,6 +526,13 @@ sub _displayRegister { $self->userLogger->warn("Do not display 2F devices!"); } + return [ 302, [ Location => $self->conf->{portal} . $am[0]->{URL} ], [] ] + if ( + @am == 1 + and not( @$_2fDevices + or $req->data->{sfRegRequired} ) + ); + # Parse second factors to display delete button if allowed and upgrade button my $displayUpgBtn = 0; my $action = ''; diff --git a/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/2F/Register/WebAuthn.pm b/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/2F/Register/WebAuthn.pm index 3ad69a0ed..cc3178557 100644 --- a/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/2F/Register/WebAuthn.pm +++ b/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/2F/Register/WebAuthn.pm @@ -82,11 +82,9 @@ sub _generate_user_handle { sub _registrationchallenge { my ( $self, $req, $user ) = @_; - my @_2fDevices = $self->find2fByType( $req, $req->userData ); - - # Check if user can register one more 2F device - my $size = @_2fDevices; - my $maxSize = $self->conf->{max2FDevices}; + my @alldevices = $self->find2fDevicesByType( $req, $req->userData ); + my $size = @alldevices; + my $maxSize = $self->conf->{max2FDevices}; $self->logger->debug("Registered 2F Device(s): $size / $maxSize"); if ( $size >= $maxSize ) { $self->userLogger->warn("Max number of 2F devices is reached"); @@ -179,8 +177,8 @@ sub _registration { . "\n" ); if ( - $self->find2fByKey( - $req, $req->userData, $self->type, + $self->find2fDevicesByKey( + $req, $req->userData, $self->type, "_credentialId", $credential_id ) ) diff --git a/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/2F/WebAuthn.pm b/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/2F/WebAuthn.pm index 8ad27eaeb..c86f3e2f5 100644 --- a/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/2F/WebAuthn.pm +++ b/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/2F/WebAuthn.pm @@ -36,8 +36,7 @@ sub init { if ( $self->conf->{webauthn2fSelfRegistration} and $self->conf->{webauthn2fActivation} eq '1' ) { - $self->conf->{webauthn2fActivation} = - '$_2fDevices && $_2fDevices =~ /"type"\s*:\s*"WebAuthn"/s'; + $self->conf->{webauthn2fActivation} = 'has2f("WebAuthn")'; } return 0 unless ( $self->Lemonldap::NG::Portal::Main::SecondFactor::init() ); diff --git a/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Lib/2fDevices.pm b/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Lib/2fDevices.pm new file mode 100644 index 000000000..6656540b8 --- /dev/null +++ b/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Lib/2fDevices.pm @@ -0,0 +1,309 @@ +package Lemonldap::NG::Portal::Lib::2fDevices; + +=pod + +=head1 NAME + +Lemonldap::NG::Portal::Lib::2fDevices - Role for registrable second factors + +=head1 DESCRIPTION + +This role provides LemonLDAP::NG modules with a high-level interface to storing +information on registrable second factors into the persistent session. + +It is recommended that _2fDevices is never accessed directly from code outside +of this module + +=head1 METHODS + +=over + +=cut + +use strict; +use Mouse::Role; +use JSON; + +requires qw(p conf logger); + +our $VERSION = '2.0.15'; + +=item update2fDevice + +Updates one field of a registered device + + $self->update2fDevice($req, $info, $type, $key, $value, $update_key, $update_value); + +=over 4 + +=item req: Current LemonLDAP::NG request + +=item info: hashref of current session information + +=item type: 'type' field of the device to update + +=item key, value: update the device whose 'key' field equals value + +=item update_key, update_value: set the matched devices' 'update_key' field to update_value + +=back + +Returns true if the update was sucessful + +=cut + +sub update2fDevice { + my ( $self, $req, $info, $type, $key, $value, $update_key, $update_value ) + = @_; + + my $user = $info->{ $self->conf->{whatToTrace} }; + + my $_2fDevices = $self->get2fDevices( $req, $info ); + return 0 unless $_2fDevices; + + my @found = + grep { $_->{type} eq $type and $_->{$key} eq $value } @{$_2fDevices}; + + for my $device (@found) { + $device->{$update_key} = $update_value; + } + + if (@found) { + $self->p->updatePersistentSession( $req, + { _2fDevices => to_json($_2fDevices) }, $user ); + return 1; + } + return 0; +} + +=item add2fDevice + +Register a new device + + $self->add2fDevice($req, $info, $device); + +=over 4 + +=item req: Current LemonLDAP::NG request + +=item info: hashref of current session information + +=item device: hashref of device details. It must contain at least a 'type', +'name' and 'epoch' key + +=back + +Returns true if the update was sucessful + +=cut + +sub add2fDevice { + my ( $self, $req, $info, $device ) = @_; + + my $_2fDevices = $self->get2fDevices( $req, $info ); + + push @{$_2fDevices}, $device; + $self->logger->debug( + "Append 2F Device: { type => $device->{type}, name => $device->{name} }" + ); + $self->p->updatePersistentSession( $req, + { _2fDevices => to_json($_2fDevices) } ); + return 1; +} + +=item del2fDevices + +Delete the devices specified in the @$devices array + + $self->del2fDevices($req, $info, $devices); + +=over 4 + +=item req: Current LemonLDAP::NG request + +=item info: hashref of current session information + +=item device: arrayref of type+epoch hashrefs + + [ { type => xxx, epoch => xxx }, { type => xxx, epoch => xxx } ] + +=back + +Returns true if the update was sucessful + +=cut + +sub del2fDevices { + my ( $self, $req, $info, $devices ) = @_; + + return 0 unless ( ref($devices) eq "ARRAY" ); + + my $_2fDevices = $self->get2fDevices( $req, $info ); + return 0 unless $_2fDevices; + + my @updated_2fDevices = @{$_2fDevices}; + my $need_update = 0; + + for my $device_spec (@$devices) { + next unless ( ref($device_spec) eq "HASH" ); + my $type = $device_spec->{type}; + my $epoch = $device_spec->{epoch}; + next unless ( $type and $epoch ); + + my $size_before = @updated_2fDevices; + @updated_2fDevices = + grep { not( $_->{type} eq $type and $_->{epoch} eq $epoch ) } + @updated_2fDevices; + if ( @updated_2fDevices < $size_before ) { + $need_update = 1; + $self->logger->debug( + "Deleted 2F Device: { type => $type, epoch => $epoch }"); + } + } + + if ($need_update) { + $self->p->updatePersistentSession( $req, + { _2fDevices => to_json( [@updated_2fDevices] ) } ); + } + return 1; +} + +=item del2fDevice + +Delete a single device + + $self->del2fDevice($req, $info, $type, $epoch); + +=over 4 + +=item req: Current LemonLDAP::NG request + +=item info: hashref of current session information + +=item type: type of the device to remove + +=item epoch: timestamp of the device to remove + +=back + +Returns true if the update was sucessful + +=cut + +sub del2fDevice { + my ( $self, $req, $info, $type, $epoch ) = @_; + + return $self->del2fDevices( $req, $info, + [ { type => $type, epoch => $epoch } ] ); +} + +=item find2fDevicesByKey + +Find devices from one of its attributes + + $self->find2fDevicesByKey($req, $info, $type, $key, $value); + +=over 4 + +=item req: Current LemonLDAP::NG request + +=item info: hashref of current session information + +=item type: device type + +=item key, value: attribute to search in the device hash and the value to filter on + +=back + +Returns an array of devices for which type, key and value match the supplied ones + +=cut + +sub find2fDevicesByKey { + my ( $self, $req, $info, $type, $key, $value ) = @_; + + my $_2fDevices = $self->get2fDevices( $req, $info ); + return unless $_2fDevices; + + my @found = + grep { $_->{type} eq $type and $_->{$key} eq $value } @{$_2fDevices}; + return @found; +} + +=item get2fDevices + +Return all registrable devices. + + $self->get2fDevices($req, $info); + +=over 4 + +=item req: Current LemonLDAP::NG request + +=item info: hashref of current session information + +=back + +Returns an arrayref of all registrable devices, or undef if an error occured + +=cut + +sub get2fDevices { + my ( $self, $req, $info ) = @_; + + my $_2fDevices; + if ( $info->{_2fDevices} ) { + $_2fDevices = + eval { from_json( $info->{_2fDevices}, { allow_nonref => 1 } ); }; + if ($@) { + $self->logger->error("Corrupted session (_2fDevices): $@"); + return; + } + } + else { + # Return new ArrayRef + return []; + } + if ( ref($_2fDevices) eq "ARRAY" ) { + return $_2fDevices; + } + else { + return; + } +} + +=item find2fDevicesByType + +Return all registrable devices of a certain type. If type is not given, return +all registrable devices + + $self->find2fDevicesByType($req, $info, $type); + +=over 4 + +=item req: Current LemonLDAP::NG request + +=item info: hashref of current session information + +=item type: type of registrable device to return + +=back + +Returns an array of all matching devices + +=cut + +sub find2fDevicesByType { + my ( $self, $req, $info, $type ) = @_; + + my $_2fDevices = $self->get2fDevices( $req, $info ); + return unless $_2fDevices; + + return @{$_2fDevices} unless $type; + my @found = grep { $_->{type} eq $type } @{$_2fDevices}; + return @found; +} +1; + +=back + diff --git a/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Lib/WebAuthn.pm b/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Lib/WebAuthn.pm index f1741ef45..6d0b4446a 100644 --- a/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Lib/WebAuthn.pm +++ b/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Lib/WebAuthn.pm @@ -7,11 +7,12 @@ use JSON qw(decode_json from_json to_json); use Digest::SHA qw(sha256); use URI; use Carp; +with 'Lemonldap::NG::Portal::Lib::2fDevices'; our $VERSION = '2.0.12'; -has rp_id => ( is => 'rw', lazy => 1, builder => "_build_rp_id" ); -has origin => ( is => 'rw', lazy => 1, builder => "_build_origin" ); +has rp_id => ( is => 'rw', lazy => 1, builder => "_build_rp_id" ); +has origin => ( is => 'rw', lazy => 1, builder => "_build_origin" ); has type => ( is => 'ro', default => 'WebAuthn' ); has verifier => ( is => 'rw', lazy => 1, builder => "_build_verifier" ); @@ -67,7 +68,7 @@ sub generateChallenge { my ( $self, $req, $data ) = @_; # Find webauthn devices for user - my @webauthn_devices = $self->find2fByType( $req, $data, $self->type ); + my @webauthn_devices = $self->find2fDevicesByType( $req, $data, $self->type ); unless (@webauthn_devices) { return; } @@ -138,7 +139,7 @@ sub validateAssertion { # credential.id If the user was identified before the authentication # ceremony was initiated, e.g., via a username or cookie, verify that the # identified user is the owner of credentialSource. - my @webauthn_devices = $self->find2fByType( $req, $data, $self->type ); + my @webauthn_devices = $self->find2fDevicesByType( $req, $data, $self->type ); my @matching_credentials = grep { $_->{_credentialId} eq $credential_id } @webauthn_devices; if ( @matching_credentials < 1 ) { @@ -234,109 +235,4 @@ sub decode_credential { return $credential; } -sub update2fDevice { - my ( $self, $req, $info, $type, $key, $value, $update_key, $update_value ) - = @_; - - my $user = $info->{ $self->conf->{whatToTrace} }; - - my $_2fDevices = $self->get2fDevices( $req, $info ); - return 0 unless $_2fDevices; - - my @found = - grep { $_->{type} eq $type and $_->{$key} eq $value } @{$_2fDevices}; - - for my $device (@found) { - $device->{$update_key} = $update_value; - } - - if (@found) { - $self->p->updatePersistentSession( $req, - { _2fDevices => to_json($_2fDevices) }, $user ); - return 1; - } - return 0; -} - -sub add2fDevice { - my ( $self, $req, $info, $device ) = @_; - - my $_2fDevices = $self->get2fDevices( $req, $info ); - - push @{$_2fDevices}, $device; - $self->logger->debug( - "Append 2F Device: { type => 'Webauthn', name => $device->{name} }"); - $self->p->updatePersistentSession( $req, - { _2fDevices => to_json($_2fDevices) } ); - return 1; -} - -sub del2fDevice { - my ( $self, $req, $info, $type, $epoch ) = @_; - - my $_2fDevices = $self->get2fDevices( $req, $info ); - return 0 unless $_2fDevices; - - my @updated_2fDevices = - grep { not( $_->{type} eq $type and $_->{epoch} eq $epoch ) } - @{$_2fDevices}; - $self->logger->debug( - "Deleted 2F Device: { type => $type, epoch => $epoch }"); - $self->p->updatePersistentSession( $req, - { _2fDevices => to_json( [@updated_2fDevices] ) } ); - return 1; -} - -sub find2fByKey { - my ( $self, $req, $info, $type, $key, $value ) = @_; - - my $_2fDevices = $self->get2fDevices( $req, $info ); - return unless $_2fDevices; - - my @found = - grep { $_->{type} eq $type and $_->{$key} eq $value } @{$_2fDevices}; - return @found; -} - -## @method get2fDevices($req, $info) -# Validate logout request -# @param req Request object -# @param info HashRef of session data -# @return undef or ArrayRef of second factors - -sub get2fDevices { - my ( $self, $req, $info ) = @_; - - my $_2fDevices; - if ( $info->{_2fDevices} ) { - $_2fDevices = - eval { from_json( $info->{_2fDevices}, { allow_nonref => 1 } ); }; - if ($@) { - $self->logger->error("Corrupted session (_2fDevices): $@"); - return; - } - } - else { - # Return new ArrayRef - return []; - } - if ( ref($_2fDevices) eq "ARRAY" ) { - return $_2fDevices; - } - else { - return; - } -} - -sub find2fByType { - my ( $self, $req, $info, $type ) = @_; - - my $_2fDevices = $self->get2fDevices( $req, $info ); - return unless $_2fDevices; - - return @{$_2fDevices} unless $type; - my @found = grep { $_->{type} eq $type } @{$_2fDevices}; - return @found; -} - 1;