From 66903d1e769be457ba3b0076ee5a5d5f150422cf Mon Sep 17 00:00:00 2001 From: Maxime Besson Date: Mon, 5 Sep 2022 10:55:01 +0200 Subject: [PATCH 1/7] improve SSL unit tests --- lemonldap-ng-portal/t/29-AuthSSL.t | 148 +++++++++++++++++------------ 1 file changed, 85 insertions(+), 63 deletions(-) diff --git a/lemonldap-ng-portal/t/29-AuthSSL.t b/lemonldap-ng-portal/t/29-AuthSSL.t index 5358c7b049..1b89443d06 100644 --- a/lemonldap-ng-portal/t/29-AuthSSL.t +++ b/lemonldap-ng-portal/t/29-AuthSSL.t @@ -1,80 +1,102 @@ use Test::More; +use IO::String; use strict; require 't/test-lib.pm'; my $res; -my $client = LLNG::Manager::Test->new( { - ini => { - logLevel => 'error', - useSafeJail => 1, - authentication => 'SSL', - userDB => 'Null', - SSLVar => 'SSL_CLIENT_S_DN_Custom', - sslByAjax => 1, - sslHost => 'https://authssl.example.com:19876' +subtest 'Legacy AJAX SSL Auth' => sub { + my $client = LLNG::Manager::Test->new( { + ini => { + logLevel => 'error', + useSafeJail => 1, + authentication => 'SSL', + userDB => 'Null', + SSLVar => 'SSL_CLIENT_S_DN_Custom', + sslByAjax => 1, + sslHost => 'https://authssl.example.com:19876' + } } - } -); + ); -ok( - $res = $client->_get( - '/', - query => 'url=aHR0cDovL3Rlc3QxLmV4YW1wbGUuY29tLw==', - accept => 'text/html' - ), - 'Get Menu' -); -my $pdata = 'lemonldappdata=' . expectCookie( $res, 'lemonldappdata' ); + ok( + $res = $client->_get( + '/', + query => 'url=aHR0cDovL3Rlc3QxLmV4YW1wbGUuY29tLw==', + accept => 'text/html' + ), + 'Get Menu' + ); + my $pdata = 'lemonldappdata=' . expectCookie( $res, 'lemonldappdata' ); -ok( - $res->[2]->[0] =~ + ok( + $res->[2]->[0] =~ m%%s, - ' SSL AJAX URL found' -) or print STDERR Dumper( $res->[2]->[0] ); -ok( $res->[2]->[0] =~ qr%[2]->[0] ); -ok( $res->[2]->[0] =~ /ssl\.(?:min\.)?js/, 'Get sslChoice javascript' ) - or print STDERR Dumper( $res->[2]->[0] ); -count(4); + ' SSL AJAX URL found' + ) or print STDERR Dumper( $res->[2]->[0] ); + ok( $res->[2]->[0] =~ qr%[2]->[0] ); + ok( $res->[2]->[0] =~ /ssl\.(?:min\.)?js/, 'Get ssl javascript' ) + or print STDERR Dumper( $res->[2]->[0] ); + count(4); -ok( - $res = $client->_get( - '/', - cookie => $pdata, - accept => 'text/html', - custom => { SSL_CLIENT_S_DN_Custom => 'dwho' } - ), - 'Auth query' -); -expectCookie($res); -expectRedirection( $res, 'http://test1.example.com/' ); -$pdata = expectCookie( $res, 'lemonldappdata' ); -ok( $pdata eq '', 'pdata is empty' ); -count(2); + my ( $host, $url, $query ) = expectForm( $res, '#', undef, 'nossl' ); -&Lemonldap::NG::Handler::Main::cfgNum( 0, 0 ); -$client = LLNG::Manager::Test->new( { - ini => { - logLevel => 'error', - useSafeJail => 1, - authentication => 'SSL', - userDB => 'Null', - } - } -); + # AJAX request + ok( + $res = $client->_get( + '/', + cookie => $pdata, + accept => 'application/json', + custom => { SSL_CLIENT_S_DN_Custom => 'dwho' } + ), + 'Auth query' + ); + my $json = expectJSON($res); + is( $json->{result}, 1, "Correct result" ); + is( $json->{error}, 0, "No error" ); + + my $id = expectCookie($res); + $pdata = expectCookie( $res, 'lemonldappdata' ); + ok( $pdata eq '', 'pdata is empty' ); + count(2); -ok( - $res = $client->_get( '/', custom => { SSL_CLIENT_S_DN_Email => 'dwho' } ), - 'Auth query' -); + ok( + $res = $client->_post( + '/', IO::String->new($query), + length => length($query), + accept => 'text/html', + cookie => "lemonldap=$id", + ), + 'Post form' + ); + expectRedirection( $res, 'http://test1.example.com/' ); +}; + +subtest 'Regular SSL Auth' => sub { + &Lemonldap::NG::Handler::Main::cfgNum( 0, 0 ); + my $client = LLNG::Manager::Test->new( { + ini => { + logLevel => 'error', + useSafeJail => 1, + authentication => 'SSL', + userDB => 'Null', + } + } + ); -expectOK($res); -expectCookie($res); -count(1); + my $res; + ok( + $res = + $client->_get( '/', custom => { SSL_CLIENT_S_DN_Email => 'dwho' } ), + 'Auth query' + ); -clean_sessions(); + expectOK($res); + expectCookie($res); + count(1); +}; -done_testing( count() ); +done_testing(); -- GitLab From e896ad3749f2fefa054543ad29fe3358ddae6205 Mon Sep 17 00:00:00 2001 From: Maxime Besson Date: Tue, 6 Sep 2022 09:59:51 +0200 Subject: [PATCH 2/7] Split the Ajax SSL Auth process into a route (#2792) --- .../lib/Lemonldap/NG/Portal/Auth/SSL.pm | 119 ++++++++++++++++-- 1 file changed, 107 insertions(+), 12 deletions(-) diff --git a/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Auth/SSL.pm b/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Auth/SSL.pm index 3378431b8c..7e434fe507 100644 --- a/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Auth/SSL.pm +++ b/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Auth/SSL.pm @@ -4,6 +4,7 @@ use strict; use Mouse; use Lemonldap::NG::Portal::Main::Constants qw( PE_CERTIFICATEREQUIRED + PE_ERROR PE_BADCERTIFICATE PE_FIRSTACCESS PE_OK @@ -17,6 +18,22 @@ extends 'Lemonldap::NG::Portal::Main::Auth'; has AjaxInitScript => ( is => 'rw', default => '' ); has Name => ( is => 'ro', default => 'SSL' ); +has InitCmd => ( + is => 'ro', + default => q@$self->p->setHiddenFormValue( $req, usertoken => 0, '', 0 )@ +); + +# Auth Token +has authott => ( + is => 'rw', + lazy => 1, + default => sub { + my $ott = + $_[0]->{p}->loadModule('Lemonldap::NG::Portal::Lib::OneTimeToken'); + $ott->cache(0); + return $ott; + } +); sub init { my ($self) = @_; @@ -24,19 +41,49 @@ sub init { . $self->conf->{sslHost} . '"}' ) if $self->conf->{sslByAjax}; + + $self->addUnauthRoute( 'authssl' => 'auth_ssl', ['GET'] ); + + # Used for session upgrade/reauthn + $self->addAuthRoute( 'authssl' => 'auth_ssl', ['GET'] ); + return 1; } -# Read username in SSL environment variables, or return an error -# @return Lemonldap::NG::Portal constant -sub extractFormInfo { +# Create authentication token so you can use 2FA, notifications, etc, with Ajax +sub auth_ssl { my ( $self, $req ) = @_; - # If this is the ajax query, allow response to contain HTML code - # to update the portal error message - if ( $req->wantJSON ) { + my $ssl_user = $self->get_user_from_req($req); + if ($ssl_user) { + my $token = $self->authott->createToken( { + ssl_user => $ssl_user, + type => 'auth_ssl_token', + } + ); + if ($token) { + return $self->sendJSONresponse( + $req, + { + usertoken => $token, + error => PE_OK, + } + ); + } + else { + $self->logger->error("Could not create user token for $ssl_user"); + $req->wantErrorRender(1); + return $self->p->do( $req, [ sub { PE_ERROR } ] ); + } + } + else { $req->wantErrorRender(1); + return $self->p->do( $req, [ sub { PE_CERTIFICATEREQUIRED } ] ); } +} + +sub get_user_from_req { + my ( $self, $req ) = @_; my $field = $self->conf->{SSLVar}; if ( $req->env->{SSL_CLIENT_I_DN} ) { @@ -49,16 +96,41 @@ sub extractFormInfo { $field = $tmp; } } - $req->env->{$field} - ? $self->logger->debug("Using SSL environment variable $field") - : $self->logger->notice( - "No name found in certificate, check your configuration"); - if ( $req->env->{$field} and $req->user( $req->env->{$field} ) ) { + my $value = $req->env->{$field}; + if ($value) { + $self->logger->debug("Using SSL environment variable $field"); + } + else { + $self->logger->notice( + "No name found in certificate, check your configuration"); + } + + return $value; +} + +# Read username in SSL environment variables, or return an error +# @return Lemonldap::NG::Portal constant +sub extractFormInfo { + my ( $self, $req ) = @_; + + # If this is the ajax query, allow response to contain HTML code + # to update the portal error message + if ( $req->wantJSON ) { + $req->wantErrorRender(1); + } + + my $token_id = $req->param('usertoken'); + if ($token_id) { + return $self->_set_user_from_token( $req, $token_id ); + } + + my $ssl_user = $self->get_user_from_req($req); + + if ( $ssl_user and $req->user($ssl_user) ) { $self->userLogger->notice( "GoodSSL authentication for " . $req->user ); return PE_OK; } elsif ( $req->env->{SSL_CLIENT_S_DN} ) { - $self->userLogger->warn("$field was not found in user certificate"); return PE_BADCERTIFICATE; } elsif ( $self->conf->{sslByAjax} and not $req->param('nossl') ) { @@ -73,6 +145,9 @@ sub extractFormInfo { $self->logger->debug( "Send init/script -> " . $req->data->{customScript} ); $req->data->{waitingMessage} = 1; + + eval( $self->InitCmd ); + die 'Unable to launch init commmand ' . $self->{InitCmd} if ($@); return PE_FIRSTACCESS; } else { @@ -88,6 +163,26 @@ sub extractFormInfo { } } +sub _set_user_from_token { + my ( $self, $req, $token_id ) = @_; + my $token = $self->authott->getToken($token_id); + if ($token) { + if ( $token->{type} eq 'auth_ssl_token' ) { + $req->user( $token->{ssl_user} ); + $self->userLogger->notice( + "GoodSSL authentication for " . $req->user ); + return PE_OK; + } + else { + $self->logger->error( "Unexpected token type: " . $token->{type} ); + return PE_ERROR; + } + } + + $self->logger->error("Could not fetch user token $token_id"); + return PE_ERROR; +} + sub authenticate { return PE_OK; } -- GitLab From 2012895c0c34fc2e4f92011cb6c04aa65e760fae Mon Sep 17 00:00:00 2001 From: Maxime Besson Date: Mon, 12 Sep 2022 10:26:58 +0200 Subject: [PATCH 3/7] Move the SSL AJAX logic into a lib (#2792) --- lemonldap-ng-portal/MANIFEST | 1 + .../lib/Lemonldap/NG/Portal/Auth/SSL.pm | 84 ++++-------- .../lib/Lemonldap/NG/Portal/Auth/_Ajax.pm | 121 ++++++++++++++++++ 3 files changed, 145 insertions(+), 61 deletions(-) create mode 100644 lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Auth/_Ajax.pm diff --git a/lemonldap-ng-portal/MANIFEST b/lemonldap-ng-portal/MANIFEST index 826ede3b9b..1d75ef1c97 100644 --- a/lemonldap-ng-portal/MANIFEST +++ b/lemonldap-ng-portal/MANIFEST @@ -23,6 +23,7 @@ lib/Lemonldap/NG/Portal/2F/UTOTP.pm lib/Lemonldap/NG/Portal/2F/WebAuthn.pm lib/Lemonldap/NG/Portal/2F/Yubikey.pm lib/Lemonldap/NG/Portal/Auth.pod +lib/Lemonldap/NG/Portal/Auth/_Ajax.pm lib/Lemonldap/NG/Portal/Auth/_WebForm.pm lib/Lemonldap/NG/Portal/Auth/AD.pm lib/Lemonldap/NG/Portal/Auth/Apache.pm diff --git a/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Auth/SSL.pm b/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Auth/SSL.pm index 7e434fe507..5863ae9e19 100644 --- a/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Auth/SSL.pm +++ b/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Auth/SSL.pm @@ -18,22 +18,10 @@ extends 'Lemonldap::NG::Portal::Main::Auth'; has AjaxInitScript => ( is => 'rw', default => '' ); has Name => ( is => 'ro', default => 'SSL' ); -has InitCmd => ( - is => 'ro', - default => q@$self->p->setHiddenFormValue( $req, usertoken => 0, '', 0 )@ -); -# Auth Token -has authott => ( - is => 'rw', - lazy => 1, - default => sub { - my $ott = - $_[0]->{p}->loadModule('Lemonldap::NG::Portal::Lib::OneTimeToken'); - $ott->cache(0); - return $ott; - } -); +has auth_id => ( is => 'ro', default => 'ssl' ); + +with 'Lemonldap::NG::Portal::Auth::_Ajax'; sub init { my ($self) = @_; @@ -41,40 +29,22 @@ sub init { . $self->conf->{sslHost} . '"}' ) if $self->conf->{sslByAjax}; - - $self->addUnauthRoute( 'authssl' => 'auth_ssl', ['GET'] ); - - # Used for session upgrade/reauthn - $self->addAuthRoute( 'authssl' => 'auth_ssl', ['GET'] ); - return 1; } # Create authentication token so you can use 2FA, notifications, etc, with Ajax -sub auth_ssl { +sub auth_route { my ( $self, $req ) = @_; my $ssl_user = $self->get_user_from_req($req); if ($ssl_user) { - my $token = $self->authott->createToken( { - ssl_user => $ssl_user, - type => 'auth_ssl_token', + return $self->ajax_success( + $req, + $ssl_user, + { + _Issuer => $req->env->{SSL_CLIENT_I_DN}, } ); - if ($token) { - return $self->sendJSONresponse( - $req, - { - usertoken => $token, - error => PE_OK, - } - ); - } - else { - $self->logger->error("Could not create user token for $ssl_user"); - $req->wantErrorRender(1); - return $self->p->do( $req, [ sub { PE_ERROR } ] ); - } } else { $req->wantErrorRender(1); @@ -119,15 +89,26 @@ sub extractFormInfo { $req->wantErrorRender(1); } - my $token_id = $req->param('usertoken'); + my $token_id = $req->param('ajax_auth_token'); if ($token_id) { - return $self->_set_user_from_token( $req, $token_id ); + my $token = $self->get_auth_token( $req, $token_id ); + if ( $token->{user} ) { + my $user = $token->{user}; + $self->userLogger->notice( "GoodSSL authentication for " . $user ); + $req->user($user); + $req->data->{_Issuer} = $token->{extraInfo}->{_Issuer}; + return PE_OK; + } + else { + return PE_ERROR; + } } my $ssl_user = $self->get_user_from_req($req); if ( $ssl_user and $req->user($ssl_user) ) { $self->userLogger->notice( "GoodSSL authentication for " . $req->user ); + $req->data->{_Issuer} = $req->env->{SSL_CLIENT_I_DN}; return PE_OK; } elsif ( $req->env->{SSL_CLIENT_S_DN} ) { @@ -163,26 +144,6 @@ sub extractFormInfo { } } -sub _set_user_from_token { - my ( $self, $req, $token_id ) = @_; - my $token = $self->authott->getToken($token_id); - if ($token) { - if ( $token->{type} eq 'auth_ssl_token' ) { - $req->user( $token->{ssl_user} ); - $self->userLogger->notice( - "GoodSSL authentication for " . $req->user ); - return PE_OK; - } - else { - $self->logger->error( "Unexpected token type: " . $token->{type} ); - return PE_ERROR; - } - } - - $self->logger->error("Could not fetch user token $token_id"); - return PE_ERROR; -} - sub authenticate { return PE_OK; } @@ -190,6 +151,7 @@ sub authenticate { sub setAuthSessionInfo { my ( $self, $req ) = @_; $req->sessionInfo->{authenticationLevel} = $self->conf->{SSLAuthnLevel}; + $req->sessionInfo->{_Issuer} = $req->data->{_Issuer}; return PE_OK; } diff --git a/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Auth/_Ajax.pm b/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Auth/_Ajax.pm new file mode 100644 index 0000000000..eedc77d8c6 --- /dev/null +++ b/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Auth/_Ajax.pm @@ -0,0 +1,121 @@ +##@file +# Ajax-based authentication methods (SSL, Kerberos, WebAuthn...) +# This class lets you easily implement a Javascript-based authentication +# method. + +package Lemonldap::NG::Portal::Auth::_Ajax; + +use strict; +use Mouse::Role; +use Lemonldap::NG::Portal::Main::Constants qw( + PE_OK + PE_ERROR +); + +our $VERSION = '2.0.16'; + +# addUnauthRoute/addAuthRoute are provided by deriving your plugin from +# 'Lemonldap::NG::Portal::Main::Auth' or 'Lemonldap::NG::Portal::Main::Plugin' + +# You must provide an auth_id attribute that returns a simple string +# identifying your plugin, eg: 'ssl', 'krb', ... +# You must also implement an auth_route method that will be served on +# /auth[auth_id] +# The auth_route method validates your JS-supplied challenge, and must call +# ajax_success to return the authentication token +requires qw(auth_id addUnauthRoute addAuthRoute auth_route); + +has InitCmd => ( + is => 'ro', + default => + q@$self->p->setHiddenFormValue( $req, ajax_auth_token => 0, '', 0 )@ +); + +# Auth Token +has authott => ( + is => 'rw', + lazy => 1, + default => sub { + my $ott = + $_[0]->{p}->loadModule('Lemonldap::NG::Portal::Lib::OneTimeToken'); + $ott->cache(0); + return $ott; + } +); + +around 'init' => sub { + my $orig = shift; + my $self = shift; + + $self->addUnauthRoute( + ( 'auth' . $self->auth_id ) => '_auth_route', + ['GET'] + ); + + # Used for session upgrade/reauthn + $self->addAuthRoute( ( 'auth' . $self->auth_id ) => 'auth_route', ['GET'] ); + + return $self->$orig(); +}; + +sub _auth_route { + my ( $self, $req, @path ) = @_; + + # Run beforeAuth steps + $req->steps( [ @{ $self->p->beforeAuth } ] ); + my $res = $self->p->process($req); + if ( $res && $res > 0 ) { + $req->wantErrorRender(1); + return $self->p->do( $req, [ sub { $res } ] ); + } + + return $self->auth_route( $req, @path ); +} + +# You should call this method in your 'extractFormInfo' to validate the +# Authentication token +sub get_auth_token { + my ( $self, $req, $token_id ) = @_; + my $token = $self->authott->getToken($token_id); + if ($token) { + if ( $token->{type} eq ( 'auth_token_' . $self->auth_id ) ) { + return $token; + } + else { + $self->logger->error( "Unexpected token type: " . $token->{type} ); + return; + } + } + + $self->logger->error("Could not fetch user token $token_id"); + return; +} + +# You should call this method in your auth_route method to create the Authentication token, +# $user is the main identified, that UserDB will lookup. Extra information may +# be stored in the session by setAuthSessionInfo +sub ajax_success { + my ( $self, $req, $user, $extraInfo ) = @_; + my $token = $self->authott->createToken( { + user => $user, + type => 'auth_token_' . $self->auth_id, + extraInfo => $extraInfo, + } + ); + if ($token) { + return $self->sendJSONresponse( + $req, + { + ajax_auth_token => $token, + error => PE_OK, + } + ); + } + else { + $self->logger->error("Could not create user token for $user"); + $req->wantErrorRender(1); + return $self->p->do( $req, [ sub { PE_ERROR } ] ); + } +} + +1; -- GitLab From e4a8bc365d2e64dd74429712078deec6be550940 Mon Sep 17 00:00:00 2001 From: Maxime Besson Date: Tue, 6 Sep 2022 10:00:11 +0200 Subject: [PATCH 4/7] Update forms/js for SSL ajax auth (#2792) --- lemonldap-ng-portal/site/coffee/ssl.coffee | 5 ++++- lemonldap-ng-portal/site/coffee/sslChoice.coffee | 5 ++++- lemonldap-ng-portal/site/htdocs/static/common/js/ssl.js | 3 +++ lemonldap-ng-portal/site/htdocs/static/common/js/ssl.min.js | 2 +- .../site/htdocs/static/common/js/ssl.min.js.map | 2 +- .../site/htdocs/static/common/js/sslChoice.js | 3 +++ .../site/htdocs/static/common/js/sslChoice.min.js | 2 +- .../site/htdocs/static/common/js/sslChoice.min.js.map | 2 +- 8 files changed, 18 insertions(+), 6 deletions(-) diff --git a/lemonldap-ng-portal/site/coffee/ssl.coffee b/lemonldap-ng-portal/site/coffee/ssl.coffee index d365bae135..458a31c723 100644 --- a/lemonldap-ng-portal/site/coffee/ssl.coffee +++ b/lemonldap-ng-portal/site/coffee/ssl.coffee @@ -8,9 +8,12 @@ tryssl = () -> dataType: 'json', xhrFields: withCredentials: true - # If request succeed, cookie is set, posting form to get redirection + # If request succeed, posting form to get redirection # or menu success: (data) -> + # If we contain a ajax_auth_token, add it to form + if data.ajax_auth_token + $('#lform').find('input[name="ajax_auth_token"]').attr("value", data.ajax_auth_token) sendUrl path console.log 'Success -> ', data # Case else, will display PE_BADCREDENTIALS or fallback to next auth diff --git a/lemonldap-ng-portal/site/coffee/sslChoice.coffee b/lemonldap-ng-portal/site/coffee/sslChoice.coffee index 1b1da67652..73b3e4e1eb 100644 --- a/lemonldap-ng-portal/site/coffee/sslChoice.coffee +++ b/lemonldap-ng-portal/site/coffee/sslChoice.coffee @@ -8,9 +8,12 @@ tryssl = () -> dataType: 'json', xhrFields: withCredentials: true - # If request succeed, cookie is set, posting form to get redirection + # If request succeed, posting form to get redirection # or menu success: (data) -> + # If we contain a ajax_auth_token, add it to form + if data.ajax_auth_token + $('#lformSSL').find('input[name="ajax_auth_token"]').attr("value", data.ajax_auth_token) sendUrl path console.log 'Success -> ', data # Case else, will display PE_BADCREDENTIALS or fallback to next auth diff --git a/lemonldap-ng-portal/site/htdocs/static/common/js/ssl.js b/lemonldap-ng-portal/site/htdocs/static/common/js/ssl.js index 2a9f924d32..15621dc9c0 100644 --- a/lemonldap-ng-portal/site/htdocs/static/common/js/ssl.js +++ b/lemonldap-ng-portal/site/htdocs/static/common/js/ssl.js @@ -13,6 +13,9 @@ withCredentials: true }, success: function(data) { + if (data.ajax_auth_token) { + $('#lform').find('input[name="ajax_auth_token"]').attr("value", data.ajax_auth_token); + } sendUrl(path); return console.log('Success -> ', data); }, diff --git a/lemonldap-ng-portal/site/htdocs/static/common/js/ssl.min.js b/lemonldap-ng-portal/site/htdocs/static/common/js/ssl.min.js index 45298b2c65..1f28551e09 100644 --- a/lemonldap-ng-portal/site/htdocs/static/common/js/ssl.min.js +++ b/lemonldap-ng-portal/site/htdocs/static/common/js/ssl.min.js @@ -1 +1 @@ -(function(){var r,o;o=function(){var n;return n=window.location.pathname,console.log("path -> ",n),console.log("Call URL -> ",window.datas.sslHost),$.ajax(window.datas.sslHost,{dataType:"json",xhrFields:{withCredentials:!0},success:function(o){return r(n),console.log("Success -> ",o)},error:function(o){return 0===o.status&&r(n),o.responseJSON&&"error"in o.responseJSON&&"9"===o.responseJSON.error&&r(n),o.responseJSON&&"html"in o.responseJSON&&($("#errormsg").html(o.responseJSON.html),$(window).trigger("load")),console.log("Error during AJAX SSL authentication",o)}}),!1},r=function(o){var n;return(n=$("#lform").attr("action")).match(/^#$/)?n=o:n+=o,console.log("form action URL -> ",n),$("#lform").attr("action",n),$("#lform").submit()},$(document).ready(function(){return $(".sslclick").on("click",o)})}).call(this); \ No newline at end of file +!function(){var o=function(){var n=window.location.pathname;return console.log("path -> ",n),console.log("Call URL -> ",window.datas.sslHost),$.ajax(window.datas.sslHost,{dataType:"json",xhrFields:{withCredentials:!0},success:function(o){return o.ajax_auth_token&&$("#lform").find('input[name="ajax_auth_token"]').attr("value",o.ajax_auth_token),t(n),console.log("Success -> ",o)},error:function(o){return 0===o.status&&t(n),o.responseJSON&&"error"in o.responseJSON&&"9"===o.responseJSON.error&&t(n),o.responseJSON&&"html"in o.responseJSON&&($("#errormsg").html(o.responseJSON.html),$(window).trigger("load")),console.log("Error during AJAX SSL authentication",o)}}),!1},t=function(o){var n=$("#lform").attr("action");return n.match(/^#$/)?n=o:n+=o,console.log("form action URL -> ",n),$("#lform").attr("action",n),$("#lform").submit()};$(document).ready(function(){return $(".sslclick").on("click",o)})}.call(this); \ No newline at end of file diff --git a/lemonldap-ng-portal/site/htdocs/static/common/js/ssl.min.js.map b/lemonldap-ng-portal/site/htdocs/static/common/js/ssl.min.js.map index ecd33ec2a1..9b396094f0 100644 --- a/lemonldap-ng-portal/site/htdocs/static/common/js/ssl.min.js.map +++ b/lemonldap-ng-portal/site/htdocs/static/common/js/ssl.min.js.map @@ -1 +1 @@ -{"version":3,"sources":["ssl.js"],"names":["sendUrl","tryssl","path","window","location","pathname","console","log","datas","sslHost","$","ajax","dataType","xhrFields","withCredentials","success","data","error","result","status","responseJSON","html","trigger","form_url","attr","match","submit","document","ready","on","call","this"],"mappings":"CACA,WACE,IAAIA,EAASC,EAEbA,EAAS,WACP,IAAIC,EA2BJ,OA1BAA,EAAOC,OAAOC,SAASC,SACvBC,QAAQC,IAAI,WAAYL,GACxBI,QAAQC,IAAI,eAAgBJ,OAAOK,MAAMC,SACzCC,EAAEC,KAAKR,OAAOK,MAAMC,QAAS,CAC3BG,SAAU,OACVC,UAAW,CACTC,iBAAiB,GAEnBC,QAAS,SAASC,GAEhB,OADAhB,EAAQE,GACDI,QAAQC,IAAI,cAAeS,IAEpCC,MAAO,SAASC,GAWd,OAVsB,IAAlBA,EAAOC,QACTnB,EAAQE,GAENgB,EAAOE,cAAgB,UAAWF,EAAOE,cAA8C,MAA9BF,EAAOE,aAAaH,OAC/EjB,EAAQE,GAENgB,EAAOE,cAAgB,SAAUF,EAAOE,eAC1CV,EAAE,aAAaW,KAAKH,EAAOE,aAAaC,MACxCX,EAAEP,QAAQmB,QAAQ,SAEbhB,QAAQC,IAAI,uCAAwCW,OAGxD,GAGTlB,EAAU,SAASE,GACjB,IAAIqB,EASJ,OARAA,EAAWb,EAAE,UAAUc,KAAK,WACfC,MAAM,OACjBF,EAAWrB,EAEXqB,GAAsBrB,EAExBI,QAAQC,IAAI,sBAAuBgB,GACnCb,EAAE,UAAUc,KAAK,SAAUD,GACpBb,EAAE,UAAUgB,UAGrBhB,EAAEiB,UAAUC,MAAM,WAChB,OAAOlB,EAAE,aAAamB,GAAG,QAAS5B,OAGnC6B,KAAKC"} \ No newline at end of file +{"version":3,"sources":["ssl.js"],"names":["tryssl","path","window","location","pathname","console","log","datas","sslHost","$","ajax","dataType","xhrFields","withCredentials","success","data","ajax_auth_token","find","attr","sendUrl","error","result","status","responseJSON","html","trigger","form_url","match","submit","document","ready","on","call","this"],"mappings":"CACA,WACE,IAEAA,EAAS,WACP,IACAC,EAAOC,OAAOC,SAASC,SA6BvB,OA5BAC,QAAQC,IAAI,WAAYL,GACxBI,QAAQC,IAAI,eAAgBJ,OAAOK,MAAMC,SACzCC,EAAEC,KAAKR,OAAOK,MAAMC,QAAS,CAC3BG,SAAU,OACVC,UAAW,CACTC,iBAAiB,GAEnBC,QAAS,SAASC,GAKhB,OAJIA,EAAKC,iBACPP,EAAE,UAAUQ,KAAK,iCAAiCC,KAAK,QAASH,EAAKC,iBAEvEG,EAAQlB,GACDI,QAAQC,IAAI,cAAeS,IAEpCK,MAAO,SAASC,GAWd,OAVsB,IAAlBA,EAAOC,QACTH,EAAQlB,GAENoB,EAAOE,cAAgB,UAAWF,EAAOE,cAA8C,MAA9BF,EAAOE,aAAaH,OAC/ED,EAAQlB,GAENoB,EAAOE,cAAgB,SAAUF,EAAOE,eAC1Cd,EAAE,aAAae,KAAKH,EAAOE,aAAaC,MACxCf,EAAEP,QAAQuB,QAAQ,SAEbpB,QAAQC,IAAI,uCAAwCe,OAGxD,GAGTF,EAAU,SAASlB,GACjB,IACAyB,EAAWjB,EAAE,UAAUS,KAAK,UAQ5B,OAPIQ,EAASC,MAAM,OACjBD,EAAWzB,EAEXyB,GAAsBzB,EAExBI,QAAQC,IAAI,sBAAuBoB,GACnCjB,EAAE,UAAUS,KAAK,SAAUQ,GACpBjB,EAAE,UAAUmB,UAGrBnB,EAAEoB,UAAUC,MAAM,WAChB,OAAOrB,EAAE,aAAasB,GAAG,QAAS/B,MAGnCgC,KAAKC"} \ No newline at end of file diff --git a/lemonldap-ng-portal/site/htdocs/static/common/js/sslChoice.js b/lemonldap-ng-portal/site/htdocs/static/common/js/sslChoice.js index 6495d78833..5016f06963 100644 --- a/lemonldap-ng-portal/site/htdocs/static/common/js/sslChoice.js +++ b/lemonldap-ng-portal/site/htdocs/static/common/js/sslChoice.js @@ -13,6 +13,9 @@ withCredentials: true }, success: function(data) { + if (data.ajax_auth_token) { + $('#lformSSL').find('input[name="ajax_auth_token"]').attr("value", data.ajax_auth_token); + } sendUrl(path); return console.log('Success -> ', data); }, diff --git a/lemonldap-ng-portal/site/htdocs/static/common/js/sslChoice.min.js b/lemonldap-ng-portal/site/htdocs/static/common/js/sslChoice.min.js index b8d8e791ac..0cab88a082 100644 --- a/lemonldap-ng-portal/site/htdocs/static/common/js/sslChoice.min.js +++ b/lemonldap-ng-portal/site/htdocs/static/common/js/sslChoice.min.js @@ -1 +1 @@ -(function(){var r,o;o=function(){var n;return n=window.location.pathname,console.log("path -> ",n),console.log("Call URL -> ",window.datas.sslHost),$.ajax(window.datas.sslHost,{dataType:"json",xhrFields:{withCredentials:!0},success:function(o){return r(n),console.log("Success -> ",o)},error:function(o){return 0===o.status&&r(n),o.responseJSON&&"error"in o.responseJSON&&"9"===o.responseJSON.error&&r(n),o.responseJSON&&"html"in o.responseJSON&&($("#errormsg").html(o.responseJSON.html),$(window).trigger("load")),console.log("Error during AJAX SSL authentication",o)}}),!1},r=function(o){var n;return(n=$("#lformSSL").attr("action")).match(/^#$/)?n=o:n+=o,console.log("form action URL -> ",n),$("#lformSSL").attr("action",n),$("#lformSSL").submit()},$(document).ready(function(){return $(".sslclick").on("click",o)})}).call(this); \ No newline at end of file +!function(){var o=function(){var n=window.location.pathname;return console.log("path -> ",n),console.log("Call URL -> ",window.datas.sslHost),$.ajax(window.datas.sslHost,{dataType:"json",xhrFields:{withCredentials:!0},success:function(o){return o.ajax_auth_token&&$("#lformSSL").find('input[name="ajax_auth_token"]').attr("value",o.ajax_auth_token),t(n),console.log("Success -> ",o)},error:function(o){return 0===o.status&&t(n),o.responseJSON&&"error"in o.responseJSON&&"9"===o.responseJSON.error&&t(n),o.responseJSON&&"html"in o.responseJSON&&($("#errormsg").html(o.responseJSON.html),$(window).trigger("load")),console.log("Error during AJAX SSL authentication",o)}}),!1},t=function(o){var n=$("#lformSSL").attr("action");return n.match(/^#$/)?n=o:n+=o,console.log("form action URL -> ",n),$("#lformSSL").attr("action",n),$("#lformSSL").submit()};$(document).ready(function(){return $(".sslclick").on("click",o)})}.call(this); \ No newline at end of file diff --git a/lemonldap-ng-portal/site/htdocs/static/common/js/sslChoice.min.js.map b/lemonldap-ng-portal/site/htdocs/static/common/js/sslChoice.min.js.map index b49038c35d..f6ec65c333 100644 --- a/lemonldap-ng-portal/site/htdocs/static/common/js/sslChoice.min.js.map +++ b/lemonldap-ng-portal/site/htdocs/static/common/js/sslChoice.min.js.map @@ -1 +1 @@ -{"version":3,"sources":["sslChoice.js"],"names":["sendUrl","tryssl","path","window","location","pathname","console","log","datas","sslHost","$","ajax","dataType","xhrFields","withCredentials","success","data","error","result","status","responseJSON","html","trigger","form_url","attr","match","submit","document","ready","on","call","this"],"mappings":"CACA,WACE,IAAIA,EAASC,EAEbA,EAAS,WACP,IAAIC,EA2BJ,OA1BAA,EAAOC,OAAOC,SAASC,SACvBC,QAAQC,IAAI,WAAYL,GACxBI,QAAQC,IAAI,eAAgBJ,OAAOK,MAAMC,SACzCC,EAAEC,KAAKR,OAAOK,MAAMC,QAAS,CAC3BG,SAAU,OACVC,UAAW,CACTC,iBAAiB,GAEnBC,QAAS,SAASC,GAEhB,OADAhB,EAAQE,GACDI,QAAQC,IAAI,cAAeS,IAEpCC,MAAO,SAASC,GAWd,OAVsB,IAAlBA,EAAOC,QACTnB,EAAQE,GAENgB,EAAOE,cAAgB,UAAWF,EAAOE,cAA8C,MAA9BF,EAAOE,aAAaH,OAC/EjB,EAAQE,GAENgB,EAAOE,cAAgB,SAAUF,EAAOE,eAC1CV,EAAE,aAAaW,KAAKH,EAAOE,aAAaC,MACxCX,EAAEP,QAAQmB,QAAQ,SAEbhB,QAAQC,IAAI,uCAAwCW,OAGxD,GAGTlB,EAAU,SAASE,GACjB,IAAIqB,EASJ,OARAA,EAAWb,EAAE,aAAac,KAAK,WAClBC,MAAM,OACjBF,EAAWrB,EAEXqB,GAAsBrB,EAExBI,QAAQC,IAAI,sBAAuBgB,GACnCb,EAAE,aAAac,KAAK,SAAUD,GACvBb,EAAE,aAAagB,UAGxBhB,EAAEiB,UAAUC,MAAM,WAChB,OAAOlB,EAAE,aAAamB,GAAG,QAAS5B,OAGnC6B,KAAKC"} \ No newline at end of file +{"version":3,"sources":["sslChoice.js"],"names":["tryssl","path","window","location","pathname","console","log","datas","sslHost","$","ajax","dataType","xhrFields","withCredentials","success","data","ajax_auth_token","find","attr","sendUrl","error","result","status","responseJSON","html","trigger","form_url","match","submit","document","ready","on","call","this"],"mappings":"CACA,WACE,IAEAA,EAAS,WACP,IACAC,EAAOC,OAAOC,SAASC,SA6BvB,OA5BAC,QAAQC,IAAI,WAAYL,GACxBI,QAAQC,IAAI,eAAgBJ,OAAOK,MAAMC,SACzCC,EAAEC,KAAKR,OAAOK,MAAMC,QAAS,CAC3BG,SAAU,OACVC,UAAW,CACTC,iBAAiB,GAEnBC,QAAS,SAASC,GAKhB,OAJIA,EAAKC,iBACPP,EAAE,aAAaQ,KAAK,iCAAiCC,KAAK,QAASH,EAAKC,iBAE1EG,EAAQlB,GACDI,QAAQC,IAAI,cAAeS,IAEpCK,MAAO,SAASC,GAWd,OAVsB,IAAlBA,EAAOC,QACTH,EAAQlB,GAENoB,EAAOE,cAAgB,UAAWF,EAAOE,cAA8C,MAA9BF,EAAOE,aAAaH,OAC/ED,EAAQlB,GAENoB,EAAOE,cAAgB,SAAUF,EAAOE,eAC1Cd,EAAE,aAAae,KAAKH,EAAOE,aAAaC,MACxCf,EAAEP,QAAQuB,QAAQ,SAEbpB,QAAQC,IAAI,uCAAwCe,OAGxD,GAGTF,EAAU,SAASlB,GACjB,IACAyB,EAAWjB,EAAE,aAAaS,KAAK,UAQ/B,OAPIQ,EAASC,MAAM,OACjBD,EAAWzB,EAEXyB,GAAsBzB,EAExBI,QAAQC,IAAI,sBAAuBoB,GACnCjB,EAAE,aAAaS,KAAK,SAAUQ,GACvBjB,EAAE,aAAamB,UAGxBnB,EAAEoB,UAAUC,MAAM,WAChB,OAAOrB,EAAE,aAAasB,GAAG,QAAS/B,MAGnCgC,KAAKC"} \ No newline at end of file -- GitLab From 2e0b3ce04546cee0407eeb0e0523fc92f1deae84 Mon Sep 17 00:00:00 2001 From: Maxime Besson Date: Mon, 5 Sep 2022 14:26:23 +0200 Subject: [PATCH 5/7] Unit tests for SSL ajax auth (#2792) --- lemonldap-ng-portal/t/29-AuthSSL.t | 162 +++++++++++++++++++++++++---- 1 file changed, 144 insertions(+), 18 deletions(-) diff --git a/lemonldap-ng-portal/t/29-AuthSSL.t b/lemonldap-ng-portal/t/29-AuthSSL.t index 1b89443d06..d0fcc66f06 100644 --- a/lemonldap-ng-portal/t/29-AuthSSL.t +++ b/lemonldap-ng-portal/t/29-AuthSSL.t @@ -1,21 +1,41 @@ use Test::More; use IO::String; +use URI; use strict; require 't/test-lib.pm'; my $res; -subtest 'Legacy AJAX SSL Auth' => sub { +sub testUserTokenSSLAuth { + my %params = @_; + my $choice = $params{'choice'}; + my $client = LLNG::Manager::Test->new( { ini => { - logLevel => 'error', - useSafeJail => 1, - authentication => 'SSL', - userDB => 'Null', - SSLVar => 'SSL_CLIENT_S_DN_Custom', - sslByAjax => 1, - sslHost => 'https://authssl.example.com:19876' + logLevel => 'error', + useSafeJail => 1, + ( + $choice + ? ( + authentication => 'Choice', + userDB => 'Same', + authChoiceParam => 'test', + authChoiceModules => { + '1_demo' => 'Demo;Demo;Null', + '2_ssl' => 'SSL;Demo;Null', + }, + ) + : ( + authentication => 'SSL', + userDB => 'Demo', + ) + ), + + SSLVar => 'SSL_CLIENT_S_DN_Custom', + sslByAjax => 1, + sslHost => 'https://authssl.example.com/authssl', + restSessionServer => 1, } } ); @@ -32,23 +52,123 @@ subtest 'Legacy AJAX SSL Auth' => sub { ok( $res->[2]->[0] =~ -m%%s, +m%%s, ' SSL AJAX URL found' ) or print STDERR Dumper( $res->[2]->[0] ); ok( $res->[2]->[0] =~ qr%[2]->[0] ); - ok( $res->[2]->[0] =~ /ssl\.(?:min\.)?js/, 'Get ssl javascript' ) + my $scriptname = "ssl" . ( $choice ? "Choice" : "" ) . "(?:min)?\.js"; + ok( $res->[2]->[0] =~ /$scriptname/, 'Get ssl javascript' ) or print STDERR Dumper( $res->[2]->[0] ); - count(4); - my ( $host, $url, $query ) = expectForm( $res, '#', undef, 'nossl' ); + my ( $host, $url, $query ) = + expectForm( $res, '#', undef, 'nossl', 'ajax_auth_token' ); # AJAX request + ok( + $res = $client->_get( + '/authssl', + accept => 'application/json', + custom => { + SSL_CLIENT_S_DN_Custom => 'dwho', + SSL_CLIENT_I_DN => 'cn=MyIssuer' + } + ), + 'Auth query' + ); + my $json = expectJSON($res); + ok( $json->{ajax_auth_token}, "User token was returned" ); + my $ajax_auth_token = $json->{ajax_auth_token}; + + $query .= "&ajax_auth_token=$ajax_auth_token"; + + ok( + $res = $client->_post( + '/', IO::String->new($query), + length => length($query), + accept => 'text/html', + ), + 'Post form' + ); + my $id = expectCookie($res); + expectRedirection( $res, 'http://test1.example.com/' ); + expectSessionAttributes( + $client, $id, + authenticationLevel => 5, + _auth => 'SSL', + _Issuer => 'cn=MyIssuer', + _user => 'dwho', + uid => 'dwho' + ); +} + +sub testLegacyAjaxSSL { + my %params = @_; + my $choice = $params{'choice'}; + my $client = LLNG::Manager::Test->new( { + ini => { + logLevel => 'error', + useSafeJail => 1, + ( + $choice + ? ( + authentication => 'Choice', + userDB => 'Same', + authChoiceParam => 'test', + authChoiceModules => { + '1_demo' => 'Demo;Demo;Null', + '2_ssl' => 'SSL;Demo;Null', + }, + sslHost => + 'https://authssl.example.com:19876/?test=2_ssl' + ) + : ( + authentication => 'SSL', + userDB => 'Demo', + sslHost => 'https://authssl.example.com:19876/' + ) + ), + SSLVar => 'SSL_CLIENT_S_DN_Custom', + sslByAjax => 1, + } + } + ); + ok( $res = $client->_get( '/', - cookie => $pdata, + query => 'url=aHR0cDovL3Rlc3QxLmV4YW1wbGUuY29tLw==', + accept => 'text/html' + ), + 'Get Menu' + ); + my $pdata = 'lemonldappdata=' . expectCookie( $res, 'lemonldappdata' ); + + ok( + $res->[2]->[0] =~ +m%%s, + ' SSL AJAX URL found' + ) or print STDERR Dumper( $res->[2]->[0] ); + my $sslHost = URI->new($1); + is( $sslHost->authority, "authssl.example.com:19876", "Correct hostname" ); + is( $sslHost->path, "/", "Correct path" ); + is( $sslHost->query, ( $choice ? "test=2_ssl" : undef ), "Correct query" ); + + ok( $res->[2]->[0] =~ qr%[2]->[0] ); + my $scriptname = "ssl" . ( $choice ? "Choice" : "" ) . "(?:min)?\.js"; + ok( $res->[2]->[0] =~ /$scriptname/, 'Get ssl javascript' ) + or print STDERR Dumper( $res->[2]->[0] ); + + my ( $host, $url, $query ) = expectForm( $res, '#', undef, 'nossl' ); + + # AJAX request + ok( + $res = $client->_get( + $sslHost->path, + query => $sslHost->query, accept => 'application/json', custom => { SSL_CLIENT_S_DN_Custom => 'dwho' } ), @@ -59,9 +179,6 @@ m%