diff --git a/lemonldap-ng-portal/MANIFEST b/lemonldap-ng-portal/MANIFEST
index 8af2a63d9..9f5bfc770 100644
--- a/lemonldap-ng-portal/MANIFEST
+++ b/lemonldap-ng-portal/MANIFEST
@@ -633,6 +633,7 @@ t/67-CheckUser-with-issuer-SAML-POST.t
t/67-CheckUser-with-token.t
t/67-CheckUser.t
t/68-ContextSwitching-with-Logout.t
+t/68-ContextSwitching-with-TOTP-and-Notification.t
t/68-ContextSwitching.t
t/68-Impersonation-with-doubleCookies.t
t/68-Impersonation-with-filtered-merge.t
diff --git a/lemonldap-ng-portal/t/68-ContextSwitching-with-TOTP-and-Notification.t b/lemonldap-ng-portal/t/68-ContextSwitching-with-TOTP-and-Notification.t
new file mode 100644
index 000000000..31a0e3358
--- /dev/null
+++ b/lemonldap-ng-portal/t/68-ContextSwitching-with-TOTP-and-Notification.t
@@ -0,0 +1,316 @@
+use Test::More;
+use strict;
+use IO::String;
+
+require 't/test-lib.pm';
+
+my $res;
+my $file = "$main::tmpDir/20160530_msmith_dGVzdHJlZg==.json";
+
+open F, "> $file" or die($!);
+print F '[
+{
+ "uid": "msmith",
+ "date": "2016-05-30",
+ "reference": "testref",
+ "title": "Test title",
+ "subtitle": "Test subtitle",
+ "text": "This is a test text",
+ "check": ["Accept test","Accept test2"]
+}
+]';
+close F;
+
+my $client = LLNG::Manager::Test->new( {
+ ini => {
+ logLevel => 'error',
+ authentication => 'Demo',
+ userDB => 'Same',
+ loginHistoryEnabled => 0,
+ portalMainLogo => 'common/logos/logo_llng_old.png',
+ contextSwitchingRule => 1,
+ contextSwitchingIdRule => 1,
+ totp2fSelfRegistration => 1,
+ totp2fActivation => 1,
+ contextSwitchingStopWithLogout => 0,
+ notification => 1,
+ notificationStorage => 'File',
+ notificationStorageOptions => { dirName => $main::tmpDir },
+ }
+ }
+);
+
+## Try to authenticate
+ok(
+ $res = $client->_post(
+ '/',
+ IO::String->new('user=dwho&password=dwho'),
+ length => 23,
+ accept => 'text/html',
+ ),
+ 'Auth query'
+);
+count(1);
+my $id = expectCookie($res);
+expectRedirection( $res, 'http://auth.example.com/' );
+
+# 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' );
+my ( $key, $token );
+ok( $key = $res->{secret}, 'Found secret' );
+ok( $token = $res->{token}, 'Found token' );
+$key = Convert::Base32::decode_base32($key);
+count(4);
+
+# Post code
+my $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";
+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, 'Key is registered' );
+count(5);
+$client->logout($id);
+
+## Try to authenticate
+ok( $res = $client->_get( '/', accept => 'text/html' ), 'Get Menu', );
+my ( $host, $url, $query ) = expectForm( $res, '#', undef, 'user', 'password' );
+$query =~ s/user=/user=rtyler/;
+$query =~ s/password=/password=rtyler/;
+ok(
+ $res = $client->_post(
+ '/',
+ IO::String->new($query),
+ length => length($query),
+ accept => 'text/html',
+ ),
+ 'Auth query'
+);
+count(2);
+$id = expectCookie($res);
+expectRedirection( $res, 'http://auth.example.com/' );
+
+# Get Menu
+# ------------------------
+ok(
+ $res = $client->_get(
+ '/',
+ cookie => "lemonldap=$id",
+ accept => 'text/html'
+ ),
+ 'Get Menu',
+);
+expectOK($res);
+ok(
+ $res->[2]->[0] =~ m%Connected as rtyler%,
+ 'Connected as rtyler'
+) or print STDERR Dumper( $res->[2]->[0] );
+expectAuthenticatedAs( $res, 'rtyler' );
+ok(
+ $res->[2]->[0] =~
+ m%contextSwitching_ON%,
+ 'Connected as rtyler'
+) or print STDERR Dumper( $res->[2]->[0] );
+count(3);
+
+# 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' );
+my ( $key, $token );
+ok( $key = $res->{secret}, 'Found secret' );
+ok( $token = $res->{token}, 'Found token' );
+$key = Convert::Base32::decode_base32($key);
+count(4);
+
+# Post code
+my $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";
+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, 'Key is registered' );
+count(5);
+
+# ContextSwitching form
+# ------------------------
+ok(
+ $res = $client->_get(
+ '/switchcontext',
+ cookie => "lemonldap=$id",
+ accept => 'text/html'
+ ),
+ 'ContextSwitching form',
+);
+( $host, $url, $query ) =
+ expectForm( $res, undef, '/switchcontext', 'spoofId' );
+ok( $res->[2]->[0] =~ m%%,
+ 'Found trspan="contextSwitching_ON"' )
+ or explain( $res->[2]->[0], 'trspan="contextSwitching_ON"' );
+
+## POST form
+$query =~ s/spoofId=/spoofId=dwho/;
+ok(
+ $res = $client->_post(
+ '/switchcontext',
+ IO::String->new($query),
+ cookie => "lemonldap=$id",
+ length => length($query),
+ accept => 'text/html',
+ ),
+ 'POST switchcontext'
+);
+ok( $res->[2]->[0] =~ m%%,
+ 'Found trspan="contextSwitching_OFF"' )
+ or explain( $res->[2]->[0], 'trspan="contextSwitching_OFF"' );
+$id = expectCookie($res);
+ok(
+ $res = $client->_get(
+ '/',
+ cookie => "lemonldap=$id",
+ accept => 'text/html'
+ ),
+ 'Get Menu',
+);
+expectAuthenticatedAs( $res, 'dwho' );
+ok( $res->[2]->[0] =~ m%%,
+ 'Found trspan="contextSwitching_OFF"' )
+ or explain( $res->[2]->[0], 'trspan="contextSwitching_OFF"' );
+count(6);
+
+# Stop ContextSwitching
+# ------------------------
+ok(
+ $res = $client->_get(
+ '/switchcontext',
+ cookie => "lemonldap=$id",
+ accept => 'text/html'
+ ),
+ 'Stop context switching',
+);
+$id = expectCookie($res);
+ok(
+ $res = $client->_get(
+ '/',
+ cookie => "lemonldap=$id",
+ accept => 'text/html'
+ ),
+ 'Get Menu',
+);
+count(2);
+expectAuthenticatedAs( $res, 'rtyler' );
+
+# ContextSwitching form
+# ------------------------
+ok(
+ $res = $client->_get(
+ '/switchcontext',
+ cookie => "lemonldap=$id",
+ accept => 'text/html'
+ ),
+ 'ContextSwitching form',
+);
+( $host, $url, $query ) =
+ expectForm( $res, undef, '/switchcontext', 'spoofId' );
+ok( $res->[2]->[0] =~ m%%,
+ 'Found trspan="contextSwitching_ON"' )
+ or explain( $res->[2]->[0], 'trspan="contextSwitching_ON"' );
+
+## POST form
+$query =~ s/spoofId=/spoofId=msmith/;
+ok(
+ $res = $client->_post(
+ '/switchcontext',
+ IO::String->new($query),
+ cookie => "lemonldap=$id",
+ length => length($query),
+ accept => 'text/html',
+ ),
+ 'POST switchcontext'
+);
+ok( $res->[2]->[0] =~ m%%,
+ 'Found trspan="contextSwitching_OFF"' )
+ or explain( $res->[2]->[0], 'trspan="contextSwitching_OFF"' );
+$id = expectCookie($res);
+ok(
+ $res = $client->_get(
+ '/',
+ cookie => "lemonldap=$id",
+ accept => 'text/html'
+ ),
+ 'Get Menu',
+);
+expectAuthenticatedAs( $res, 'msmith' );
+ok( $res->[2]->[0] =~ m%%,
+ 'Found trspan="contextSwitching_OFF"' )
+ or explain( $res->[2]->[0], 'trspan="contextSwitching_OFF"' );
+count(6);
+
+# Stop ContextSwitching
+# ------------------------
+ok(
+ $res = $client->_get(
+ '/switchcontext',
+ cookie => "lemonldap=$id",
+ accept => 'text/html'
+ ),
+ 'Stop context switching',
+);
+$id = expectCookie($res);
+ok(
+ $res = $client->_get(
+ '/',
+ cookie => "lemonldap=$id",
+ accept => 'text/html'
+ ),
+ 'Get Menu',
+);
+count(2);
+expectAuthenticatedAs( $res, 'rtyler' );
+$client->logout($id);
+
+clean_sessions();
+
+done_testing( count() );
diff --git a/lemonldap-ng-portal/t/68-ContextSwitching.t b/lemonldap-ng-portal/t/68-ContextSwitching.t
index 02694b9fc..7704005d1 100644
--- a/lemonldap-ng-portal/t/68-ContextSwitching.t
+++ b/lemonldap-ng-portal/t/68-ContextSwitching.t
@@ -2,19 +2,15 @@ use Test::More;
use strict;
use IO::String;
-BEGIN {
- require 't/test-lib.pm';
-}
+require 't/test-lib.pm';
my $res;
-
my $client = LLNG::Manager::Test->new( {
ini => {
logLevel => 'error',
authentication => 'Demo',
userDB => 'Same',
loginHistoryEnabled => 0,
- brutForceProtection => 0,
portalMainLogo => 'common/logos/logo_llng_old.png',
requireToken => 0,
checkUser => 0,
@@ -376,7 +372,7 @@ ok(
count(6);
ok( $res->[2]->[0] =~ m%%, 'Found PE_SESSIONEXPIRED' )
- or explain( $res->[2]->[0], 'Sessuion expired' );
+ or explain( $res->[2]->[0], 'Session expired' );
ok(
$res = $client->_get(
'/',