diff --git a/lemonldap-ng-portal/MANIFEST b/lemonldap-ng-portal/MANIFEST index 4e6bd672e30b7feafe37150820edb4d89fe7c28c..1eed476c562bdca4f0d98d9d0d3c796da3e5e88e 100644 --- a/lemonldap-ng-portal/MANIFEST +++ b/lemonldap-ng-portal/MANIFEST @@ -149,6 +149,7 @@ lib/Lemonldap/NG/Portal/Plugins/SingleSession.pm lib/Lemonldap/NG/Portal/Plugins/SOAPServer.pm lib/Lemonldap/NG/Portal/Plugins/Status.pm lib/Lemonldap/NG/Portal/Plugins/StayConnected.pm +lib/Lemonldap/NG/Portal/Plugins/TrustedBrowser.pm lib/Lemonldap/NG/Portal/Plugins/Upgrade.pm lib/Lemonldap/NG/Portal/Register/AD.pm lib/Lemonldap/NG/Portal/Register/Base.pm diff --git a/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Main/Init.pm b/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Main/Init.pm index b0496e409bec6442c905526dcc5eb32076951a76..db275a4b503559682c82abc5f1d8aadba73fea03 100644 --- a/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Main/Init.pm +++ b/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Main/Init.pm @@ -31,6 +31,7 @@ has _userDB => ( is => 'rw' ); has _passwordDB => ( is => 'rw' ); has _sfEngine => ( is => 'rw' ); has _captcha => ( is => 'rw' ); +has _trustedBrowser => ( is => 'rw' ); has loadedModules => ( is => 'rw' ); @@ -329,6 +330,15 @@ sub reloadConf { unless $self->{_sfEngine} = $self->loadPlugin( $self->conf->{'sfEngine'} ); + # Load trusted browser engine + return $self->fail + unless $self->_trustedBrowser( + $self->loadPlugin( + $self->conf->{'trustedBrowserEngine'} + || "::Plugins::TrustedBrowser" + ) + ); + # Load Captcha module return $self->fail unless $self->_captcha( diff --git a/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Main/Run.pm b/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Main/Run.pm index 794c895c9ca71cef6df0d5859b3afc41214a766f..53ed27f6ca2f2bf1dcd395afa7effcc41fccdc6e 100644 --- a/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Main/Run.pm +++ b/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Main/Run.pm @@ -40,7 +40,7 @@ sub sessionData { } sub validSession { - qw(storeHistory buildCookie); + qw(storeHistory rememberBrowser buildCookie); } # RESPONSE HANDLER @@ -1384,4 +1384,10 @@ sub buildUrl { return $uri->as_string; } +sub rememberBrowser { + my ( $self, $req ) = @_; + + return $self->_trustedBrowser->newDevice($req); +} + 1; diff --git a/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Plugins/StayConnected.pm b/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Plugins/StayConnected.pm index 87b6d51a0fac3eb9fc8e448259c5e5dbe03d886d..641a8fd2ba96a9229898587c1b1a694fd8de8bce 100644 --- a/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Plugins/StayConnected.pm +++ b/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Plugins/StayConnected.pm @@ -6,62 +6,22 @@ use strict; use Mouse; use Lemonldap::NG::Portal::Main::Constants qw( PE_OK - PE_SENDRESPONSE ); our $VERSION = '2.17.0'; extends qw( Lemonldap::NG::Portal::Main::Plugin - Lemonldap::NG::Portal::Lib::OtherSessions - Lemonldap::NG::Common::TOTP ); # INTERFACE -use constant endAuth => 'newDevice'; -use constant beforeAuth => 'check'; -use constant beforeLogout => 'logout'; - -# INITIALIZATION +use constant beforeAuth => 'check'; has rule => ( is => 'rw', default => sub { 0 } ); -has ott => ( - is => 'rw', - lazy => 1, - default => sub { - my $ott = - $_[0]->{p}->loadModule('Lemonldap::NG::Portal::Lib::OneTimeToken'); - $ott->timeout( $_[0]->conf->{formTimeout} ); - return $ott; - } -); -has cookieName => ( - is => 'ro', - lazy => 1, - default => sub { - $_[0]->conf->{stayConnectedCookieName} || 'llngconnection'; - } -); -has singleSession => ( - is => 'ro', - lazy => 1, - default => sub { - $_[0]->conf->{stayConnectedSingleSession}; - } -); - -# Default timeout: 1 month -has timeout => ( - is => 'ro', - lazy => 1, - default => sub { - $_[0]->conf->{stayConnectedTimeout} || 2592000; - } -); +# INITIALIZATION sub init { my ($self) = @_; - $self->addAuthRoute( registerbrowser => 'storeBrowser', ['POST'] ); # Parse activation rule $self->rule( @@ -71,298 +31,39 @@ sub init { return 1; } -# RUNNING METHODS - -# Registration: detect if user wants to stay connected. -# Then ask for browser fingerprint -sub newDevice { - my ( $self, $req ) = @_; - my $checkLogins = $req->param('checkLogins'); - $self->logger->debug("StayConnected: checkLogins set") if $checkLogins; - - if ( $req->param('stayconnected') - && $self->rule->( $req, $req->sessionInfo ) ) - { - my $totpSecret = $self->newSecret; - my $token = $self->ott->createToken( { - name => $req->sessionInfo->{ $self->conf->{whatToTrace} }, - totpSecret => $totpSecret, - ( - $checkLogins - ? ( history => $req->sessionInfo->{_loginHistory} ) - : () - ) - } - ); - $req->response( - $self->p->sendHtml( - $req, - '../common/registerBrowser', - params => { - URL => $req->urldc, - TOTPSEC => $totpSecret, - TOKEN => $token, - ACTION => '/registerbrowser', - CHECKLOGINS => $checkLogins - } - ) - ); - return PE_SENDRESPONSE; - } - return PE_OK; -} - -# Store data in a long-time session -sub storeBrowser { - my ( $self, $req ) = @_; - $req->urldc( $req->param('url') ); - $req->mustRedirect(1); - if ( $self->rule->( $req, $req->sessionInfo ) ) { - if ( my $token = $req->param('token') ) { - if ( my $tmp = $self->ott->getToken($token) ) { - my $uid = $req->userData->{ $self->conf->{whatToTrace} }; - if ( $tmp->{name} eq $uid ) { - if ( my $fg = $req->param('fg') ) { - my $isTotp = ( $fg =~ s/^TOTP_// ) ? 1 : 0; - if ( - $isTotp - and !$self->verifyCode( - 30, 1, 6, $tmp->{totpSecret}, $fg - ) - ) - { - $self->logger->warn( - "Failed to register device, bad TOTP"); - } - else { - my $ps = $self->newConnectionSession( { - _utime => time + $self->timeout, - _session_uid => $uid, - _connectedSince => time, - dataKeep => $req->data->{dataToKeep}, - ( - $isTotp - ? ( totpSecret => $tmp->{totpSecret} ) - : ( fingerprint => $fg ) - ), - }, - ); - - # Cookie available 30 days by default - $req->addCookie( - $self->p->cookie( - name => $self->cookieName, - value => $ps->id, - max_age => $self->timeout, - secure => $self->conf->{securedCookie}, - ) - ); - - $req->sessionInfo->{_loginHistory} = $tmp->{history} - if exists $tmp->{history}; - - # Store connection ID in current session - $self->p->updateSession( $req, - { _stayConnectedSession => $ps->id } ); - } - } - else { - $self->logger->warn( - "Browser did not return fingerprint"); - } - } - else { - $self->userLogger->error( - "StayConnected: mismatch UID ($tmp->{name} / $uid)"); - } - } - else { - $self->userLogger->error( - "StayConnected called with an expired token"); - } - } - else { - $self->userLogger->error('StayConnected called without token'); - } - } - else { - $self->userLogger->error('StayConnected not allowed'); - } - - # Return persistent connection cookie - return $self->p->do( $req, [ @{ $self->p->endAuth }, sub { PE_OK } ] ); -} - # Check for: -# - persistent connection cookie -# - valid session -# - uniq id is kept -# Then delete authentication methods from "steps" array. sub check { my ( $self, $req ) = @_; - if ( $self->rule->( $req, $req->sessionInfo ) ) { - if ( my $cid = $req->cookies->{ $self->cookieName } ) { - my $ps = Lemonldap::NG::Common::Session->new( - storageModule => $self->conf->{globalStorage}, - storageModuleOptions => $self->conf->{globalStorageOptions}, - kind => "SSO", - id => $cid, - ); - if ( $ps - and my $uid = $ps->data->{_session_uid} - and time() < $ps->data->{_utime} ) - { - $self->logger->debug('Persistent connection found'); - if ( my $fg = $req->param('fg') - and my $token = $req->param('token') ) - { - if ( my $prm = $self->ott->getToken($token) ) { - $req->data->{dataKeep} = $ps->data->{dataKeep}; - $self->logger->debug('Persistent connection found'); - if ( $self->conf->{stayConnectedBypassFG} ) { - return $self->skipAuthentication( $req, $uid, $cid, - 0 ); - } - else { - if ( $fg =~ s/^TOTP_// ) { - return $self->skipAuthentication( $req, $uid, - $cid, 1 ) - if $self->verifyCode( 30, 1, 6, - $ps->data->{totpSecret}, $fg ) > 0; - } - elsif ( $fg eq $ps->data->{fingerprint} ) { - return $self->skipAuthentication( $req, $uid, - $cid, 1 ); - } - $self->userLogger->warn( - "Fingerprint changed for $uid"); - $ps->remove; - $self->logout($req); - } - } - else { - $self->userLogger->notice( - "StayConnected: expired token for $uid"); - } - } - else { - my $token = $self->ott->createToken( $req->parameters ); - $req->response( - $self->p->sendHtml( - $req, - '../common/registerBrowser', - params => { - TOKEN => $token, - ACTION => '#', - } - ) - ); - return PE_SENDRESPONSE; - } - } - else { - $self->userLogger->notice('Persistent connection expired'); - unless ( $ps->{error} ) { - $self->logger->debug( - 'Persistent connection session id = ' . $ps->{id} ); - $self->logger->debug( - 'Persistent connection session _utime = ' - . $ps->data->{_utime} ); - $ps->remove; - } - } - } - } - else { - $self->userLogger->error('StayConnected not allowed'); + + if ( !$self->rule->( $req, $req->sessionInfo ) ) { + $self->logger->debug("Stay Connected not allowed"); } - return PE_OK; -} -sub logout { - my ( $self, $req ) = @_; - $req->addCookie( - $self->p->cookie( - name => $self->cookieName, - value => 0, - expires => 'Wed, 21 Oct 2015 00:00:00 GMT', - secure => $self->conf->{securedCookie}, - ) - ); + my $trustedBrowser = $self->p->_trustedBrowser; - # Try to clean stayconnected cookie - my $cid = $req->sessionInfo->{_stayConnectedSession}; - if ($cid) { - my $ps = Lemonldap::NG::Common::Session->new( - storageModule => $self->conf->{globalStorage}, - storageModuleOptions => $self->conf->{globalStorageOptions}, - kind => "SSO", - id => $cid, - ); - if ($ps) { - $self->logger->debug("Cleaning up StayConnected session $cid"); - $ps->remove; - } + # Run TrustedBrowser challenge + if ( $trustedBrowser->mustChallenge($req) ) { + return $trustedBrowser->challenge( $req, '#'); + } + elsif ( my $state = $trustedBrowser->getKnownBrowserState($req) ) { + return $self->skipAuthentication( $req, $state ); } - return PE_OK; } # Remove authentication steps from the login flow sub skipAuthentication { - my ( $self, $req, $uid, $cid, $fp ) = @_; + my ( $self, $req, $state ) = @_; + my $uid = $state->{_trustedUser}; $req->user($uid); - $req->sessionInfo->{_stayConnectedSession} = $cid; + $req->sessionInfo->{_stayConnectedSession} = + $state->{_stayConnectedSession}; my @steps = grep { ref $_ or $_ !~ /^(?:extractFormInfo|authenticate)$/ } @{ $req->steps }; $req->steps( \@steps ); - $self->userLogger->notice( "$uid connected by StayConnected cookie" - . ( $fp ? "" : " without fingerprint checking" ) ); + $self->userLogger->notice("$uid connected by StayConnected cookie"); return PE_OK; } -sub removeExistingSessions { - my ( $self, $uid ) = @_; - $self->logger->debug("StayConnected: removing all sessions for $uid"); - - my $sessions = - $self->module->searchOn( $self->moduleOpts, '_session_uid', $uid ); - - foreach ( keys %{ $sessions || {} } ) { - if ( - my $ps = Lemonldap::NG::Common::Session->new( - storageModule => $self->conf->{globalStorage}, - storageModuleOptions => $self->conf->{globalStorageOptions}, - kind => "SSO", - id => $_, - ) - ) - { - # If this is a StayConnected session, remove it - $ps->remove if $ps->{data}->{_connectedSince}; - $self->logger->debug("StayConnected removed session $_"); - } - else { - $self->logger->debug("StayConnected session $_ expired"); - } - } -} - -sub newConnectionSession { - my ( $self, $info ) = @_; - - # Remove existing sessions - if ( $self->singleSession ) { - $self->removeExistingSessions( $info->{_session_uid} ); - } - - return Lemonldap::NG::Common::Session->new( - storageModule => $self->conf->{globalStorage}, - storageModuleOptions => $self->conf->{globalStorageOptions}, - kind => "SSO", - info => $info, - ); -} - 1; diff --git a/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Plugins/TrustedBrowser.pm b/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Plugins/TrustedBrowser.pm new file mode 100644 index 0000000000000000000000000000000000000000..3cf1510c9c448cc539753a27f51873fd8ab839f5 --- /dev/null +++ b/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Plugins/TrustedBrowser.pm @@ -0,0 +1,408 @@ +# Trusted Browser service +# This package provides the ability to remember a browser across SSO logins +# It is meant to be used by multiple features +# - Auto-login (StayConnected) +# - 2FA bypass +# +# Browser registration is entirely handled by this plugin, and trigger by setting +# $req->param('stayconnected'), usually by the user clicking a checkbox. +# Request state is handled automatically during browser registration. +# +# Recognition of an already registered browser is not triggered by the plugin +# itself but by other plugins (StayConnected, 2FA engine). +# Plugins should call 'mustChallenge' to know if they need to start a TOTP +# challenge with the web browser. If it is the case, they run 'challenge' to +# prepare the validation form containing the JS challenge. +# +# Plugins must indicate a route for the challenge form to POST to. Plugins must +# implement that route, and in that route, call 'getKnownBrowserState' to check +# that the browser really is trusted, and then, continue the current flow. +# +# Plugins are responsible for saving the request state and restoring it during +# the recognition phase. +# + +package Lemonldap::NG::Portal::Plugins::TrustedBrowser; + +use strict; +use Mouse; +use Lemonldap::NG::Portal::Main::Constants qw( + PE_OK + PE_ERROR + PE_SENDRESPONSE +); + +our $VERSION = '2.17.0'; + +extends qw( + Lemonldap::NG::Portal::Main::Plugin + Lemonldap::NG::Portal::Lib::OtherSessions + Lemonldap::NG::Common::TOTP +); + +# INTERFACE + +use constant beforeLogout => 'logout'; + +# INITIALIZATION +has ott => ( + is => 'rw', + lazy => 1, + default => sub { + my $ott = + $_[0]->{p}->loadModule('Lemonldap::NG::Portal::Lib::OneTimeToken'); + $ott->timeout( $_[0]->conf->{formTimeout} ); + return $ott; + } +); +has cookieName => ( + is => 'ro', + lazy => 1, + default => sub { + $_[0]->conf->{stayConnectedCookieName} || 'llngconnection'; + } +); +has singleSession => ( + is => 'ro', + lazy => 1, + default => sub { + $_[0]->conf->{stayConnectedSingleSession}; + } +); + +# Default timeout: 1 month +has timeout => ( + is => 'ro', + lazy => 1, + default => sub { + $_[0]->conf->{stayConnectedTimeout} || 2592000; + } +); + +sub init { + my ($self) = @_; + $self->addUnauthRoute( registerbrowser => 'storeBrowser', ['POST'] ); + + return 1; +} + +# RUNNING METHODS + +# This method is called by the portal in standard auth flows + +sub newDevice { + my ( $self, $req ) = @_; + + if ( $req->param('stayconnected') ) { + my $totpSecret = $self->newSecret; + + my $state = $self->_getRegisterStateTplParams( $req, $totpSecret ); + $req->response( + $self->p->sendHtml( + $req, + '../common/registerBrowser', + params => { + TOTPSEC => $totpSecret, + ACTION => '/registerbrowser', + %$state, + } + ) + ); + return PE_SENDRESPONSE; + } + return PE_OK; +} + +# Store TOTP secret + request state into a temporary session +# and return relevant template parameters +sub _getRegisterStateTplParams { + my ( $self, $req, $totpSecret ) = @_; + + my $checkLogins = $req->param('checkLogins') || 0; + my $url = $req->urldc; + my $token = $self->ott->createToken( { + id => $req->id, + sessionInfo => $req->sessionInfo, + totpSecret => $totpSecret, + } + ); + my %state = ( + CHECKLOGINS => $checkLogins, + URL => $url, + TOKEN => $token, + ); + return \%state; +} + +# This method handles the browser response to the registerBrowser JS code +# and persists the registration +sub storeBrowser { + my ( $self, $req ) = @_; + + if ( my $totpSecret = $self->_restoreRegisterState($req) ) { + my $name = $req->sessionInfo->{ $self->conf->{whatToTrace} }; + if ( my $fg = $req->param('fg') ) { + my $isTotp = ( $fg =~ s/^TOTP_// ) ? 1 : 0; + if ( $isTotp + and !$self->verifyCode( 30, 1, 6, $totpSecret, $fg ) ) + { + $self->logger->warn( "Failed to register device, bad TOTP, " + . "device will not be remembered" ); + } + else { + my $ps = $self->newConnectionSession( { ( + _session_uid => $name, + $isTotp + ? ( totpSecret => $totpSecret ) + : ( fingerprint => $fg ) + ), + }, + ); + + # Cookie available 30 days by default + $req->addCookie( + $self->p->cookie( + name => $self->cookieName, + value => $ps->id, + max_age => $self->timeout, + secure => $self->conf->{securedCookie}, + ) + ); + + # Store connection ID in current session + $self->p->updateSession( $req, + { _stayConnectedSession => $ps->id } ); + } + } + else { + $self->logger->warn( "Browser did not return fingerprint, " + . "browser will not be remembered" ); + } + } + else { + $self->userLogger->error( + "Cannot restore trusted browser registration state"); + return $self->p->do( $req, [ sub { PE_ERROR } ] ); + } + + # Resume normal login flow + $req->mustRedirect(1); + return $self->p->do( $req, + [ 'buildCookie', @{ $self->p->endAuth }, sub { PE_OK } ] ); +} + +sub _restoreRegisterState { + my ( $self, $req ) = @_; + + if ( my $token = $req->param('token') ) { + if ( my $saved_state = $self->ott->getToken($token) ) { + + # checkLogins is restored automatically as a request parameter + $req->urldc( $req->param('url') ); + $req->sessionInfo( $saved_state->{sessionInfo} ); + $req->id( $saved_state->{id} ); + return $saved_state->{totpSecret}; + } + else { + $self->userLogger->error("Invalid trusted browser state token"); + } + } + else { + $self->userLogger->error('Missing trusted browser state token'); + } + return; + +} + +sub getTrustedBrowserSessionFromReq { + my ( $self, $req ) = @_; + + if ( my $cid = $req->cookies->{ $self->cookieName } ) { + return $self->getTrustedBrowserSession($cid); + } + else { + $self->logger->debug("No trusted browser cookie was sent"); + } + return; +} + +sub getTrustedBrowserSession { + my ( $self, $cid ) = @_; + my $ps = Lemonldap::NG::Common::Session->new( + storageModule => $self->conf->{globalStorage}, + storageModuleOptions => $self->conf->{globalStorageOptions}, + kind => "SSO", + id => $cid, + ); + if ( $ps->data->{_session_uid} and ( time() < $ps->data->{_utime} ) ) { + $self->logger->debug('Persistent connection found'); + return $ps; + } + else { + $self->userLogger->notice('Persistent connection expired'); + unless ( $ps->{error} ) { + $self->logger->debug( + 'Persistent connection session id = ' . $ps->{id} ); + $self->logger->debug( 'Persistent connection session _utime = ' + . $ps->data->{_utime} ); + $ps->remove; + } + } + return; +} + +sub _hasFingerprintParams { + my ( $self, $req ) = @_; + return ( $req->param('fg') and $req->param('token') ); +} + +sub mustChallenge { + my ( $self, $req, $expected_user ) = @_; + + my $ps = $self->getTrustedBrowserSessionFromReq($req); + if ( $ps and not( $self->_hasFingerprintParams($req) ) ) { + if ($expected_user) { + return ( $ps->data->{_session_uid} + and $ps->data->{_session_uid} eq $expected_user ); + } + else { + return 1; + } + } + return 0; +} + +sub getKnownBrowserState { + my ( $self, $req ) = @_; + + my $ps = $self->getTrustedBrowserSessionFromReq($req); + if ( $ps and $self->_hasFingerprintParams($req) ) { + my $fg = $req->param('fg'); + my $token = $req->param('token'); + my $uid = $ps->data->{_session_uid}; + + my $token_data = $self->ott->getToken($token); + return unless $token_data; + + if ( $self->checkFingerprint( $req, $ps, $uid, $fg ) ) { + return { + _trustedUser => $uid, + _stayConnectedSession => $ps->id, + data => $token_data, + }; + } + else { + $self->userLogger->warn("Fingerprint changed for $uid"); + $ps->remove; + $self->logout($req); + } + } + else { + $self->logger->debug("No fingerprint or token parameter was sent"); + } + return; +} + +sub checkFingerprint { + my ( $self, $req, $ps, $uid, $fg ) = @_; + $self->logger->debug('Persistent connection found'); + if ( $self->conf->{stayConnectedBypassFG} ) { + return 1; + } + else { + if ( $fg =~ s/^TOTP_// ) { + return 1 + if ( + $self->verifyCode( 30, 1, 6, $ps->data->{totpSecret}, $fg ) > + 0 ); + } + elsif ( $fg eq $ps->data->{fingerprint} ) { + return 1; + } + } + return 0; +} + +sub challenge { + my ( $self, $req, $action, $info ) = @_; + my $token = $self->ott->createToken($info); + $req->response( + $self->p->sendHtml( + $req, + '../common/registerBrowser', + params => { + TOKEN => $token, + ACTION => $action, + } + ) + ); + return PE_SENDRESPONSE; +} + +sub logout { + my ( $self, $req ) = @_; + $req->addCookie( + $self->p->cookie( + name => $self->cookieName, + value => 0, + expires => 'Wed, 21 Oct 2015 00:00:00 GMT', + secure => $self->conf->{securedCookie}, + ) + ); + + # Try to clean stayconnected cookie + my $cid = $req->sessionInfo->{_stayConnectedSession}; + if ($cid) { + my $ps = $self->getTrustedBrowserSession($cid); + if ($ps) { + $self->logger->debug("Cleaning up StayConnected session $cid"); + $ps->remove; + } + } + + return PE_OK; +} + +sub removeExistingSessions { + my ( $self, $uid ) = @_; + $self->logger->debug("StayConnected: removing all sessions for $uid"); + + my $sessions = + $self->module->searchOn( $self->moduleOpts, '_session_uid', $uid ); + + foreach ( keys %{ $sessions || {} } ) { + if ( my $ps = $self->getTrustedBrowserSession($_) ) { + + # If this is a StayConnected session, remove it + $ps->remove if $ps->{data}->{_connectedSince}; + $self->logger->debug("StayConnected removed session $_"); + } + else { + $self->logger->debug("StayConnected session $_ expired"); + } + } +} + +sub newConnectionSession { + my ( $self, $info ) = @_; + + $info ||= {}; + + # Remove existing sessions + if ( $self->singleSession ) { + $self->removeExistingSessions( $info->{_session_uid} ); + } + + return Lemonldap::NG::Common::Session->new( + storageModule => $self->conf->{globalStorage}, + storageModuleOptions => $self->conf->{globalStorageOptions}, + kind => "SSO", + info => { + _utime => time + $self->timeout, + _connectedSince => time, + %$info, + } + ); +} + +1; diff --git a/lemonldap-ng-portal/t/64-StayConnected-single-connection.t b/lemonldap-ng-portal/t/64-StayConnected-single-connection.t new file mode 100644 index 0000000000000000000000000000000000000000..d1387ca8b275631d40b6198d201e45568559cb8c --- /dev/null +++ b/lemonldap-ng-portal/t/64-StayConnected-single-connection.t @@ -0,0 +1,118 @@ +use warnings; +use Test::More; +use strict; +use IO::String; + +require 't/test-lib.pm'; + +my $client = LLNG::Manager::Test->new( + { + ini => { + logLevel => 'error', + useSafeJail => 1, + stayConnected => '$env->{REMOTE_ADDR} eq "127.0.0.1"', + loginHistoryEnabled => 1, + securedCookie => 1, + stayConnectedTimeout => 1000, + stayConnectedCookieName => 'llngpersistent', + stayConnectedSingleSession => 1, + portalMainLogo => 'common/logos/logo_llng_old.png', + accept => 'text/html', + } + } +); + +sub login_create_persistent_cookie_from_scratch { + my ($client) = @_; + my $res; + + ok( + $res = $client->_post( + '/', + IO::String->new('user=dwho&password=dwho&stayconnected=1'), + length => 39 + ), + 'Auth query' + ); + count(1); + my ( $host, $url, $query ) = + expectForm( $res, undef, '/registerbrowser', 'fg', 'token' ); + + # Push fingerprint + $query =~ s/fg=/fg=aaa/; + ok( + $res = $client->_post( + '/registerbrowser', + IO::String->new($query), + length => length($query), + accept => 'text/html', + ), + 'Post fingerprint' + ); + count(1); + my $id = expectCookie($res); + expectRedirection( $res, 'http://auth.example.com/' ); + my $cid = expectCookie( $res, 'llngpersistent' ); + return ( $id, $cid ); +} + +sub try_connect_with_persistent_cookie { + my ( $client, $cid ) = @_; + my $res; + ok( + $res = $client->_get( + '/', + cookie => "llngpersistent=$cid", + accept => 'text/html', + ), + 'Try to auth with persistent cookie' + ); + count(1); + expectOK($res); + if ( $res->[2]->[0] =~ qr/Register browser/ ) { + my ( $host, $url, $query ) = + expectForm( $res, '#', undef, 'fg', 'token' ); + + # Push fingerprint + $query =~ s/fg=/fg=aaa/; + ok( + $res = $client->_post( + '/', + IO::String->new($query), + cookie => "llngpersistent=$cid", + length => length($query), + accept => 'text/html', + ), + 'Post fingerprint' + ); + count(1); + expectRedirection( $res, 'http://auth.example.com/' ); + return expectCookie($res); + } + else { + return; + } +} + +# Create a persistent connection +my ( $id, $cid ) = login_create_persistent_cookie_from_scratch($client); +$id = try_connect_with_persistent_cookie( $client, $cid ); +ok( $id, "Got cookie" ); +$id = try_connect_with_persistent_cookie( $client, $cid ); +ok( $id, "Got cookie" ); +count(2); + +# Create a second persistent connection +my ( $id2, $cid2 ) = login_create_persistent_cookie_from_scratch($client); +$id2 = try_connect_with_persistent_cookie( $client, $cid2 ); +ok( $id2, "Got cookie" ); +count(1); + +# First persistent cookie should not be valid anymore +$id = try_connect_with_persistent_cookie( $client, $cid ); +ok( !$id, "First persistent ID is no longer valid" ); +count(1); + +clean_sessions(); +done_testing( count() ); + diff --git a/lemonldap-ng-portal/t/64-StayConnected-singleSession.t b/lemonldap-ng-portal/t/64-StayConnected-singleSession.t index 50fb0831287c71287ee26c85fa247f3513436e87..b3b3c84a30e0d1fd9c7546a4748e9eaf21300916 100644 --- a/lemonldap-ng-portal/t/64-StayConnected-singleSession.t +++ b/lemonldap-ng-portal/t/64-StayConnected-singleSession.t @@ -5,25 +5,22 @@ use IO::String; require 't/test-lib.pm'; -my $client = LLNG::Manager::Test->new( - { +my $client = LLNG::Manager::Test->new( { ini => { logLevel => 'error', - useSafeJail => 1, - stayConnected => '$env->{REMOTE_ADDR} eq "127.0.0.1"', + stayConnected => 1, + singleSession => 1, + notifyDeleted => 1, loginHistoryEnabled => 1, securedCookie => 1, stayConnectedTimeout => 1000, - stayConnectedCookieName => 'llngpersistent', stayConnectedSingleSession => 1, - portalMainLogo => 'common/logos/logo_llng_old.png', - accept => 'text/html', } } ); sub login_create_persistent_cookie_from_scratch { - my ($client) = @_; + my ( $client, $notifydeleted ) = @_; my $res; ok( @@ -34,8 +31,6 @@ sub login_create_persistent_cookie_from_scratch { ), 'Auth query' ); - count(1); - my $id = expectCookie($res); my ( $host, $url, $query ) = expectForm( $res, undef, '/registerbrowser', 'fg', 'token' ); @@ -46,29 +41,63 @@ sub login_create_persistent_cookie_from_scratch { '/registerbrowser', IO::String->new($query), length => length($query), - cookie => "lemonldap=$id", accept => 'text/html', ), 'Post fingerprint' ); - count(1); - expectRedirection( $res, 'http://auth.example.com/' ); - my $cid = expectCookie( $res, 'llngpersistent' ); + + my $id = expectCookie($res); + if ($notifydeleted) { + like( $res->[2]->[0], qr/sessionsDeleted/, "Show deleted sessions" ); + expectForm( $res, "auth.example.com", "/" ); + } + else { + expectRedirection( $res, 'http://auth.example.com/' ); + } + my $cid = expectCookie( $res, 'llngconnection' ); return ( $id, $cid ); } +sub sessionid_not_valid { + my ( $client, $id ) = @_; + my $res; + ok( + $res = $client->_get( + '/', + cookie => "lemonldap=$id", + accept => 'text/html', + ), + 'Check session validity' + ); + is( getHeader( $res, 'Lm-Remote-User' ), + undef, "Session ID no longer valid" ); +} + +sub sessionid_still_valid { + my ( $client, $id ) = @_; + my $res; + ok( + $res = $client->_get( + '/', + cookie => "lemonldap=$id", + accept => 'text/html', + ), + 'Check session validity' + ); + expectAuthenticatedAs( $res, "dwho" ); +} + sub try_connect_with_persistent_cookie { - my ( $client, $cid ) = @_; + my ( $client, $cid, $notifydeleted ) = @_; my $res; ok( $res = $client->_get( '/', - cookie => "llngpersistent=$cid", + cookie => "llngconnection=$cid", accept => 'text/html', ), 'Try to auth with persistent cookie' ); - count(1); expectOK($res); if ( $res->[2]->[0] =~ qr/Register browser/ ) { my ( $host, $url, $query ) = @@ -80,14 +109,20 @@ sub try_connect_with_persistent_cookie { $res = $client->_post( '/', IO::String->new($query), - cookie => "llngpersistent=$cid", + cookie => "llngconnection=$cid", length => length($query), accept => 'text/html', ), 'Post fingerprint' ); - count(1); - expectRedirection( $res, 'http://auth.example.com/' ); + if ($notifydeleted) { + like( $res->[2]->[0], qr/sessionsDeleted/, + "Show deleted sessions" ); + expectForm( $res, "auth.example.com", "/" ); + } + else { + expectRedirection( $res, 'http://auth.example.com/' ); + } return expectCookie($res); } else { @@ -95,25 +130,36 @@ sub try_connect_with_persistent_cookie { } } -# Create a persistent connection -my ( $id, $cid ) = login_create_persistent_cookie_from_scratch($client); -$id = try_connect_with_persistent_cookie( $client, $cid ); -ok( $id, "Got cookie" ); -$id = try_connect_with_persistent_cookie( $client, $cid ); -ok( $id, "Got cookie" ); -count(2); - -# Create a second persistent connection -my ( $id2, $cid2 ) = login_create_persistent_cookie_from_scratch($client); -$id2 = try_connect_with_persistent_cookie( $client, $cid2 ); -ok( $id2, "Got cookie" ); -count(1); - -# First persistent cookie should not be valid anymore -$id = try_connect_with_persistent_cookie( $client, $cid ); -ok( !$id, "First persistent ID is no longer valid" ); -count(1); +subtest "Login with stay connected, then with persistent cookie" + . ", user sees notification" => sub { + + clean_sessions(); + + # Create a persistent connection + my ( $id, $cid ) = login_create_persistent_cookie_from_scratch($client); + sessionid_still_valid( $client, $id ); + + # NotifyDeleted must be shown + my $id2 = try_connect_with_persistent_cookie( $client, $cid, 1 ); + sessionid_still_valid( $client, $id2 ); + sessionid_not_valid( $client, $id ); + }; + +subtest "Login with stay connected, then without persistent cookie" + . ", user sees notification" => sub { + + clean_sessions(); + + # Create a persistent connection + my ( $id, $cid ) = login_create_persistent_cookie_from_scratch($client); + sessionid_still_valid( $client, $id ); + + # NotifyDeleted must be shown + my ( $id2, $cid2 ) = + login_create_persistent_cookie_from_scratch( $client, 1 ); + sessionid_not_valid( $client, $id ); + }; clean_sessions(); -done_testing( count() ); +done_testing(); diff --git a/lemonldap-ng-portal/t/64-StayConnected-with-2F-and-History.t b/lemonldap-ng-portal/t/64-StayConnected-with-2F-and-History.t index 3e6b1bbdf0800bf93d2b2060843d089d28a6c8a1..b0934a26be5b2b23e1a774d16859ddd8ecf81019 100644 --- a/lemonldap-ng-portal/t/64-StayConnected-with-2F-and-History.t +++ b/lemonldap-ng-portal/t/64-StayConnected-with-2F-and-History.t @@ -229,7 +229,6 @@ JjTJecOOS+88fK8qL1TrYv5rapIdqUI7aQ== ), 'Post code' ); - $id = expectCookie($res); ( $host, $url, $query ) = expectForm( $res, undef, '/registerbrowser', 'fg', 'token' ); @@ -240,11 +239,11 @@ JjTJecOOS+88fK8qL1TrYv5rapIdqUI7aQ== '/registerbrowser', IO::String->new($query), length => length($query), - cookie => "lemonldap=$id", accept => 'text/html', ), 'Post fingerprint' ); + $id = expectCookie($res); my $cid = expectCookie( $res, 'llngconnection' ); # History is displayed @@ -334,17 +333,6 @@ JjTJecOOS+88fK8qL1TrYv5rapIdqUI7aQ== 'Push U2F signature' ); - # See https://github.com/mschout/perl-authen-u2f-tester/issues/2 - if ( $Authen::U2F::Tester::VERSION >= 0.03 ) { - $id = expectCookie($res); - } - else { - count(1); - pass( -'Authen::2F::Tester-0.02 signatures are not recognized by Yubico library' - ); - } - ( $host, $url, $query ) = expectForm( $res, undef, '/registerbrowser', 'fg', 'token' ); @@ -355,11 +343,22 @@ JjTJecOOS+88fK8qL1TrYv5rapIdqUI7aQ== '/registerbrowser', IO::String->new($query), length => length($query), - cookie => "lemonldap=$id", accept => 'text/html', ), 'Post fingerprint' ); + + # See https://github.com/mschout/perl-authen-u2f-tester/issues/2 + if ( $Authen::U2F::Tester::VERSION >= 0.03 ) { + $id = expectCookie($res); + } + else { + count(1); + pass( +'Authen::2F::Tester-0.02 signatures are not recognized by Yubico library' + ); + } + $cid = expectCookie( $res, 'llngconnection' ); # History is displayed diff --git a/lemonldap-ng-portal/t/64-StayConnected-with-History.t b/lemonldap-ng-portal/t/64-StayConnected-with-History.t index 5b0d9fd8edd86e7ffdb5078ebf6711525cf911db..e34c30867b76bfb363989aa6f94030a9d302ec34 100644 --- a/lemonldap-ng-portal/t/64-StayConnected-with-History.t +++ b/lemonldap-ng-portal/t/64-StayConnected-with-History.t @@ -34,7 +34,6 @@ ok( 'Auth query' ); count(1); -my $id = expectCookie($res); my ( $host, $url, $query ) = expectForm( $res, undef, '/registerbrowser', 'fg', 'token' ); @@ -46,12 +45,11 @@ ok( '/registerbrowser', IO::String->new($query), length => length($query), - cookie => "lemonldap=$id", accept => 'text/html', ), 'Post fingerprint' ); -expectRedirection( $res, 'http://auth.example.com/' ); +expectPortalError( $res, 24 ); count(1); # Try to authenticate @@ -65,7 +63,6 @@ ok( 'Auth query' ); count(1); -$id = expectCookie($res); ( $host, $url, $query ) = expectForm( $res, undef, '/registerbrowser', 'fg', 'token' ); @@ -76,11 +73,11 @@ ok( '/registerbrowser', IO::String->new($query), length => length($query), - cookie => "lemonldap=$id", accept => 'text/html', ), 'Post fingerprint' ); +my $id = expectCookie($res); expectRedirection( $res, 'http://auth.example.com/' ); my $cid = expectCookie( $res, 'llngpersistent' ); ok( $res->[1]->[5] =~ /\bsecure\b/, ' Secure cookie found' ) @@ -192,7 +189,6 @@ ok( 'Auth query' ); count(1); -$id = expectCookie($res); ( $host, $url, $query ) = expectForm( $res, undef, '/registerbrowser', 'fg', 'token' ); @@ -203,12 +199,12 @@ ok( '/registerbrowser', IO::String->new($query), length => length($query), - cookie => "lemonldap=$id", accept => 'text/html', ), 'Post fingerprint' ); count(1); +$id = expectCookie($res); $cid = expectCookie( $res, 'llngpersistent' ); ok( $res->[2]->[0] =~ qr%new($query), length => length($query), - cookie => "lemonldap=$id", accept => 'text/html', ), 'Post fingerprint' ); expectRedirection( $res, 'http://auth.example.com/' ); +my $id = expectCookie($res); my $cid = expectCookie( $res, 'llngconnection' ); ok( $res->[1]->[5] =~ /\bHttpOnly=1\b/, ' HTTP cookie found' ) or explain( $res->[1]->[5], 'HTTP cookie found' ); diff --git a/lemonldap-ng-portal/t/64-StayConnected.t b/lemonldap-ng-portal/t/64-StayConnected.t index 83a384232fed938d06ccd1b6c2ffa8dcbb66c78e..6aeac8c470acfb64824155d9a1a2562ebd8519c8 100644 --- a/lemonldap-ng-portal/t/64-StayConnected.t +++ b/lemonldap-ng-portal/t/64-StayConnected.t @@ -31,7 +31,6 @@ subtest "Register session, use it, then logout" => sub { ), 'Auth query' ); - my $id = expectCookie($res); my ( $host, $url, $query ) = expectForm( $res, undef, '/registerbrowser', 'fg', 'token' ); @@ -42,11 +41,11 @@ subtest "Register session, use it, then logout" => sub { '/registerbrowser', IO::String->new($query), length => length($query), - cookie => "lemonldap=$id", accept => 'text/html', ), 'Post fingerprint' ); + my $id = expectCookie($res); expectRedirection( $res, 'http://auth.example.com/' ); my $cid = expectCookie( $res, 'llngpersistent' ); ok( $res->[1]->[5] =~ /\bsecure\b/, ' Secure cookie found' ) @@ -99,7 +98,6 @@ subtest "Make sure connection ID is saved on first login too" => sub { ), 'Auth query' ); - my $id = expectCookie($res); my ( $host, $url, $query ) = expectForm( $res, undef, '/registerbrowser', 'fg', 'token' ); @@ -110,11 +108,11 @@ subtest "Make sure connection ID is saved on first login too" => sub { '/registerbrowser', IO::String->new($query), length => length($query), - cookie => "lemonldap=$id", accept => 'text/html', ), 'Post fingerprint' ); + my $id = expectCookie($res); expectRedirection( $res, 'http://auth.example.com/' ); my $cid = expectCookie( $res, 'llngpersistent' ); ok( $res->[1]->[5] =~ /\bsecure\b/, ' Secure cookie found' )