From c4b22d38c9050c78d3421f86c1e81fde23a88a59 Mon Sep 17 00:00:00 2001 From: Maxime Besson Date: Thu, 11 Nov 2021 16:18:00 +0100 Subject: [PATCH] Add unit tests (#2656) --- lemonldap-ng-portal/t/32-CAS-Proxy.t | 254 +++++++++++++++++++++++++++ 1 file changed, 254 insertions(+) create mode 100644 lemonldap-ng-portal/t/32-CAS-Proxy.t diff --git a/lemonldap-ng-portal/t/32-CAS-Proxy.t b/lemonldap-ng-portal/t/32-CAS-Proxy.t new file mode 100644 index 000000000..1d026acf4 --- /dev/null +++ b/lemonldap-ng-portal/t/32-CAS-Proxy.t @@ -0,0 +1,254 @@ +use lib 'inc'; +use Test::More; # skip_all => 'CAS is in rebuild'; +use strict; +use IO::String; +use LWP::UserAgent; +use LWP::Protocol::PSGI; +use MIME::Base64; +use XML::LibXML; + +use LWP::UserAgent; +use LWP::Protocol::PSGI; + +our $PGTs = {}; + +LWP::Protocol::PSGI->register( + sub { + my $req = Plack::Request->new(@_); + my $iou = $req->param("pgtIou"); + my $pgt = $req->param("pgtId"); + $PGTs->{$iou} = $pgt; + return [ 200, [], [] ]; + } +); + +BEGIN { + require 't/test-lib.pm'; +} + +my $debug = 'error'; +my ( $issuer, $res ); + +eval { require XML::Simple }; +plan skip_all => "Missing dependencies: $@" if ($@); + +ok( $issuer = issuer(), 'Issuer portal' ); +count(1); + +my $s = "user=french&password=french"; + +# Login +ok( + $res = $issuer->_post( + '/', + IO::String->new($s), + accept => 'text/html', + length => length($s), + ), + 'Post authentication' +); +count(1); +my $idpId = expectCookie($res); + +# Request to an unknown service is rejected +ok( + $res = $issuer->_get( + '/cas/login', + cookie => "lemonldap=$idpId", + query => 'service=http://auth.sp3.com/', + accept => 'text/html' + ), + 'Query CAS server' +); +count(1); + +expectPortalError( $res, 68, "Unauthorized CAS service" ); + +my $ticket; +my $iou; + +# Get a ticket for CAS application +$ticket = casGetTicket( $issuer, $idpId, "http://casapp.com/" ); + +# CAS application gets PGT +my $pgt = casGetPgt( $issuer, $ticket, "http://casapp.com/", + "http://casapp.com/proxy" ); + +# CAS application gets PT for web service +my $pt = casGetProxyTicket( $issuer, $pgt, "http://service.com/srv" ); + +# Service validate PT and gets PGT +my $pgt2 = casGetPgt( $issuer, $pt, "http://service.com/srv", + "http://service.com/proxy" ); + +# Service gets PT for sub-service +my $pt2 = casGetProxyTicket( $issuer, $pgt2, "http://subservice.com/srv" ); + +# Sub-service validates PT +my $res = casGetProxyResponse( $issuer, $pt2, "http://subservice.com/srv" ); +expectCasSuccess($res); + +is_deeply( [ + casXPathAll( + $res->[2]->[0], + '/cas:serviceResponse/cas:authenticationSuccess' + . '/cas:proxies/cas:proxy/text()' + ) + ], + [ "http://service.com/proxy", "http://casapp.com/proxy" ], + "Found proxies in correct order" +); +count(1); + +# Make sure PGT is still valid a long time later +Time::Fake->offset("+10h"); +my $pt = casGetProxyTicket( $issuer, $pgt, "http://service.com/srv" ); +expectCasSuccess( + casGetProxyResponse( $issuer, $pt, "http://service.com/srv" ) ); + +clean_sessions(); +done_testing( count() ); + +sub issuer { + return LLNG::Manager::Test->new( { + ini => { + logLevel => $debug, + domain => 'idp.com', + portal => 'http://auth.idp.com', + authentication => 'Demo', + userDB => 'Same', + issuerDBCASActivation => 1, + casAttr => 'uid', + casTicketExpiration => '300', + casAppMetaDataOptions => { + sp => { + casAppMetaDataOptionsService => 'http://casapp.com/', + }, + }, + casAppMetaDataExportedVars => { + sp => { + cn => 'cn', + mail => 'mail', + uid => 'uid', + }, + sp2 => { + cn => 'cn', + mail => 'mail', + uid => 'uid', + }, + }, + casAccessControlPolicy => 'error', + multiValuesSeparator => ';', + } + } + ); +} + +sub casGetTicket { + my ( $issuer, $id, $service ) = @_; + ok( + my $res = $issuer->_get( + '/cas/login', + cookie => "lemonldap=$id", + query => 'service=' . $service, + accept => 'text/html' + ), + 'Query CAS server' + ); + count(1); + my ($ticket) = + expectRedirection( $res, qr#^http://casapp.com/\?.*ticket=([^&]+)# ); + + return $ticket; +} + +sub casGetPgt { + my ( $issuer, $ticket, $service, $pgtUrl ) = @_; + ok( + my $res = $issuer->_get( + '/cas/p3/proxyValidate', + query => buildForm( { + service => $service, + ticket => $ticket, + pgtUrl => $pgtUrl, + } + ), + accept => 'text/html' + ), + 'Query CAS server' + ); + count(1); + + expectOK($res); + + my $pgt = casXPath( $res->[2]->[0], '//cas:proxyGrantingTicket/text()' ); + return $PGTs->{$pgt}; +} + +sub casGetProxyResponse { + my ( $issuer, $ticket, $service ) = @_; + ok( + my $res = $issuer->_get( + '/cas/p3/proxyValidate', + query => buildForm( { service => $service, ticket => $ticket } ), + accept => 'text/html' + ), + 'Query CAS server' + ); + + expectOK($res); + count(1); + return $res; +} + +sub casGetProxyTicket { + my ( $issuer, $pgt, $service ) = @_; + ok( + my $res = $issuer->_get( + '/cas/proxy', + query => buildForm( { + pgt => $pgt, + targetService => $service, + } + ), + accept => 'text/html' + ), + 'Query CAS server' + ); + count(1); + + my $pt = casXPath( $res->[2]->[0], + '/cas:serviceResponse/cas:proxySuccess/cas:proxyTicket/text()' ); + return $pt; +} + +sub expectCasSuccess { + my ($res) = @_; + my $content = $res->[2]->[0]; + ok( + casXPath( $content, '/cas:serviceResponse/cas:authenticationSuccess' ), + "Cas response contains authenticationSuccess" + ); + count(1); +} + +sub casXPath { + my ( $xmlString, $expr ) = @_; + + my $dom = XML::LibXML->load_xml( string => $xmlString ); + my $xpc = XML::LibXML::XPathContext->new($dom); + $xpc->registerNs( 'cas', 'http://www.yale.edu/tp/cas' ); + my ($match) = $xpc->findnodes($expr); + ok($match); + count(1); + return $match; +} + +sub casXPathAll { + my ( $xmlString, $expr ) = @_; + + my $dom = XML::LibXML->load_xml( string => $xmlString ); + my $xpc = XML::LibXML::XPathContext->new($dom); + $xpc->registerNs( 'cas', 'http://www.yale.edu/tp/cas' ); + return $xpc->findnodes($expr); +}