Merge branch 'v2.0' into 2683

This commit is contained in:
Christophe Maudoux 2022-01-15 11:04:43 +01:00
commit 344eae6f3e
39 changed files with 671 additions and 57 deletions

View File

@ -71,6 +71,10 @@ Then go in ``CAS Service`` to define:
matching. By default, LemonLDAP::NG will try to find a declared CAS
Application matching the hostname of the requested application if it cannot
find a match using the full path. See :ref:`idpcas-url-matching` for details
- **Temporary ticket lifetime**: (since *2.0.14*): restricts how long Service
and Proxy tickets are valid after being generated. For compatibility, the
default value of ``0`` means they are valid for the entire session duration.
But the CAS spefications recommends ``300`` (5 minutes).
.. tip::

View File

@ -62,6 +62,7 @@ casSrvMetaDataOptions Root of CAS server optio
casStorage Apache::Session module to store CAS user data ✔
casStorageOptions Apache::Session module parameters ✔
casStrictMatching Disable host-based matching of CAS services ✔
casTicketExpiration CAS Service and Proxy tickets TTL ✔
cda Enable Cross Domain Authentication ✔ ✔
certificateResetByMailCeaAttribute ✔
certificateResetByMailCertificateAttribute ✔

View File

@ -4,12 +4,11 @@ REST session backend
Session <type> can be 'global' for SSO sessions or 'persistent' for
persistent sessions.
LL::NG portal provides REST end points for sessions management:
LL::NG Portal provides REST end points for sessions management:
- GET /sessions/<type>/<session-id> : get session datas
- GET /sessions/<type>/<session-id>/<key> : get a session key value
- GET /sessions/<type>/<session-id>/[k1,k2] : get some session key
value
- GET /sessions/<type>/<session-id>/[k1,k2] : get some keys value
- POST /sessions/<type> : create a session
- PUT /sessions/<type>/<session-id> : update some keys
- DELETE /sessions/<type>/<session-id> : delete a session
@ -20,17 +19,21 @@ Sessions for connected users (used by :doc:`LLNG Proxy<authproxy>`):
- GET /session/my/<type>/key : get session key
- DELETE /session/my : ask for logout
Authorizations for connected users (always enabled):
Services for connected users (always enabled):
- GET /mysession/?authorizationfor=<base64-encoded-url>: ask if url is
authorizated
- GET /mysession/?authorizationfor=<base64-encoded-url> : ask if an url
is authorized
- GET /mysession/?whoami : get "my" uid
- PUT /mysession/<type> : update some persistent data (restricted)
- DELETE /mysession/<type>/key : delete key in data (restricted)
- GET /myapplications : get "my" appplications list
This session backend can be used to share sessions stored in a
non-network backend (like
:doc:`file session backend<filesessionbackend>`) or in a network backend
protected with a firewall that only accepts HTTP flows.
Most of the time, REST session backend is used by Handlers installed on
Most of the time, REST session backend is used by Handlers deployed on
external servers.
To configure it, REST session backend will be set through Manager in
@ -69,16 +72,16 @@ Name Comment Example
=================== ======================================== ==================================================
`user` and `password` parameters are only used if the entry point `index.fcgi/sessions/global`
is protected by a basic authentication. Thus, handlers will make requests to the portal
is protected by a basic authentication. Thus, handlers will make requests to the Portal
using these parameters.
.. attention::
By default, user password and other secret keys are
hidden by LLNG REST server. You can force REST server to export their
hidden by LL::NG REST server. You can force REST server to export their
real values by selecting "Export secret attributes in REST" in the
manager. This less secure option is disabled by default.
Manager. This less secure option is disabled by default.
Apache
~~~~~~

View File

