Fix proxied services in CAS (#1183)

environments/ppa-mbqj77/deployments/1
Xavier Guimard 8 years ago
parent f07c2e40cd
commit 00423fc223
  1. 2
      lemonldap-ng-common/lib/Lemonldap/NG/Common/Conf/Constants.pm
  2. 2
      lemonldap-ng-common/lib/Lemonldap/NG/Common/Conf/ReConstants.pm
  3. 8
      lemonldap-ng-manager/lib/Lemonldap/NG/Manager/Build/Tree.pm
  4. 1
      lemonldap-ng-manager/site/htdocs/static/languages/en.json
  5. 1
      lemonldap-ng-manager/site/htdocs/static/languages/fr.json
  6. 2
      lemonldap-ng-manager/site/htdocs/static/reverseTree.json
  7. 2
      lemonldap-ng-manager/site/htdocs/static/struct.json
  8. 2
      lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Auth/CAS.pm
  9. 9
      lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Lib/CAS.pm
  10. 6
      lemonldap-ng-portal/t/31-Auth-and-issuer-CAS-default.t
  11. 260
      lemonldap-ng-portal/t/31-Auth-and-issuer-CAS-proxied.t

@ -25,7 +25,7 @@ use constant SESSIONSEXPLORERSECTION => "sessionsExplorer";
use constant APPLYSECTION => "apply";
our $hashParameters = qr/^(?:(?:l(?:o(?:ca(?:lSessionStorageOption|tionRule)|goutService)|dapExportedVar|wp(?:Ssl)?Opt)|(?:(?:d(?:emo|bi)|facebook|webID)ExportedVa|exported(?:Heade|Va))r|g(?:r(?:antSessionRule|oup)|lobalStorageOption)|n(?:otificationStorageOption|ginxCustomHandler)|re(?:moteGlobalStorageOption|loadUrl)|macro)s|o(?:idc(?:RPMetaData(?:(?:Option(?:sExtraClaim)?|ExportedVar)s|Node)|OPMetaData(?:(?:ExportedVar|Option)s|J(?:SON|WKS)|Node)|S(?:erviceMetaDataAuthnContext|torageOptions))|penIdExportedVars)|s(?:aml(?:S(?:PMetaData(?:(?:ExportedAttribute|Option)s|Node|XML)|torageOptions)|IDPMetaData(?:(?:ExportedAttribute|Option)s|Node|XML))|essionDataToRemember|laveExportedVars)|c(?:as(?:S(?:rvMetaData(?:(?:ExportedVar|Option)s|Node)|torageOptions)|A(?:ppMetaData(?:(?:ExportedVar|Option)s|Node)|ttributes))|(?:ustomAddParam|ombModule)s)|p(?:ersistentStorageOptions|o(?:rtalSkinRules|st))|a(?:uthChoiceModules|pplicationList)|v(?:hostOptions|irtualHost)|S(?:MTPTLSOpts|SLVarIf))$/;
our @sessionTypes = ( 'remoteGlobal', 'cas', 'global', 'localSession', 'persistent', 'saml', 'oidc' );
our @sessionTypes = ( 'remoteGlobal', 'global', 'localSession', 'persistent', 'saml', 'oidc', 'cas' );
sub NO {qr/^(?:off|no|0)?$/i}

@ -59,7 +59,7 @@ our $authParameters = {
yubikeyParams => [qw(yubikeyAuthnLevel yubikeyClientID yubikeySecretKey yubikeyPublicIDSize)],
};
our $issuerParameters = {
issuerDBCAS => [qw(issuerDBCASActivation issuerDBCASPath issuerDBCASRule casAttr casAttributes casAccessControlPolicy casStorage casStorageOptions)],
issuerDBCAS => [qw(issuerDBCASActivation issuerDBCASPath issuerDBCASRule casAttr casAttributes casAccessControlPolicy)],
issuerDBGet => [qw(issuerDBGetActivation issuerDBGetPath issuerDBGetRule issuerDBGetParameters)],
issuerDBOpenID => [qw(issuerDBOpenIDActivation issuerDBOpenIDPath issuerDBOpenIDRule openIdIssuerSecret openIdAttr openIdSPList openIdSreg_fullname openIdSreg_nickname openIdSreg_language openIdSreg_postcode openIdSreg_timezone openIdSreg_country openIdSreg_gender openIdSreg_email openIdSreg_dob)],
issuerDBOpenIDConnect => [qw(issuerDBOpenIDConnectActivation issuerDBOpenIDConnectPath issuerDBOpenIDConnectRule)],

@ -121,7 +121,7 @@ sub tree {
{
title => 'casParams',
help => 'authcas.html',
nodes => [ 'casAuthnLevel' ]
nodes => ['casAuthnLevel']
},
{
title => 'dbiParams',
@ -405,8 +405,6 @@ sub tree {
'casAttr',
'casAttributes',
'casAccessControlPolicy',
'casStorage',
'casStorageOptions'
]
}
]
@ -907,6 +905,10 @@ sub tree {
},
'oidcOPMetaDataNodes',
'oidcRPMetaDataNodes',
{
title => 'casServiceMetadata',
nodes => [ 'casStorage', 'casStorageOptions' ]
},
'casSrvMetaDataNodes',
'casAppMetaDataNodes',
];

@ -113,6 +113,7 @@
"casAttr": "CAS login",
"casAttributes": "CAS exported attributes",
"casParams": "CAS parameters",
"casServiceMetadata": "CAS service",
"casSrv": "CAS Server",
"casSrvMetaDataExportedVars": "Exported attributes",
"casSrvMetaDataOptions": "Options",

@ -113,6 +113,7 @@
"casAttr": "Identifiant CAS",
"casAttributes": "Attributs CAS",
"casParams": "Paramètres CAS",
"casServiceMetadata": "Service CAS",
"casSrv": "Serveur CAS",
"casSrvMetaDataExportedVars": "Attributs exportés",
"casSrvMetaDataOptions": "Options",

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

@ -218,7 +218,7 @@ sub extractFormInfo {
unless ($pt) {
$self->logger->error(
"CAS: No proxy ticket recevied for service $service");
"CAS: No proxy ticket received for service $service");
return PE_ERROR;
}

@ -35,7 +35,8 @@ sub loadSrv {
unless ( $self->conf->{casSrvMetaDataOptions}
and %{ $self->conf->{casSrvMetaDataOptions} } )
{
$self->logger->warn("No CAS application found in configuration");
$self->logger->error("No CAS servers found in configuration");
return 0;
}
$self->casSrvList( $self->conf->{casSrvMetaDataOptions} );
return 1;
@ -47,7 +48,7 @@ sub loadApp {
unless ( $self->conf->{casAppMetaDataOptions}
and %{ $self->conf->{casAppMetaDataOptions} } )
{
$self->logger->warn("No CAS server found in configuration");
$self->logger->info("No CAS apps found in configuration");
}
foreach ( keys %{ $self->conf->{casAppMetaDataOptions} } ) {
my $tmp =
@ -329,7 +330,9 @@ sub validateST {
my $proxied = $srvConf->{casSrvMetaDataOptionsProxiedServices} || {};
my $proxy_url;
if (%$proxied) {
$proxy_url = $self->p->fullUrl($req) . '?casProxy=1';
$proxy_url = $self->p->fullUrl($req);
die if($proxy_url =~ /casProxy=1/);
$proxy_url .= ( $proxy_url =~ /\?/ ? '&' : '?' ) . 'casProxy=1';
if ( $self->conf->{authChoiceParam}
and my $tmp = $req->param( $self->conf->{authChoiceParam} ) )
{

@ -160,7 +160,7 @@ no warnings 'redefine';
sub LWP::UserAgent::request {
my ( $self, $req ) = @_;
ok( $req->uri =~ m#http://auth.((?:id|s)p).com([^\?]*)(?:\?(.*))?$#,
'SOAP request' );
' SOAP request' );
my $host = $1;
my $url = $2;
my $query = $3;
@ -175,7 +175,7 @@ sub LWP::UserAgent::request {
query => $query,
type => 'application/xml',
),
"Execute POST request to $url"
" Execute POST request to $url"
);
}
else {
@ -185,7 +185,7 @@ sub LWP::UserAgent::request {
type => 'application/xml',
query => $query,
),
"Execute request to $url"
" Execute request to $url"
);
}
expectOK($res);

@ -0,0 +1,260 @@
use Test::More; # skip_all => 'CAS is in rebuild';
use strict;
use IO::String;
use MIME::Base64;
BEGIN {
require 't/test-lib.pm';
}
my $debug = 'error';
my ( $issuer, $sp, $res );
my %handlerOR = ( issuer => [], sp => [] );
no warnings 'redefine';
ok( $issuer = issuer(), 'Issuer portal' );
$handlerOR{issuer} = \@Lemonldap::NG::Handler::Main::_onReload;
count(1);
switch ('sp');
ok( $sp = sp(), 'SP portal' );
count(1);
$handlerOR{sp} = \@Lemonldap::NG::Handler::Main::_onReload;
# Simple SP access
ok(
$res = $sp->_get(
'/', accept => 'text/html',
),
'Unauth SP request'
);
count(1);
expectRedirection( $res,
'http://auth.idp.com/cas/login?service=http%3A%2F%2Fauth.sp.com%2F' );
# Query IdP
switch ('issuer');
ok(
$res = $issuer->_get(
'/cas/login',
query => 'service=http://auth.sp.com/',
accept => 'text/html'
),
'Query CAS server'
);
count(1);
expectOK($res);
# Try to authenticate to IdP
my $body = $res->[2]->[0];
$body =~ s/^.*?<form.*?>//s;
$body =~ s#</form>.*$##s;
my %fields =
( $body =~ /<input type="hidden".+?name="(.+?)".+?value="(.*?)"/sg );
$fields{user} = $fields{password} = 'french';
use URI::Escape;
my $s = join( '&', map { "$_=" . uri_escape( $fields{$_} ) } keys %fields );
ok(
$res = $issuer->_post(
'/cas/login',
IO::String->new($s),
accept => 'text/html',
length => length($s),
),
'Post authentication'
);
count(1);
my ($query) =
expectRedirection( $res, qr#^http://auth.sp.com/\?(ticket=[^&]+)$# );
my $idpId = expectCookie($res);
# Back to SP
switch ('sp');
ok( $res = $sp->_get( '/', query => $query, accept => 'text/html' ),
'Query SP with ticket' );
count(1);
my $spId = expectCookie($res);
# Test authentication
ok( $res = $sp->_get( '/', cookie => "lemonldap=$spId" ), 'Get / on SP' );
count(1);
expectOK($res);
expectAuthenticatedAs( $res, 'french' );
# Test attributes
ok( $res = $sp->_get("/sessions/global/$spId"), 'Get UTF-8' );
expectOK($res);
ok( $res = eval { JSON::from_json( $res->[2]->[0] ) }, ' GET JSON' )
or print STDERR $@;
ok( $res->{cn} eq 'Frédéric Accents', 'UTF-8 values' )
or explain( $res, 'cn => Frédéric Accents' );
count(3);
# Logout initiated by SP
ok(
$res = $sp->_get(
'/',
query => 'logout',
cookie => "lemonldap=$spId",
accept => 'text/html'
),
'Query SP for logout'
);
count(1);
expectOK($res);
ok(
$res->[2]->[0] =~ m#iframe src="http://auth.idp.com(/cas/logout)\?(.+?)"#s,
'Found iframe'
);
count(1);
# Query IdP with iframe src
my $url = $1;
$query = $2;
ok( getHeader( $res, 'Content-Security-Policy' ) =~ /child-src auth.idp.com/,
'Frame is authorizated' )
or
explain( $res->[1], 'Content-Security-Policy => ...child-src auth.idp.com' );
count(1);
switch ('issuer');
ok(
$res = $issuer->_get(
$url,
query => $query,
accept => 'text/html',
cookie => "lemonldap=$idpId"
),
'Get iframe from IdP'
);
count(1);
expectOK($res);
ok( getHeader( $res, 'Content-Security-Policy' ) !~ /frame-ancestors/,
' Frame can be embedded' )
or explain( $res->[1],
'Content-Security-Policy does not contain a frame-ancestors' );
count(1);
# Verify that user has been disconnected
ok( $res = $issuer->_get( '/', cookie => "lemonldap=$idpId" ), 'Query IdP' );
count(1);
expectReject($res);
switch ('sp');
ok(
$res =
$sp->_get( '/', accept => 'text/html', cookie => "lemonldap=$idpId" ),
'Query IdP'
);
count(1);
expectRedirection( $res,
'http://auth.idp.com/cas/login?service=http%3A%2F%2Fauth.sp.com%2F' );
clean_sessions();
done_testing( count() );
# Redefine LWP methods for tests
no warnings 'redefine';
sub LWP::UserAgent::request {
my ( $self, $req ) = @_;
ok( $req->uri =~ m#http://auth.((?:id|s)p).com([^\?]*)(?:\?(.*))?$#,
' Request to '.$req->uri );
my $host = $1;
my $url = $2;
my $query = $3;
my $res;
my $client = ( $host eq 'idp' ? $issuer : $sp );
if ( $req->method eq 'POST' ) {
my $s = $req->content;
ok(
$res = $client->_post(
$url, IO::String->new($s),
length => length($s),
query => $query,
type => 'application/xml',
),
" Execute POST request to $url"
);
}
else {
ok(
$res = $client->_get(
$url,
type => 'application/xml',
query => $query,
),
" Execute request to $url"
);
}
expectOK($res);
my $httpResp = HTTP::Response->new( $res->[0], 'OK' );
while ( my $name = shift @{ $res->[1] } ) {
$httpResp->header( $name, shift( @{ $res->[1] } ) );
}
$httpResp->content( join( '', @{ $res->[2] } ) );
count(2);
return $httpResp;
}
sub switch {
my $type = shift;
@Lemonldap::NG::Handler::Main::_onReload = @{
$handlerOR{$type};
};
}
sub issuer {
return LLNG::Manager::Test->new(
{
ini => {
logLevel => $debug,
templatesDir => 'site/htdocs/static',
domain => 'idp.com',
portal => 'http://auth.idp.com',
authentication => 'Demo',
userDB => 'Same',
issuerDBCASActivation => 1,
casAttr => 'uid',
casAttributes => { cn => 'cn', uid => 'uid', },
casAccessControlPolicy => 'none',
multiValuesSeparator => ';',
}
}
);
}
sub sp {
return LLNG::Manager::Test->new(
{
ini => {
logLevel => $debug,
domain => 'sp.com',
portal => 'http://auth.sp.com',
authentication => 'CAS',
userDB => 'CAS',
restSessionServer => 1,
issuerDBCASActivation => 0,
multiValuesSeparator => ';',
casSrvMetaDataExportedVars => {
idp => {
cn => 'cn',
mail => 'mail',
uid => 'uid',
}
},
casSrvMetaDataOptions => {
idp => {
casSrvMetaDataOptionsUrl => 'http://auth.idp.com/cas',
casSrvMetaDataOptionsGateway => 0,
casSrvMetaDataOptionsProxiedServices => {
test => 'http://test.sp.com/',
},
}
},
},
}
);
}
Loading…
Cancel
Save