Allow override of username attribute for CAS apps

Global CAS options allows the admistrator to set the session attribute
that gets exported to all CAS application as the main identifier
(cas:user)

This commit adds the ability to override this configuration for a
particular CAS application.

OIDC already allows this

Fixes #1713
This commit is contained in:
Maxime Besson 2019-04-25 20:35:41 +02:00
parent 62f16721ff
commit 2f9e6aa623
15 changed files with 370 additions and 11 deletions

View File

@ -24,7 +24,7 @@ our $specialNodeHash = {
our $doubleHashKeys = 'issuerDBGetParameters';
our $simpleHashKeys = '(?:(?:l(?:o(?:calSessionStorageOption|goutService)|dapExportedVar|wp(?:Ssl)?Opt)|re(?:moteGlobalStorageOption|st2f(?:Verify|Init)Arg|loadUrl)|c(?:as(?:StorageOption|Attribute)|ustomAddParam|ombModule)|(?:(?:d(?:emo|bi)|facebook|webID)E|e)xportedVar|g(?:r(?:antSessionRule|oup)|lobalStorageOption)|n(?:otificationStorageOption|ginxCustomHandler)|p(?:ersistentStorageOption|ortalSkinRule)|macro)s|o(?:idcS(?:erviceMetaDataAuthnContext|torageOptions)|penIdExportedVars)|s(?:(?:amlStorageOption|laveExportedVar)s|essionDataToRemember)|a(?:ut(?:hChoiceMod|oSigninR)ules|pplicationList)|S(?:MTPTLSOpts|SLVarIf))';
our $specialNodeKeys = '(?:(?:(?:saml(?:ID|S)|oidc[OR])P|cas(?:App|Srv))MetaDataNode|virtualHost)s';
our $casAppMetaDataNodeKeys = 'casAppMetaData(?:Options(?:Servic|Rul)e|ExportedVars)';
our $casAppMetaDataNodeKeys = 'casAppMetaData(?:Options(?:UserAttribut|Servic|Rul)e|ExportedVars)';
our $casSrvMetaDataNodeKeys = 'casSrvMetaData(?:Options(?:ProxiedServices|DisplayName|SortNumber|Gateway|Renew|Icon|Url)|ExportedVars)';
our $oidcOPMetaDataNodeKeys = 'oidcOPMetaData(?:Options(?:C(?:lient(?:Secret|ID)|heckJWTSignature|onfigurationURI)|S(?:toreIDToken|ortNumber|cope)|TokenEndpointAuthMethod|(?:JWKSTimeou|Promp)t|I(?:DTokenMaxAge|con)|U(?:iLocales|seNonce)|Display(?:Name)?|AcrValues|MaxAge)|ExportedVars|J(?:SON|WKS))';
our $oidcRPMetaDataNodeKeys = 'oidcRPMetaData(?:Options(?:(?:PostLogoutRedirectUri|ExtraClaim)s|I(?:DToken(?:Expiration|SignAlg)|con)|Logout(?:SessionRequired|Type|Url)|AccessTokenExpiration|R(?:edirectUris|ule)|Client(?:Secret|ID)|BypassConsent|DisplayName|UserIDAttr)|ExportedVars)';

View File

@ -678,6 +678,9 @@ sub attributes {
'casAppMetaDataOptionsService' => {
'type' => 'url'
},
'casAppMetaDataOptionsUserAttribute' => {
'type' => 'text'
},
'casAttr' => {
'type' => 'text'
},

View File

@ -1824,6 +1824,10 @@ sub attributes {
type => 'url',
documentation => 'CAS App service',
},
casAppMetaDataOptionsUserAttribute => {
type => 'text',
documentation => 'CAS User attribute',
},
casAppMetaDataOptionsRule => {
type => 'text',
test => $perlExpr,

View File

@ -252,6 +252,7 @@ sub cTrees {
form => 'simpleInputContainer',
nodes => [
'casAppMetaDataOptionsService',
'casAppMetaDataOptionsUserAttribute',
'casAppMetaDataOptionsRule'
]
},

View File

@ -17,6 +17,11 @@ function templates(tpl,key) {
"id" : tpl+"s/"+key+"/"+"casAppMetaDataOptionsService",
"title" : "casAppMetaDataOptionsService"
},
{
"get" : tpl+"s/"+key+"/"+"casAppMetaDataOptionsUserAttribute",
"id" : tpl+"s/"+key+"/"+"casAppMetaDataOptionsUserAttribute",
"title" : "casAppMetaDataOptionsUserAttribute"
},
{
"get" : tpl+"s/"+key+"/"+"casAppMetaDataOptionsRule",
"id" : tpl+"s/"+key+"/"+"casAppMetaDataOptionsRule",

File diff suppressed because one or more lines are too long

View File

@ -116,6 +116,7 @@
"casAppMetaDataOptions":"خيارات",
"casAppMetaDataOptionsService":"خدمة أل يو أر ل",
"casAppMetaDataOptionsRule":"القاعدة",
"casAppMetaDataOptionsUserAttribute":"User attribute",
"casAppName":"اسم التطبيق كاس",
"casAttr":"تسجيل الدخول كاس",
"casAttributes":"السمات المصدرة لي كاس",
@ -986,4 +987,4 @@
"samlRelayStateTimeout":"تناوب حالة مهلة الجلسة ",
"samlUseQueryStringSpecific":"استخدام أسلوب query_string المعين",
"samlOverrideIDPEntityID":"Override Entity ID when acting as IDP"
}
}

View File

@ -116,6 +116,7 @@
"casAppMetaDataOptions":"Optionen",
"casAppMetaDataOptionsService":"Service URL",
"casAppMetaDataOptionsRule":"Regel",
"casAppMetaDataOptionsUserAttribute":"User attribute",
"casAppName":"CAS App Name",
"casAttr":"CAS login",
"casAttributes":"CAS exported attributes",
@ -986,4 +987,4 @@
"samlRelayStateTimeout":"RelayState session timeout",
"samlUseQueryStringSpecific":"Use specific query_string method",
"samlOverrideIDPEntityID":"Override Entity ID when acting as IDP"
}
}