@ -29,6 +29,57 @@ None
2.0.14
------
Security
~~~~~~~~
* **CVE-2021-40874**: RESTServer pwdConfirm always returns true with Combination + Kerberos (see `issue 2612 <https://gitlab.ow2.org/lemonldap-ng/lemonldap-ng/-/issues/2612>`__)
Weak encryption used for password-protected SAML keys
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Previous versions of LemonLDAP::NG used a weak encryption algorithm to protect
SAML keys when a password was set during certificate generation.
Run the following command to check if this is your case::
lemonldap-ng-cli get samlServicePrivateKeySig
lemonldap-ng-cli get samlServicePrivateKeyEnc
If the output of either command starts with ``BEGIN ENCRYPTED PRIVATE KEY``,
then it probably means you generated your keys using the vulnerable manager
code.
In this case, you can convert your existing keys to a stronger encryption using
the following command ::
# Extract your existing keys. If samlServicePrivateKeyEnc is empty, you can
# skip it entirely
lemonldap-ng-cli get samlServicePrivateKeySig | \
sed 's/samlServicePrivateKeySig = //' > saml-sig.pem
lemonldap-ng-cli get samlServicePrivateKeyEnc | \
sed 's/samlServicePrivateKeyEnc = //' > saml-enc.pem
# Re-encrypt the private key, using the same passphrase
openssl pkey -in saml-sig.pem -aes256 -out saml-sig-aes.pem
openssl pkey -in saml-enc.pem -aes256 -out saml-enc-aes.pem
#Or, if you are using OpenSSL 3+
openssl pkey -provider legacy -provider default -in saml-sig.pem \
-aes256 -out saml-sig-aes.pem
openssl pkey -provider legacy -provider default -in saml-enc.pem \
-aes256 -out saml-enc-aes.pem
Then, simply reimport your keys ::
lemonldap-ng-cli set samlServicePrivateKeySig "$(cat saml-sig-aes.pem)"
lemonldap-ng-cli set samlServicePrivateKeyEnc "$(cat saml-enc-aes.pem)"
If is recommended to keep the same password as before, if not, adjust the
``samlServicePrivateKeySigPwd`` and ``samlServicePrivateKeyEncPwd`` variables as well.
This operation is transparent and does not require any change to your existing
SAML configuration or SAML applications
LemonLDAP::NG version is returned by the CheckState plugin
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

View File

