diff --git a/doc/sources/admin/secondfactor.rst b/doc/sources/admin/secondfactor.rst index 65f95a5adfe37addd3689fb8fe673bc4eb70c276..921f10cd2c907308c288e8ad76f96e82f4628c22 100644 --- a/doc/sources/admin/secondfactor.rst +++ b/doc/sources/admin/secondfactor.rst @@ -77,6 +77,17 @@ of doing a complete reauthentication. .. |beta| image:: /documentation/beta.png +Special Rule Activation +----------------------- + +When activating second factor an additional option is available called "Special Rule". This +option allows the second factor to be enabled only when the conditions of the expression are met. + +.. tip:: + + Using the following expression ``$targetAuthnLevel <= 10`` enables the second factor when + the application targetAuthnLevel is equal to the required authentication level. + Login timeout ------------- 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 16f0b263f9cda29d7ead7f38d95eac1f108aba00..435f87bd2a97a857320b559077492e60ecc481ac 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 @@ -922,10 +922,16 @@ sub searchForAuthorized2Fmodules { $session ||= $req->sessionInfo; my @am; foreach ( @{ $self->sfModules } ) { - $self->logger->debug( - 'Looking if ' . $_->{m}->prefix . '2f is available' ); - if ( $_->{r}->( $req, $session ) ) { - $self->logger->debug(' -> OK'); + my $authModuleName = $_->{m}->prefix; + $self->logger->debug('Looking for ' . $authModuleName . ' 2f'); + + # Adding targetAuthnLevel from req to session; + my $modifiedSession = {%$session}; + $modifiedSession->{targetAuthnLevel} = $req->{pdata}->{targetAuthnLevel} + if $req->{pdata}->{targetAuthnLevel}; + + if ( $_->{r}->( $req, $modifiedSession ) ) { + $self->logger->debug(' -> ' . $authModuleName . ' 2f is available'); push @am, $_->{m}; } } diff --git a/lemonldap-ng-portal/t/70-2F-TOTP-and-2Password-with-authnLevels-and-UpgradeOnly.t b/lemonldap-ng-portal/t/70-2F-TOTP-and-2Password-with-authnLevels-and-UpgradeOnly.t new file mode 100644 index 0000000000000000000000000000000000000000..dae7f38f63625d17fd3e02414746c7c54e502348 --- /dev/null +++ b/lemonldap-ng-portal/t/70-2F-TOTP-and-2Password-with-authnLevels-and-UpgradeOnly.t @@ -0,0 +1,231 @@ +use warnings; +use Test::More; +use strict; +use IO::String; + +require 't/test-lib.pm'; +require Lemonldap::NG::Common::TOTP; + +my $client = LLNG::Manager::Test->new( + { + ini => { + logLevel => 'error', + checkUser => 1, + sfOnlyUpgrade => 1, + totp2fSelfRegistration => 1, + totp2fActivation => "\$targetAuthnLevel <= 10", + totp2fAuthnLevel => 10, + handlerInternalCache => 5, + password2fSelfRegistration => 1, + password2fActivation => "\$targetAuthnLevel <= 5", + password2fAuthnLevel => 5, + authentication => 'Demo', + userDB => 'Same', + restSessionServer => 1, + vhostOptions => { + "test1.example.com" => { + vhostAuthnLevel => 5 + }, + "test2.example.com" => { + vhostAuthnLevel => 10 + } + } + + } + } +); +my $res; +my $id; +my $key; +my $keySecret; +my $token; +my $code; +subtest "Authenticate" => sub { + + ok( + $res = $client->_post( + '/', + IO::String->new('user=dwho&password=dwho'), + length => 23 + ), + 'Auth query' + ); + $id = expectCookie($res); + $client->logout($id); +}; + +subtest "Registering TOTP" => sub { + + ok( + $res = $client->_post( + '/', + IO::String->new('user=dwho&password=dwho'), + length => 23 + ), + 'Auth query' + ); + $id = expectCookie($res); + ok( + $res = $client->_get( + '/2fregisters/totp', + cookie => "lemonldap=$id", + accept => 'text/html' + ), + 'Form registration' + ); + ok( $res->[2]->[0] =~ /totpregistration\.(?:min\.)?js/, 'Found TOTP js' ); + + # JS query + ok( + $res = $client->_post( + '/2fregisters/totp/getkey', IO::String->new(''), + cookie => "lemonldap=$id", + length => 0, + ), + 'Get new key' + ); + eval { $res = JSON::from_json( $res->[2]->[0] ) }; + ok( not($@), 'Content is JSON' ) + or explain( $res->[2]->[0], 'JSON content' ); + ok( $keySecret = $res->{secret}, 'Found secret' ); + ok( $token = $res->{token}, 'Found token' ); + $key = Convert::Base32::decode_base32($keySecret); + + # Post code + ok( $code = Lemonldap::NG::Common::TOTP::_code( undef, $key, 0, 30, 6 ), + 'Code' ); + ok( $code =~ /^\d{6}$/, 'Code contains 6 digits' ); + my $s = "code=$code&token=$token&TOTPName=MyTOTP"; + ok( + $res = $client->_post( + '/2fregisters/totp/verify', + IO::String->new($s), + length => length($s), + cookie => "lemonldap=$id", + ), + 'Post code' + ); + eval { $res = JSON::from_json( $res->[2]->[0] ) }; + ok( not($@), 'Content is JSON' ) + or explain( $res->[2]->[0], 'JSON content' ); + ok( $res->{result} == 1, 'TOTP is registered' ); + + Time::Fake->offset("+10s"); + $client->logout($id); +}; + +subtest 'Register Password 2FA' => sub { + + ok( + $res = $client->_post( + '/', + IO::String->new('user=dwho&password=dwho'), + length => 23 + ), + 'Auth query' + ); + $id = expectCookie($res); + ok( $res = $client->_get( '/', accept => 'text/html' ), 'Get Menu', ); + my ( $host, $url, $query ) = + expectForm( $res, '#', undef, 'user', 'password' ); + + $query =~ s/user=/user=dwho/; + $query =~ s/password=/password=dwho/; + ok( + $res = $client->_post( + '/', + IO::String->new($query), + length => length($query), + accept => 'text/html', + ), + 'Auth query' + ); + my $id = expectCookie($res); + expectRedirection( $res, 'http://auth.example.com/' ); + + # Password form + ok( + $res = $client->_get( + '/2fregisters', + cookie => "lemonldap=$id", + accept => 'text/html', + ), + 'Form registration' + ); + + ok( + $res = $client->_get( + '/2fregisters/password', + cookie => "lemonldap=$id", + accept => 'text/html', + ), + 'Form registration' + ); + + ok( $res->[2]->[0] =~ /password2fregistration\.(?:min\.)?js/, + 'Found password js' ); + + my $s = "password=somethingyouknow&passwordverify=somethingyouknow"; + ok( + $client->_post( + '/2fregisters/password/verify', + IO::String->new($s), + length => length($s), + cookie => "lemonldap=$id", + ) + ); + $client->logout($id); +}; + +subtest "Check test1 vhost asks TOTP and 2Password" => sub { + + my $requestParams = + 'user=dwho&password=dwho&url=aHR0cDovL3Rlc3QxLmV4YW1wbGUuY29t'; + ok( + $res = $client->_post( + '/', + IO::String->new($requestParams), + length => length($requestParams), + ), + 'Auth query' + ); + $key = Convert::Base32::decode_base32($keySecret); + + # Post code + my ( $host, $url, $query ) = + expectForm( $res, undef, '/2fchoice', 'token' ); +}; + +subtest "Check test2 vhost asks only TOTP" => sub { + + my $requestParams = + 'user=dwho&password=dwho&url=aHR0cDovL3Rlc3QyLmV4YW1wbGUuY29t'; + ok( + $res = $client->_post( + '/', + IO::String->new($requestParams), + length => length($requestParams), + ), + 'Auth query' + ); + $key = Convert::Base32::decode_base32($keySecret); + + # Post code + my ( $host, $url, $query ) = + expectForm( $res, undef, '/totp2fcheck', 'token' ); + ok( $code = Lemonldap::NG::Common::TOTP::_code( undef, $key, 0, 30, 6 ), + 'Code' ); + $query =~ s/code=/code=$code/; + ok( + $res = $client->_post( + '/totp2fcheck', IO::String->new($query), + length => length($query), + ), + 'Post code' + ); + $id = expectCookie($res); + $client->logout($id); +}; + +clean_sessions(); +done_testing();