View File

@ -116,6 +116,7 @@
"casAppMetaDataOptions":"Options",
"casAppMetaDataOptionsService":"Service URL",
"casAppMetaDataOptionsRule":"Rule",
"casAppMetaDataOptionsUserAttribute":"User attribute",
"casAppName":"CAS App Name",
"casAttr":"CAS login",
"casAttributes":"CAS exported attributes",

View File

@ -116,6 +116,7 @@
"casAppMetaDataOptions":"Options",
"casAppMetaDataOptionsService":"URL du service",
"casAppMetaDataOptionsRule":"Règle",
"casAppMetaDataOptionsUserAttribute":"Attribut de l'identifiant",
"casAppName":"Nom de l'application CAS",
"casAttr":"Identifiant CAS",
"casAttributes":"Attributs CAS",

View File

@ -116,6 +116,7 @@
"casAppMetaDataOptions":"Opzioni",
"casAppMetaDataOptionsService":"URL del servizio",
"casAppMetaDataOptionsRule":"Regola",
"casAppMetaDataOptionsUserAttribute":"User attribute",
"casAppName":"Nome App CAS",
"casAttr":"Login CAS",
"casAttributes":"Attributi CAS esportati",
@ -986,4 +987,4 @@
"samlRelayStateTimeout":"Timeout di sessione di RelayState",
"samlUseQueryStringSpecific":"Utilizza il metodo specifico query_string",
"samlOverrideIDPEntityID":"Sostituisci l'ID entità quando agisce come IDP"
}
}

View File

@ -116,6 +116,7 @@
"casAppMetaDataOptions":"Tùy chọn",
"casAppMetaDataOptionsService":"Dịch vụ URL",
"casAppMetaDataOptionsRule":"Quy tắc",
"casAppMetaDataOptionsUserAttribute":"User attribute",
"casAppName":"Tên ứng dụng CAS",
"casAttr":"Đăng nhập CAS ",
"casAttributes":"Thuộc tính CAS đã được xuất",
@ -986,4 +987,4 @@
"samlRelayStateTimeout":"Thời gian hết hạn phiên RelayState ",
"samlUseQueryStringSpecific":"Sử dụng phương pháp query_string cụ thể",
"samlOverrideIDPEntityID":"Override Entity ID when acting as IDP"
}
}

View File

@ -116,6 +116,7 @@
"casAppMetaDataOptions":"选项",
"casAppMetaDataOptionsService":"服务 URL",
"casAppMetaDataOptionsRule":"规则",
"casAppMetaDataOptionsUserAttribute":"User attribute",
"casAppName":"CAS App 名称",
"casAttr":"CAS 登录",
"casAttributes":"CAS 声明的attributes",
@ -986,4 +987,4 @@
"samlRelayStateTimeout":"RelayState session timeout",
"samlUseQueryStringSpecific":"Use specific query_string method",
"samlOverrideIDPEntityID":"Override Entity ID when acting as IDP"
}
}

View File