@ -29,6 +29,7 @@ sub defaultValues {
'captcha_size' => 6,
'casAccessControlPolicy' => 'none',
'casAuthnLevel' => 1,
'casTicketExpiration' => 0,
'certificateResetByMailCeaAttribute' => 'description',
'certificateResetByMailCertificateAttribute' =>
'userCertificate;binary',

View File

@ -815,6 +815,10 @@ qr/(?:(?:https?):\/\/(?:(?:(?:(?:(?:(?:[a-zA-Z0-9][-a-zA-Z0-9]*)?[a-zA-Z0-9])[.]
'default' => 0,
'type' => 'bool'
},
'casTicketExpiration' => {
'default' => 0,
'type' => 'int'
},
'cda' => {
'default' => 0,
'type' => 'bool'

View File

@ -2489,6 +2489,11 @@ sub attributes {
type => 'bool',
documentation => 'Disable host-based matching of CAS services',
},
casTicketExpiration => {
default => 0,
type => 'int',
documentation => 'Expiration time of Service and Proxy tickets',
},
issuerDBCASActivation => {
default => 0,
type => 'bool',

View File

@ -1393,6 +1393,7 @@ sub tree {
'casStorageOptions',
'casAttributes',
'casStrictMatching',
'casTicketExpiration',
]
},

View File

@ -212,7 +212,10 @@ sub _generateX509 {
my $strCert = Net::SSLeay::PEM_get_string_X509($cert);
my $strPrivate;
if ($password) {
$strPrivate = Net::SSLeay::PEM_get_string_PrivateKey( $key, $password );
my $alg = Net::SSLeay::EVP_get_cipherbyname("AES-256-CBC")
|| Net::SSLeay::EVP_get_cipherbyname("DES-EDE3-CBC");
$strPrivate =
Net::SSLeay::PEM_get_string_PrivateKey( $key, $password, $alg );
}
else {
$strPrivate = Net::SSLeay::PEM_get_string_PrivateKey($key);

View File

@ -44,10 +44,14 @@ sub tests {
# Check if portal URL is well formated
portalURL => sub {
my $url = $conf->{portal};
# Append or remove trailing slashes
$conf->{portal} =~ s%/*$%/%;
return (
1,
(
( $conf->{portal} =~ m%/$% )
( $url =~ m%/$% )
? ''
: "Portal URL should end with a /"
)

View File

@ -167,6 +167,7 @@
"casStorage":"اسم وحدة جلسات كاس",
"casStorageOptions":" خيارات وحدة جلسات كاس",
"casStrictMatching":"Use strict URL matching",
"casTicketExpiration":"Temporary ticket lifetime",
"categoryName":"اسم الفئة",
"cda":"نطاقات متعددة",
"certificateMailContent":"محتوى البريد",
@ -1230,4 +1231,4 @@
"yubikey2fUrl":"خدمة أل يو أر ل",
"yubikey2fUserCanRemoveKey":"Allow user to remove Yubikey",
"zeroConfExplanations":"لا يحتوي الخادم على إعدادات. استخدام قالب لحفظ الأول"
}
}

View File

@ -167,6 +167,7 @@
"casStorage":"CAS sessions module name",
"casStorageOptions":"CAS sessions module options",
"casStrictMatching":"Use strict URL matching",
"casTicketExpiration":"Temporary ticket lifetime",
"categoryName":"Category name",
"cda":"Mehrere Domains",
"certificateMailContent":"Mail content",
@ -1230,4 +1231,4 @@
"yubikey2fUrl":"Service URL",
"yubikey2fUserCanRemoveKey":"Allow user to remove Yubikey",
"zeroConfExplanations":"Server has no configuration. Use template to save the first."
}
}

View File

@ -167,6 +167,7 @@
"casStorage":"CAS sessions module name",
"casStorageOptions":"CAS sessions module options",
"casStrictMatching":"Use strict URL matching",
"casTicketExpiration":"Temporary ticket lifetime",
"categoryName":"Category name",
"cda":"Multiple domains",
"certificateMailContent":"Mail content",

View File

@ -167,6 +167,7 @@
"casStorage":"CAS sessions module name",
"casStorageOptions":"CAS sessions module options",
"casStrictMatching":"Use strict URL matching",
"casTicketExpiration":"Temporary ticket lifetime",
"categoryName":"Nombre de categoría",
"cda":"Dominios múltiples",
"certificateMailContent":"Contenido de correo",
@ -1230,4 +1231,4 @@
"yubikey2fUrl":"Service URL",
"yubikey2fUserCanRemoveKey":"Allow user to remove Yubikey",
"zeroConfExplanations":"Server has no configuration. Use template to save the first."
}
}

View File

@ -167,6 +167,7 @@
"casStorage":"Nom du module des sessions CAS",
"casStorageOptions":"Options du module des sessions CAS",
"casStrictMatching":"Filtrage strict des URL",
"casTicketExpiration":"Expiration des tickets temporaires",
"categoryName":"Nom de la catégorie",
"cda":"Domaines multiples",
"certificateMailContent":"Contenu du mail",

View File

@ -167,6 +167,7 @@
"casStorage":"Nome del modulo sessioni CAS",
"casStorageOptions":"Opzioni del modulo sessioni CAS",
"casStrictMatching":"Use strict URL matching",
"casTicketExpiration":"Temporary ticket lifetime",
"categoryName":"Nome della categoria",
"cda":"Domini multipli",
"certificateMailContent":"Contenuto della mail",
@ -1230,4 +1231,4 @@
"yubikey2fUrl":"URL del servizio",
"yubikey2fUserCanRemoveKey":"Autorizza l'utente a rimuovere la Yubikey",
"zeroConfExplanations":"Il server non ha alcuna configurazione. Utilizza il modello per salvare il primo."
}
}

View File

@ -167,6 +167,7 @@
"casStorage":"Nazwa modułu sesji CAS",
"casStorageOptions":"Opcje modułu sesji CAS",
"casStrictMatching":"Use strict URL matching",
"casTicketExpiration":"Temporary ticket lifetime",
"categoryName":"Nazwa Kategorii",
"cda":"Wiele domen",
"certificateMailContent":"Treść wiadomości",
@ -1230,4 +1231,4 @@
"yubikey2fUrl":"URL usługi",
"yubikey2fUserCanRemoveKey":"Pozwól użytkownikowi usunąć Yubikey",
"zeroConfExplanations":"Serwer nie ma konfiguracji. Użyj szablonu, aby zapisać pierwszy."
}
}

View File

@ -167,6 +167,7 @@
"casStorage":"CAS oturumları modül adı",
"casStorageOptions":"CAS oturumları modül seçenekleri",
"casStrictMatching":"Katı URL eşleşmesi kullan",
"casTicketExpiration":"Temporary ticket lifetime",
"categoryName":"Kategori ismi",
"cda":"Çoklu alan adları",
"certificateMailContent":"E-posta içeriği",
@ -1230,4 +1231,4 @@
"yubikey2fUrl":"Servis URL'si",
"yubikey2fUserCanRemoveKey":"Yubikey'i kaldırmak için kullanıcıya izin ver",
"zeroConfExplanations":"Sunucunun yapılandırması yok. Şimdi bir tane kaydetmek için şablonu kullanın."
}
}

View File

@ -167,6 +167,7 @@
"casStorage":"Tên mô-đun phiên CAS",
"casStorageOptions":"Các tùy chọn mô-đun phiên CAS",
"casStrictMatching":"Use strict URL matching",
"casTicketExpiration":"Temporary ticket lifetime",
"categoryName":"Tên thể loại",
"cda":"Nhiều tên miền",
"certificateMailContent":"Nội dung thư",
@ -1230,4 +1231,4 @@
"yubikey2fUrl":"Dịch vụ URL",
"yubikey2fUserCanRemoveKey":"Allow user to remove Yubikey",
"zeroConfExplanations":"Máy chủ không có cấu hình. Sử dụng mẫu để lưu đầu tiên. "
}
}

View File

@ -167,6 +167,7 @@
"casStorage":"CAS 会话模块名称",
"casStorageOptions":"CAS 会话模块选项",
"casStrictMatching":"Use strict URL matching",
"casTicketExpiration":"Temporary ticket lifetime",
"categoryName":"分类名称",
"cda":"Multiple domains",
"certificateMailContent":"Mail content",
@ -1230,4 +1231,4 @@
"yubikey2fUrl":"Service URL",
"yubikey2fUserCanRemoveKey":"Allow user to remove Yubikey",
"zeroConfExplanations":"Server has no configuration. Use template to save the first."
}
}

View File

@ -167,6 +167,7 @@
"casStorage":"CAS 工作階段模組名稱",
"casStorageOptions":"CAS 工作階段模組選項",
"casStrictMatching":"Use strict URL matching",
"casTicketExpiration":"Temporary ticket lifetime",
"categoryName":"分類名稱",
"cda":"多域名",
"certificateMailContent":"郵件內容",
@ -1230,4 +1231,4 @@
"yubikey2fUrl":"服務 URL",
"yubikey2fUserCanRemoveKey":"允許使用者移除 Yubikey",
"zeroConfExplanations":"伺服器未設定。使用飯本來儲存第一個。"
}
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -582,6 +582,8 @@ t/32-CAS-Gateway.t
t/32-CAS-Hooks.t
t/32-CAS-Macros.t
t/32-CAS-Prefix.t
t/32-CAS-Proxy.t
t/32-CAS-Security.t
t/32-OIDC-ClaimTypes.t
t/32-OIDC-ClientCredentials-Grant.t
t/32-OIDC-Code-Flow-with-2F-UpgradeOnly.t

View File

@ -163,9 +163,6 @@ sub run {
# Session ID
my $session_id = $req->{sessionInfo}->{_session_id} || $req->id;
# Session creation timestamp
my $time = $req->{sessionInfo}->{_utime} || time();
# 1. LOGIN
if ( $target eq $cas_login ) {
@ -306,12 +303,20 @@ sub run {
$self->logger->debug(
"Create a CAS service ticket for service $service");
my $_utime =
$self->conf->{casTicketExpiration}
? (
time +
$self->conf->{casTicketExpiration} -
$self->conf->{timeout} )
: ( $req->{sessionInfo}->{_utime} || time() );
my $Sinfos;
$Sinfos->{type} = 'casService';
$Sinfos->{service} = $service;
$Sinfos->{renew} = $casRenewFlag;
$Sinfos->{_cas_id} = $session_id;
$Sinfos->{_utime} = $time;
$Sinfos->{_utime} = $_utime;
$Sinfos->{_casApp} = $app;
my $h = $self->p->processHook( $req, 'casGenerateServiceTicket',
@ -516,6 +521,18 @@ sub validate {
return $self->returnCasValidateError();
}
# Make sure the token is still valid, we already compensated for
# different TTLs when storing _utime
if ( $casServiceSession->{data}->{_utime} ) {
if (
time >
( $casServiceSession->{data}->{_utime} + $self->conf->{timeout} ) )
{
$self->logger->error("Session $ticket has expired");
return $self->returnCasValidateError();
}
}
$self->logger->debug("Service ticket session $ticket found");
my $service1_uri = URI->new($service);
@ -637,11 +654,16 @@ sub proxy {
'Error in proxy session management' );
}
my $_utime =
$self->conf->{casTicketExpiration}
? ( time + $self->conf->{casTicketExpiration} - $self->conf->{timeout} )
: $casProxyGrantingSession->data->{_utime};
my $Pinfos;
$Pinfos->{type} = 'casProxy';
$Pinfos->{service} = $targetService;
$Pinfos->{_cas_id} = $casProxyGrantingSession->data->{_cas_id};
$Pinfos->{_utime} = $casProxyGrantingSession->data->{_utime};
$Pinfos->{_utime} = $_utime;
$Pinfos->{proxies} = $casProxyGrantingSession->data->{proxies};
$casProxySession->update($Pinfos);
@ -711,6 +733,20 @@ sub _validate2 {
return $self->returnCasServiceValidateError( $req, 'INVALID_TICKET',
'Ticket not found' );
}
# Make sure the token is still valid, we already compensated for
# different TTLs when storing _utime
if ( $casServiceSession->{data}->{_utime} ) {
if (
time >
( $casServiceSession->{data}->{_utime} + $self->conf->{timeout} ) )
{
$self->logger->error("$urlType ticket session $ticket has expired");
return $self->returnCasServiceValidateError( $req, 'INVALID_TICKET',
'Ticket expired' );
}
}
my $app = $casServiceSession->data->{_casApp};
$self->logger->debug("$urlType ticket session $ticket found");
@ -777,7 +813,7 @@ sub _validate2 {
$PGinfos->{type} = 'casProxyGranting';
$PGinfos->{service} = $service;
$PGinfos->{_cas_id} = $casServiceSession->data->{_cas_id};
$PGinfos->{_utime} = $casServiceSession->data->{_utime};
$PGinfos->{_utime} = time;
$PGinfos->{_casApp} = $app;
# Trace proxies

View File

@ -226,8 +226,10 @@ sub returnCasServiceValidateSuccess {
}
if ($proxies) {
$self->logger->debug("Add proxies $proxies in response");
$s .= "\t\t<cas:proxies>\n\t\t\t<cas:proxy>$_</cas:proxy>\n"
foreach ( split( /$self->conf->{multiValuesSeparator}/, $proxies ) );
$s .= "\t\t<cas:proxies>\n";
$s .= "\t\t\t<cas:proxy>$_</cas:proxy>\n"
foreach (
reverse( split( $self->conf->{multiValuesSeparator}, $proxies ) ) );
$s .= "\t\t</cas:proxies>\n";
}
$s .= "\t</cas:authenticationSuccess>\n</cas:serviceResponse>\n";

View File

@ -231,6 +231,10 @@ sub findUser {
# Validate LDAP connection before use
sub validateLdap {
my ($self) = @_;
local $SIG{'PIPE'} = sub {
$self->logger->info("Reconnecting to LDAP server due to broken socket");
};
unless ($self->ldap
and $self->ldap->root_dse( attrs => ['supportedLDAPVersion'] ) )
{

View File

@ -1887,6 +1887,9 @@ sub resolveArtifact {
$message = $soap_answer->content();
$self->logger->debug("Get message $message");
}
else {
$self->logger->error("Error while sending message: ".$soap_answer->status_line);
}
}
return $message;

View File

@ -375,13 +375,7 @@ sub display {
# 3 Authentication has been refused OR first access
else {
$skinfile = 'login';
my $login = $self->userId($req);
if ( $login eq 'anonymous' ) {
$login = '';
}
elsif ( $req->user ) {
$login = $req->{user};
}
my $login = $req->user;
%templateParams = (
MAIN_LOGO => $self->conf->{portalMainLogo},
LANGS => $self->conf->{showLanguages},

View File

@ -169,7 +169,7 @@ sub displayModules {
foreach my $module ( @{ $self->menuModules } ) {
$self->logger->debug("Check if $module->[0] has to be displayed");
if ( $module->[1]->( $req, $req->sessionInfo ) ) {
if ( $module->[1]->( $req, $req->userData ) ) {
my $moduleHash = { $module->[0] => 1 };
if ( $module->[0] eq 'Appslist' ) {
$moduleHash->{'APPSLIST_LOOP'} = $self->appslist($req);
@ -177,16 +177,16 @@ sub displayModules {
elsif ( $module->[0] eq 'LoginHistory' ) {
$moduleHash->{'SUCCESS_LOGIN'} =
$self->p->mkSessionArray( $req,
$req->{sessionInfo}->{_loginHistory}->{successLogin},
$req->{userData}->{_loginHistory}->{successLogin},
"", 0, 0 );
$moduleHash->{'FAILED_LOGIN'} =
$self->p->mkSessionArray( $req,
$req->{sessionInfo}->{_loginHistory}->{failedLogin},
$req->{userData}->{_loginHistory}->{failedLogin},
"", 0, 1 );
}
elsif ( $module->[0] eq 'OidcConsents' ) {
$moduleHash->{'OIDC_CONSENTS'} =
$self->p->mkOidcConsent( $req, $req->sessionInfo );
$self->p->mkOidcConsent( $req, $req->userData );
}
push @$displayModules, $moduleHash;
}
@ -413,7 +413,7 @@ sub _filterHash {
if ( my $sub = $self->p->spRules->{$p} ) {
eval {
delete $apphash->{$key}
unless ( $sub->( $req, $req->sessionInfo ) );
unless ( $sub->( $req, $req->userData ) );
};
if ($@) {
$self->logger->error("Partner rule $p returns: $@");
@ -438,7 +438,7 @@ sub _filterHash {
delete $apphash->{$key}
unless (
$self->p->HANDLER->grant(
$req, $req->sessionInfo, $appuri, $cond, $vhost
$req, $req->userData, $appuri, $cond, $vhost
)
);
next;

View File

@ -49,6 +49,8 @@
# (restricted)
# * DELETE /mysession/<type>/key : delete key in data
# (restricted)
# * GET /myapplications : get my appplications
# list
#
# There is no conflict with SOAP server, they can be used together
@ -65,7 +67,7 @@ use Lemonldap::NG::Portal::Main::Constants qw(
URIRE
);
our $VERSION = '2.0.12';
our $VERSION = '2.0.14';
extends qw(
Lemonldap::NG::Portal::Main::Plugin
@ -247,8 +249,11 @@ sub init {
->addAuthRoute(
mysession => { ':sessionType' => 'updateMySession' },
['PUT']
);
extends @parents if ($add);
)
->addAuthRoute( myapplications => 'myApplications', ['GET'] );
extends @parents if ($add);
$self->setTypes( $self->conf ) if ( $self->conf->{restSessionServer} );
return 1;
@ -764,6 +769,23 @@ sub getUser {
}
}
sub myApplications {
my ( $self, $req ) = @_;
my @appslist = map {
my @apps = map {
{
$_->{appname} => {
AppUri => $_->{appuri},
AppDesc => $_->{appdesc}
}
}
} @{ $_->{applications} };
{ Category => $_->{catname}, Applications => \@apps },
} @{ $self->p->menu->appslist($req) };
return $self->p->sendJSONresponse( $req, { result => 1, myapplications => \@appslist } );
}
sub _checkSecret {
my ( $self, $secret ) = @_;
my $isValid = 0;

View File

@ -2,6 +2,8 @@ use Test::More;
use strict;
use IO::String;
use MIME::Base64;
use URI;
use URI::QueryParam;
require 't/test-lib.pm';
@ -80,6 +82,14 @@ ok( $res->[2]->[0] =~ m%<span trspan="connect">Connect</span>%,
or print STDERR Dumper( $res->[2]->[0] );
count(3);
my ( $host, $uri, $query ) =
expectForm( $res, undef, undef, 'user', 'password' );
my $uri = URI->new;
$uri->query($query);
is( $uri->query_param("user"), 'jdoe',
"Login is pre-filled on second attemps" );
count(1);
# Try to authenticate with bad password
# -------------------------------------
ok(

View File

@ -29,7 +29,7 @@ my ( $host, $url, $query ) =
$mock->fake_module( 'Authen::Radius', check_pwd => sub { 0 } );
# Try to authenticate with bad password
$query =~ s/user=/user=dwho/;
$query =~ s/user=[^&]*/user=dwho/;
$query =~ s/password=/password=jdoe/;
ok(
$res = $client->_post(
@ -48,7 +48,7 @@ count(1);
$mock->fake_module( 'Authen::Radius', check_pwd => sub { 1 } );
# Try to authenticate
$query =~ s/user=/user=dwho/;
$query =~ s/user=[^&]*/user=dwho/;
$query =~ s/password=/password=dwho/;
ok(
$res = $client->_post(

View File

@ -63,7 +63,7 @@ foreach (@form) {
expectForm( [ $res->[0], $res->[1], [$_] ], undef, undef, 'test' );
}
$query =~ s/user=/user=dwho/;
$query =~ s/user=[^&]*/user=dwho/;
$query =~ s/password=/password=dwho/;
$query =~ s/test=\w*\b/test=1_demo/;

View File

@ -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);
}

View File

@ -0,0 +1,169 @@
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, $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;
# Ticket cannot be validated against wrong service
$ticket = casGetTicket( $issuer, $idpId, "http://auth.sp.com/" );
expectCasFail( casGetResponse( $issuer, $ticket, "http://auth.sp2.com/" ),
"INVALID_SERVICE" );
# Tickets are invalidated after success response
$ticket = casGetTicket( $issuer, $idpId, "http://auth.sp.com/" );
expectCasSuccess( casGetResponse( $issuer, $ticket, "http://auth.sp.com/" ) );
expectCasFail( casGetResponse( $issuer, $ticket, "http://auth.sp.com/" ) );
# Tickets are invalidated after failure response
$ticket = casGetTicket( $issuer, $idpId, "http://auth.sp.com/" );
expectCasFail( casGetResponse( $issuer, $ticket, "http://auth.sp2.com/" ),
"INVALID_SERVICE" );
expectCasFail( casGetResponse( $issuer, $ticket, "http://auth.sp.com/" ) );
# Ticket are no longer valid after TTL
$ticket = casGetTicket( $issuer, $idpId, "http://auth.sp.com/" );
Time::Fake->offset("+10m");
expectCasFail( casGetResponse( $issuer, $ticket, "http://auth.sp.com/" ) );
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://auth.sp.com/',
},
sp2 => {
casAppMetaDataOptionsService => 'http://auth.sp2.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://auth.sp.com/\?.*(ticket=[^&]+)# );
return $ticket;
}
sub casGetResponse {
my ( $issuer, $ticket, $service ) = @_;
ok(
my $res = $issuer->_get(
'/cas/p3/serviceValidate',
query => 'service=' . $service . '&' . $ticket,
accept => 'text/html'
),
'Query CAS server'
);
expectOK($res);
count(1);
return $res;
}
sub expectCasFail {
my ( $res, $code ) = @_;
$code ||= "INVALID_TICKET";
my $content = $res->[2]->[0];
like(
$content,
qr,authenticationFailure code="([^"]+)",,
"CAS response indicates success"
);
my ($response_code) = $content =~ qr,authenticationFailure code="([^"]+)",;
is( $response_code, $code, "Incorrect CAS error code" );
count(2);
}
sub expectCasSuccess {
my ($res) = @_;
my $content = $res->[2]->[0];
like( $content, qr,cas:authenticationSuccess,,
"CAS response indicates success" );
count(1);
}

View File

@ -9,7 +9,7 @@ BEGIN {
my ( $client, $res, $id );
$client = LLNG::Manager::Test->new(
{ ini => { logLevel => 'error', restSessionServer => 0, }, } );
{ ini => { logLevel => 'error', restSessionServer => 0 } } );
# Try to authenticate
# -------------------
@ -55,6 +55,38 @@ ok(
count(1);
expectOK($res);
# Test myapplications endpoint
ok(
$res = $client->_get(
'/myapplications', cookie => "lemonldap=$id"
),
'Request for my applications'
);
count(1);
expectOK($res);
$res = eval { JSON::from_json( $res->[2]->[0] ) };
if ($@) {
fail("Bad JSON response: $@");
count(1);
}
ok( $res->{result} == 1, ' Result == 1' );
count(1);
ok( $res->{myapplications}->[0]->{Category} eq 'Sample applications',
' "Sample applications" category found' );
ok( scalar @{ $res->{myapplications}->[0]->{Applications} } == 2,
' Two applications found' );
ok(
$res->{myapplications}->[0]->{Applications}->[0]->{'Application Test 1'}
->{AppDesc} eq 'A simple application displaying authenticated user',
' Description app1 found'
);
ok(
$res->{myapplications}->[0]->{Applications}->[1]->{'Application Test 2'}
->{AppUri} =~ m#http://test2\.example\.com/#,
' URI app2 found'
);
count(4);
# Test logout
$client->logout($id);

View File

@ -7,9 +7,7 @@ BEGIN {
require 't/test-lib.pm';
}
my $res;
my $id;
my $json;
my ($res, $id, $json);
my $client = LLNG::Manager::Test->new( {
ini => {

View File

@ -30,7 +30,7 @@ count(1);
my ( $host, $url, $query ) =
expectForm( $res, '#', undef, 'user', 'password', 'spoofId', 'token' );
$query =~ s/user=/user=rtyler/;
$query =~ s/user=[^&]*/user=rtyler/;
$query =~ s/password=/password=rtyler/;
$query =~ s/spoofId=/spoofId=dwho/;
@ -62,7 +62,7 @@ count(1);
( $host, $url, $query ) =
expectForm( $res, '#', undef, 'user', 'password', 'spoofId', 'token' );
$query =~ s/user=/user=rtyler/;
$query =~ s/user=[^&]*/user=rtyler/;
$query =~ s/password=/password=rtyler/;
$query =~ s/spoofId=/spoofId=msmith/;
@ -86,7 +86,7 @@ count(2);
( $host, $url, $query ) =
expectForm( $res, '#', undef, 'user', 'password', 'spoofId', 'token' );
$query =~ s/user=/user=dwho/;
$query =~ s/user=[^&]*/user=dwho/;
$query =~ s/password=/password=dwho/;
$query =~ s/spoofId=/spoofId=msmith/;