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:
parent
62f16721ff
commit
2f9e6aa623
|
@ -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)';
|
||||
|
|
|
@ -678,6 +678,9 @@ sub attributes {
|
|||
'casAppMetaDataOptionsService' => {
|
||||
'type' => 'url'
|
||||
},
|
||||
'casAppMetaDataOptionsUserAttribute' => {
|
||||
'type' => 'text'
|
||||
},
|
||||
'casAttr' => {
|
||||
'type' => 'text'
|
||||
},
|
||||
|
|
|
@ -1824,6 +1824,10 @@ sub attributes {
|
|||
type => 'url',
|
||||
documentation => 'CAS App service',
|
||||
},
|
||||
casAppMetaDataOptionsUserAttribute => {
|
||||
type => 'text',
|
||||
documentation => 'CAS User attribute',
|
||||
},
|
||||
casAppMetaDataOptionsRule => {
|
||||
type => 'text',
|
||||
test => $perlExpr,
|
||||
|
|
|
@ -252,6 +252,7 @@ sub cTrees {
|
|||
form => 'simpleInputContainer',
|
||||
nodes => [
|
||||
'casAppMetaDataOptionsService',
|
||||
'casAppMetaDataOptionsUserAttribute',
|
||||
'casAppMetaDataOptionsRule'
|
||||
]
|
||||
},
|
||||
|
|
|
@ -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
|
@ -116,6 +116,7 @@
|
|||
"casAppMetaDataOptions":"خيارات",
|
||||
"casAppMetaDataOptionsService":"خدمة أل يو أر ل",
|
||||
"casAppMetaDataOptionsRule":"القاعدة",
|
||||
"casAppMetaDataOptionsUserAttribute":"User attribute",
|
||||
"casAppName":"اسم التطبيق كاس",
|
||||
"casAttr":"تسجيل الدخول كاس",
|
||||
"casAttributes":"السمات المصدرة لي كاس",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -116,6 +116,7 @@
|
|||
"casAppMetaDataOptions":"选项",
|
||||
"casAppMetaDataOptionsService":"服务 URL",
|
||||
"casAppMetaDataOptionsRule":"规则",
|
||||
"casAppMetaDataOptionsUserAttribute":"User attribute",
|
||||
"casAppName":"CAS App 名称",
|
||||
"casAttr":"CAS 登录",
|
||||
"casAttributes":"CAS 声明的attributes",
|
||||
|
|
|
@ -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");
|
||||
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
Loading…
Reference in New Issue
Block a user