@ -458,8 +458,17 @@ sub validate {
}
# Get username
my $username = $localSession->data->{ $self->conf->{casAttr}
|| $self->conf->{whatToTrace} };
my $app = $casServiceSession->data->{_casApp};
my $username_attribute =
( $app
and $self->conf->{casAppMetaDataOptions}->{$app}
->{casAppMetaDataOptionsUserAttribute} )
? $self->conf->{casAppMetaDataOptions}->{$app}
->{casAppMetaDataOptionsUserAttribute}
: ( $self->conf->{casAttr}
|| $self->conf->{whatToTrace} );
my $username = $localSession->data->{$username_attribute};
$self->logger->debug("Get username $username");
@ -728,8 +737,16 @@ sub _validate2 {
}
# Get username
my $username = $localSession->data->{ $self->conf->{casAttr}
|| $self->conf->{whatToTrace} };
my $username_attribute =
( $app
and $self->conf->{casAppMetaDataOptions}->{$app}
->{casAppMetaDataOptionsUserAttribute} )
? $self->conf->{casAppMetaDataOptions}->{$app}
->{casAppMetaDataOptionsUserAttribute}
: ( $self->conf->{casAttr}
|| $self->conf->{whatToTrace} );
my $username = $localSession->data->{$username_attribute};
$self->logger->debug("Get username $username");

View File

@ -0,0 +1,322 @@
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;
BEGIN {
require 't/test-lib.pm';
}
my $debug = 'error';
my ( $issuer, $sp, $res );
my %handlerOR = ( issuer => [], sp => [] );
# Redefine LWP methods for tests
LWP::Protocol::PSGI->register(
sub {
my $req = Plack::Request->new(@_);
ok( $req->uri =~ m#http://auth.((?:id|s)p).com([^\?]*)(?:\?(.*))?$#,
'SOAP request' );
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);
ok( getHeader( $res, 'Content-Type' ) =~ m#xml#, 'Content is XML' )
or explain( $res->[1], 'Content-Type => application/xml' );
count(3);
return $res;
}
);
ok( $issuer = issuer(), 'Issuer portal' );
$handlerOR{issuer} = \@Lemonldap::NG::Handler::Main::_onReload;
count(1);
switch ('sp');
&Lemonldap::NG::Handler::Main::cfgNum( 0, 0 );
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);
my $pdata = 'lemonldappdata=' . expectCookie( $res, 'lemonldappdata' );
# Try to authenticate with an unauthorized user 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} = 'dwho';
use URI::Escape;
my $s = join( '&', map { "$_=" . uri_escape( $fields{$_} ) } keys %fields );
ok(
$res = $issuer->_post(
'/cas/login',
IO::String->new($s),
cookie => $pdata,
accept => 'text/html',
length => length($s),
),
'Post authentication'
);
count(1);
ok( $res->[2]->[0] =~ /trmsg="68"/, 'Reject reason is 68' )
or print STDERR Dumper( $res->[2]->[0] );
count(1);
# 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);
$pdata = 'lemonldappdata=' . expectCookie( $res, 'lemonldappdata' );
# Try to authenticate with an authorized to IdP
$body = $res->[2]->[0];
$body =~ s/^.*?<form.*?>//s;
$body =~ s#</form>.*$##s;
%fields = ( $body =~ /<input type="hidden".+?name="(.+?)".+?value="(.*?)"/sg );
$fields{user} = $fields{password} = 'french';
use URI::Escape;
$s = join( '&', map { "$_=" . uri_escape( $fields{$_} ) } keys %fields );
ok(
$res = $issuer->_post(
'/cas/login',
IO::String->new($s),
cookie => $pdata,
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, with mail as the main identity attribute
ok( $res = $sp->_get( '/', cookie => "lemonldap=$spId" ), 'Get / on SP' );
count(1);
expectOK($res);
expectAuthenticatedAs( $res, 'fa@badwolf.org' );
# 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);
expectRedirection( $res, 'http://auth.sp.com/?logout' );
my $h = getHeader( $res, 'Content-Security-Policy' );
ok( ( not $h or $h !~ /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() );
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,
issuerDBCASRule => '$uid eq "french"',
casAttr => 'uid',
casAccessControlPolicy => 'error',
multiValuesSeparator => ';',
casAppMetaDataExportedVars => {
sp => {
cn => 'cn',
mail => 'mail',
uid => 'uid',
}
},
casAppMetaDataOptions => {
sp => {
casAppMetaDataOptionsService => 'http://auth.sp.com',
casAppMetaDataOptionsUserAttribute => 'mail',
},
sp2 => {
casAppMetaDataOptionsService => 'http://auth.sp2.com',
},
},
}
}
);
}
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 => ';',
exportedVars => {
cn => 'cn',
},
casSrvMetaDataExportedVars => {
idp => {
cn => 'cn',
mail => 'mail',
uid => 'uid',
}
},
casSrvMetaDataOptions => {
idp => {
casSrvMetaDataOptionsUrl => 'http://auth.idp.com/cas',
casSrvMetaDataOptionsGateway => 0,
}
},
},
}
);
}