diff --git a/lemonldap-ng-portal/t/02-Password-Demo-Hook.t b/lemonldap-ng-portal/t/02-Password-Demo-Hook.t
new file mode 100644
index 000000000..b6a4fd9cd
--- /dev/null
+++ b/lemonldap-ng-portal/t/02-Password-Demo-Hook.t
@@ -0,0 +1,91 @@
+use Test::More;
+use strict;
+use IO::String;
+use JSON;
+use Lemonldap::NG::Portal::Main::Constants
+ qw(PE_BADOLDPASSWORD PE_PASSWORD_MISMATCH PE_PP_MUST_SUPPLY_OLD_PASSWORD);
+
+require 't/test-lib.pm';
+
+my $res;
+
+my $client = LLNG::Manager::Test->new( {
+ ini => {
+ logLevel => 'error',
+ passwordDB => 'Demo',
+ portalRequireOldPassword => 1,
+ customPlugins => 't::PasswordHookPlugin',
+ }
+ }
+);
+
+ok( $res = $client->_get( '/', accept => 'text/html' ), 'Get Menu' );
+count(1);
+
+# Try to authenticate
+# -------------------
+ok(
+ $res = $client->_post(
+ '/',
+ IO::String->new('user=dwho&password=dwho'),
+ length => 23
+ ),
+ 'Auth query'
+);
+count(1);
+expectOK($res);
+my $id = expectCookie($res);
+
+# Test bad new password
+my $s = buildForm( {
+ oldpassword => "dwho",
+ newpassword => "12345",
+ confirmpassword => "12345",
+ }
+);
+ok(
+ $res = $client->_post(
+ '/',
+ IO::String->new($s),
+ cookie => "lemonldap=$id",
+ accept => 'application/json',
+ length => length($s),
+ ),
+ 'Bad new password'
+);
+count(1);
+expectReject( $res, 400, 28 );
+
+# Test good new password
+$s = buildForm( {
+ oldpassword => "dwho",
+ newpassword => "12346",
+ confirmpassword => "12346",
+ }
+);
+ok(
+ $res = $client->_post(
+ '/',
+ IO::String->new($s),
+ cookie => "lemonldap=$id",
+ accept => 'application/json',
+ length => length($s),
+ ),
+ 'Correct new password'
+);
+count(1);
+
+expectReject( $res, 200, 35, "Expect PE_PASSWORD_OK" );
+my $pdata = expectPdata($res);
+is( $pdata->{afterHook}, "dwho-dwho-12346",
+ "passwordAfterChange hook worked as expected" );
+count(1);
+
+# Test $client->logout
+$client->logout($id);
+
+#print STDERR Dumper($res);
+
+clean_sessions();
+
+done_testing( count() );
diff --git a/lemonldap-ng-portal/t/43-MailPasswordReset-Hook.t b/lemonldap-ng-portal/t/43-MailPasswordReset-Hook.t
new file mode 100644
index 000000000..449a2e1be
--- /dev/null
+++ b/lemonldap-ng-portal/t/43-MailPasswordReset-Hook.t
@@ -0,0 +1,134 @@
+use Test::More;
+use strict;
+use IO::String;
+
+BEGIN {
+ eval {
+ require 't/test-lib.pm';
+ require 't/smtp.pm';
+ };
+}
+
+my ( $res, $user, $pwd );
+my $maintests = 15;
+
+SKIP: {
+ eval
+ 'require Email::Sender::Simple;use GD::SecurityImage;use Image::Magick;';
+ if ($@) {
+ skip 'Missing dependencies', $maintests;
+ }
+
+ my $client = LLNG::Manager::Test->new( {
+ ini => {
+ logLevel => 'error',
+ useSafeJail => 1,
+ portalDisplayRegister => 1,
+ authentication => 'Demo',
+ userDB => 'Same',
+ passwordDB => 'Demo',
+ captcha_mail_enabled => 0,
+ portalDisplayResetPassword => 1,
+ customPlugins => 't::PasswordHookPlugin',
+ }
+ }
+ );
+
+ # Test form
+ # ------------------------
+ ok( $res = $client->_get( '/resetpwd', accept => 'text/html' ),
+ 'Reset form', );
+ my ( $host, $url, $query ) = expectForm( $res, '#', undef, 'mail' );
+
+ $query = 'mail=dwho%40badwolf.org';
+
+ # Post email
+ ok(
+ $res = $client->_post(
+ '/resetpwd', IO::String->new($query),
+ length => length($query),
+ accept => 'text/html',
+ cookie => 'llnglanguage=en',
+ ),
+ 'Post mail'
+ );
+
+ like( mail(), qr#Hello#, "Found english greeting" );
+
+ ok( mail() =~ m#a href="http://auth.example.com/resetpwd\?(.*?)"#,
+ 'Found link in mail' );
+ $query = $1;
+ ok(
+ $res = $client->_get(
+ '/resetpwd',
+ query => $query,
+ accept => 'text/html'
+ ),
+ 'Post mail token received by mail'
+ );
+ ( $host, $url, $query ) = expectForm( $res, '#', undef, 'token' );
+ ok( $res->[2]->[0] =~ /newpassword/s, ' Ask for a new password' );
+
+ my $badquery = $query . '&newpassword=12345&confirmpassword=12345';
+
+ # Post failing password
+ ok(
+ $res = $client->_post(
+ '/resetpwd', IO::String->new($badquery),
+ length => length($badquery),
+ accept => 'text/html'
+ ),
+ 'Post new password'
+ );
+ expectPortalError( $res, 28 );
+
+ # Post email again
+ $query = 'mail=dwho%40badwolf.org';
+ ok(
+ $res = $client->_post(
+ '/resetpwd', IO::String->new($query),
+ length => length($query),
+ accept => 'text/html',
+ cookie => 'llnglanguage=en',
+ ),
+ 'Post mail'
+ );
+
+ like( mail(), qr#Hello#, "Found english greeting" );
+
+ ok( mail() =~ m#a href="http://auth.example.com/resetpwd\?(.*?)"#,
+ 'Found link in mail' );
+ $query = $1;
+ ok(
+ $res = $client->_get(
+ '/resetpwd',
+ query => $query,
+ accept => 'text/html'
+ ),
+ 'Post mail token received by mail'
+ );
+ ( $host, $url, $query ) = expectForm( $res, '#', undef, 'token' );
+ ok( $res->[2]->[0] =~ /newpassword/s, ' Ask for a new password' );
+
+ my $goodquery = $query . '&newpassword=12346&confirmpassword=12346';
+
+ # Post accepted password
+ ok(
+ $res = $client->_post(
+ '/resetpwd', IO::String->new($goodquery),
+ length => length($goodquery),
+ accept => 'text/html'
+ ),
+ 'Post new password'
+ );
+ my $pdata = expectPdata($res);
+ is( $pdata->{afterHook}, "dwho--12346",
+ "passwordAfterChange hook worked as expected" );
+
+ ok( mail() =~ /Your password was changed/, 'Password was changed' );
+}
+count($maintests);
+
+clean_sessions();
+
+done_testing( count() );
diff --git a/lemonldap-ng-portal/t/PasswordHookPlugin.pm b/lemonldap-ng-portal/t/PasswordHookPlugin.pm
new file mode 100644
index 000000000..4799b04ba
--- /dev/null
+++ b/lemonldap-ng-portal/t/PasswordHookPlugin.pm
@@ -0,0 +1,34 @@
+package t::PasswordHookPlugin;
+
+use Mouse;
+use Lemonldap::NG::Portal::Main::Constants
+ qw/PE_PP_INSUFFICIENT_PASSWORD_QUALITY PE_OK/;
+extends 'Lemonldap::NG::Portal::Main::Plugin';
+
+use constant hook => {
+ passwordBeforeChange => 'beforeChange',
+ passwordAfterChange => 'afterChange',
+};
+
+sub init {
+ 1;
+}
+
+sub beforeChange {
+ my ( $self, $req, $user, $password, $old ) = @_;
+ if ( $password eq "12345" ) {
+ $self->logger->error("I've got the same combination on my luggage");
+ return PE_PP_INSUFFICIENT_PASSWORD_QUALITY;
+ }
+ return PE_OK;
+}
+
+sub afterChange {
+ my ( $self, $req, $user, $password, $old ) = @_;
+ $old ||= "";
+ $req->pdata->{afterHook} = "$user-$old-$password";
+ $self->logger->debug("Password changed for $user: $old -> $password");
+ return PE_OK;
+}
+
+1;