lemonldap-ng/modules/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/AuthLA.pm

1464 lines
46 KiB
Perl

#===============================================================================
# Liberty Alliance Authentication for LemonLDAP.
#
# This file is part of the LemonLDAP project and released under GPL.
#===============================================================================
package Lemonldap::NG::Portal::AuthLA;
use strict;
use warnings;
use Lemonldap::NG::Portal::SharedConf qw(:all);
use lasso;
use CGI qw/:standard/;
use CGI::Cookie;
use DBI;
use HTTP::Request;
use HTTP::Response;
use LWP::UserAgent;
use MIME::Base64;
use XML::Simple;
use XML::Parser;
use XML::XPath;
use UNIVERSAL qw( isa can VERSION );
*EXPORT_OK = *Lemonldap::NG::Portal::SharedConf::EXPORT_OK;
*EXPORT_TAGS = *Lemonldap::NG::Portal::SharedConf::EXPORT_TAGS;
*EXPORT = *Lemonldap::NG::Portal::SharedConf::EXPORT;
our $VERSION = '0.2';
our @ISA = qw(Lemonldap::NG::Portal::SharedConf);
#===============================================================================
# Global Constants
#===============================================================================
sub PE_LA_FAILED { 500 }
sub PE_LA_ARTFAILED { 501 }
sub PE_LA_DEFEDFAILED { 502 }
sub PE_LA_QUERYEMPTY { 503 }
sub PE_LA_SOAPFAILED { 504 }
sub PE_LA_SLOFAILED { 505 }
sub PE_LA_SSOFAILED { 506 }
sub PE_LA_SSOINITFAILED { 507 }
sub PE_LA_SESSIONERROR { 508 }
sub PE_LA_SEPFAILED { 509 }
sub PC_LA_URLAC { '/liberty/assertionConsumer.pl' }
sub PC_LA_URLFT { '/liberty/federationTermination.pl' }
sub PC_LA_URLFTR { '/liberty/federationTerminationReturn.pl' }
sub PC_LA_URLSL { '/liberty/singleLogout.pl' }
sub PC_LA_URLSLR { '/liberty/singleLogoutReturn.pl' }
sub PC_LA_URLSC { '/liberty/soapCall.pl' }
sub PC_LA_URLSE { '/liberty/soapEndpoint.pl' }
#===============================================================================
#===============================================================================
#
# TODO
# ------------------------------------------------------------------------------
# - category / function : comments
# ------------------------------------------------------------------------------
# - association / store : Replace files by hastable or DBI implementation
# - optimization / libertySignOn : Catching error when retrieving $providerID
# - optimization / _getAttributeValuesOfSamlAssertion : Checking errors
# - security / process : Check if URL figures in locationRules
# - security / libertyFederationTermination : Implementation
# - security / libertySoapEndpoint : Does Lemonldap::NG do defederation ?
# - wsf / setSessionInfo : Code for getting informations via wsf protocol
#
#===============================================================================
#===============================================================================
################################################################################
################################################################################
## ##
## Lemonldap::NG::Portal functions ##
## ##
################################################################################
################################################################################
#===============================================================================
# new
#===============================================================================
#
# Instanciate this class. This constructor takes special parameters with
# classical Lemonldap::NG::SharedConf parameters.
#
#===============================================================================
sub new {
my $class = shift;
my $this = $class->SUPER::new(@_);
$this->{isLibertyProcess} = 1;
$this->{laDebug} = 0 unless ( $this->{laDebug} );
die('No Liberty Alliance Service Provider data defined')
unless ( $this->{laSp} );
die('No Liberty Alliance Identity Provider file defined')
unless ( $this->{laIdpsFile} );
die('No laStorage configuration defined')
unless ( $this->{laStorage} );
die('No laLdapLoginAttribute configuration defined')
unless ( $this->{laLdapLoginAttribute} );
die('No localStorage configuration defined')
unless ( $this->{localStorage} and $this->{localStorageOptions} );
bless( $this, $class );
# Create LassoServer
$this->{laServer} = lasso::Server->new(
$this->{laSp}->{metadata},
$this->{laSp}->{privkey},
undef, #$this->{laSp}->{secretkey} ,
undef, #$this->{laSp}->{certificate} ,
);
$this->_loadXmlIdpFile();
return $this;
}
#===============================================================================
# authenticate
#===============================================================================
#
# User is authenticated automatically, no ldap authentication.
#
#===============================================================================
sub authenticate {
my $this = shift;
return $this->SUPER::authenticate()
unless ( $this->{isLibertyProcess} );
return PE_BADCREDENTIALS
unless ( defined $this->{user} );
return PE_OK;
}
#===============================================================================
# extractFormInfo
#===============================================================================
#
# This function is just override to do nothing.
# $this->{user} is already fixed in libertySetSessionInfo function.
#
#===============================================================================
sub extractFormInfo {
my $this = shift;
return $this->SUPER::extractFormInfo()
unless ( $this->{isLibertyProcess} );
return PE_OK;
}
#===============================================================================
# formateFilter
#===============================================================================
#
# By default, the user is searched in the LDAP server with its UID. Here,
# $this->{user} contains nameIdentifier of the user, which is already stored
# in LDAP directory.
#
#===============================================================================
sub formateFilter {
my $this = shift;
return $this->SUPER::formateFilter()
unless ( $this->{isLibertyProcess} );
$this->{filter} =
"(&(uid=" . $this->{user} . ")(objectClass=inetOrgPerson))";
return PE_OK;
}
#===============================================================================
# process
#===============================================================================
#
# Do portal Lemonldap::NG processing. Actions based on Lemonldap::NG structure
# and philosophy.
#
#===============================================================================
sub process {
my $this = shift;
$this->{error} = PE_OK;
# Trace param()
# my @params = $this->param() ;
# foreach( @params ) {
# $this->_debug("parameter : $_ = " . $this->param($_)) ;
# }
# while(my($k,$v) = each(%ENV)) {
# $this->_debug("env : $k = $v") ;
# }
#--------
# Nothing to do if user access to portal directly. We have to verify if
# user was redirected from a protected host.
#--------
my $url = $this->url();
my $urlr = $url . substr( $ENV{'SCRIPT_NAME'}, 1 );
if ( not $this->param('url')
and ( $url eq $this->{portal} or $urlr eq $this->{portal} ) )
{
# TODO Security tricks :
# - Check if URL figures in locationRules
$this->{error} = PE_DONE;
return $this->{error};
}
#--------
# Authentication process
#--------
my $urldir = $this->url( -absolute => 1 );
# assertionCustomer
if ( $urldir eq $this->PC_LA_URLAC ) {
$this->{error} = $this->_subProcess(
qw( libertyAssertionConsumer libertySetSessionInfo ));
$this->_debug( "Login user = '" . $this->{user} . "'" );
# federationTermination
}
elsif ( $urldir eq $this->PC_LA_URLFT ) {
$this->{error} = $this->_subProcess(
qw( libertyFederationTermination log autoRedirect ));
# federationTerminationReturn
}
elsif ( $urldir eq $this->PC_LA_URLFTR ) {
$this->{error} = $this->_subProcess(
qw( libertyFederationTerminationReturn log
autoRedirect )
);
# singleLogout : called when IDP request Logout.
}
elsif ( $urldir eq $this->PC_LA_URLSL ) {
$this->{error} = $this->_subProcess(
qw( libertyRetrieveExistingSession libertySingleLogout
libertyDeletingExistingSession )
);
$this->_debug( "Logout user = '" . $this->{'dn'} . "'" );
# OK : $this->{urldc} is fixed at the end of this process.
# singleLogoutReturn
}
elsif ( $urldir eq $this->PC_LA_URLSLR ) {
$this->{error} =
$this->_subProcess(qw( libertySingleLogoutReturn log autoRedirect ));
# soapCall
}
elsif ( $urldir eq $this->PC_LA_URLSC ) {
$this->{error} = $this->_subProcess(qw( libertySoapCall log ));
# soapEndpoint
}
elsif ( $urldir eq $this->PC_LA_URLSE ) {
$this->{error} =
$this->_subProcess(qw( libertySoapEndpoint log autoRedirect ));
# Direct access or simple access -> main
# WARNING : we permit authentication on service.
}
elsif ( not $this->param('user') and not $this->param('password') ) {
$this->{error} = $this->_subProcess(
qw( libertyRetrieveExistingSession
libertyExtractFormInfo libertySignOn log
autoRedirect )
);
# Not in liberty authentication process.
}
else {
$this->{isLibertyProcess} = 0;
}
# $this->_debug("ERROR = " . $this->{error} . "\n") ;
return 0
if ( $this->{error} );
# Liberty Process OK -> do Lemonldap::NG process.
my $err = $this->SUPER::process(@_);
$this->_subProcess(qw( log autoRedirect ))
if ( $this->{urldc} );
return $err;
}
#===============================================================================
# setSessionInfo
#===============================================================================
#
# Après une consommation d'assertion d'auth valide cette fonction est appelée
# pour initialiser les infos de session dans le cas où c'est le wsf qui est
# choisi pour récup les infos du user (sera par défaut en ldap).
#
# TODO :
# * Faire de cette fonction un override de setSessionInfo avec par défaut
# le comportement de l'ancienne version et si dans la conf recup
# attribut par wsf... recup en wsf2.0.
#
#===============================================================================
sub setSessionInfo {
my $this = shift;
# Si configuration fixée à WSF
# Alors
# Traitement de récupération des informations par WSF
# Sinon
# Traitement de récupération des informations en appelant la fonction
# SUPER::setSessionInfo.
# $this->{sessionInfo}->{dn} = "cn=tutu,ou=people,dc=example,dc=com" ;
# $this->{sessionInfo}->{cn} = "tutu" ;
# $this->{sessionInfo}->{mail} = "tutu@example;com" ;
# $this->{sessionInfo}->{uid} = "ttutu" ;
return $this->SUPER::setSessionInfo;
}
#===============================================================================
# store
#===============================================================================
#
# This function store existing association between userNameIdentifier from IDP
# and Apache session ID of Lemonldap::NG.
#
#===============================================================================
sub store {
my $this = shift;
my $err = $this->SUPER::store();
return $err if ( $err or not $this->{isLibertyProcess} );
return PE_APACHESESSIONERROR
unless ( defined $this->{laStorageOptions}->{Directory}
and defined $this->{id} );
my $dir = $this->{laStorageOptions}->{Directory};
$dir =~ s/(.*)\/?$/$1/;
# We have to store association.
# Create a file named by userNameIdentifier from IDP, single, which
# contains session ID from Apache.
if ( defined $this->{userNameIdentifier} ) {
#my %h;
#eval {
# tie %h, $this->{laStorage},
# substr($this->{userNameIdentifier},1),
# $this->{laStorageOptions};
#};
#if ( $@ ) {
# $this->_debug("$@\n");
# return PE_APACHESESSIONERROR;
#}
#$h{id} = $this->{id} ;
#$h{_utime} = time();
#untie %h;
my $file = $dir . '/' . $this->{userNameIdentifier};
$this->_debug("$file already exists : override association")
if ( -e $file );
open( MYFILE, '> ' . $file );
print MYFILE $this->{id};
close MYFILE;
# In other case, we considere that store action failed.
# So, we have to delete Apache session file.
}
else {
my $file = $dir . '/' . $this->{id};
unlink $file;
return PE_APACHESESSIONERROR;
}
return PE_OK;
}
#===============================================================================
#===============================================================================
################################################################################
################################################################################
## ##
## Some Data Access functions ##
## ##
################################################################################
################################################################################
#===============================================================================
# getIdpURLs
#===============================================================================
#
# Returns all IDP URLs
#
#===============================================================================
sub getIdpIDs {
my $this = shift;
my @tab = ();
if ( $this->{laIdps} ) {
push @tab, $_ foreach ( keys %{ $this->{laIdps} } );
}
return @tab;
}
#===============================================================================
# getProtectedURLs
#===============================================================================
#
# Returns all protected URLs
#
#===============================================================================
sub getProtectedURLs {
my $this = shift;
my @tab = ();
if ( $this->{locationRules} ) {
push @tab, $_ foreach ( keys %{ $this->{locationRules} } );
}
return @tab;
}
#===============================================================================
#===============================================================================
################################################################################
################################################################################
## ##
## Liberty Alliance functions ##
## ##
################################################################################
################################################################################
#===============================================================================
# libertyArtefactResolution
#===============================================================================
#
# This function do Liberty artefact resolution. Verification is already made
# if this function is called, normaly it is authorized.
#
#===============================================================================
sub libertyArtefactResolution {
my $this = shift;
my $lassoLogin = undef;
my $lassoHttpMethod =
( defined( $ENV{'REQUEST_METHOD'} ) and $ENV{'REQUEST_METHOD'} eq 'GET' )
? $lasso::HTTP_METHOD_REDIRECT
: $lasso::HTTP_METHOD_POST;
# Retrieve or create lassoLogin.
if ( $this->{laLogin} and defined( $this->{laLogin} ) ) {
$lassoLogin = $this->{laLogin};
}
else {
$lassoLogin = lasso::Login->new( $this->{laServer} );
}
# POST
if ( $lassoHttpMethod == $lasso::HTTP_METHOD_POST
and $this->param('LARES') )
{
my $formLares = $this->param('LARES');
if ( my $error = $lassoLogin->processAuthnResponseMsg($formLares) ) {
$this->_debug("lassoLogin->initRequest(...) : error = $error");
return PE_LA_ARTFAILED;
}
if ( my $error = $lassoLogin->acceptSso() ) {
$this->_debug("lassoLogin->acceptSso(...) : error = $error");
return PE_LA_SSOFAILED;
}
# GET : artefact is in QUERY_STRING param
}
elsif ( $lassoHttpMethod == $lasso::HTTP_METHOD_REDIRECT
and defined $ENV{'QUERY_STRING'} )
{
# NOTES :
# Documentation indicates that $formLareq is QUERY_STRING HTTP
# header. We should have
# $formLareq = $this->param('QUERY_STRING').
# But initRequest method on lassoLogin returns -502 error code
# (LASSO_PARAM_ERROR_INVALID_VALUE) when QUERY_STRING is like
# 'SAMLart=...&RelayState=...'. So, $formLareq is rebuild so
# that it only contains 'SAMLart=...'.
my $formLareq = $ENV{'QUERY_STRING'};
if ( $this->param('SAMLart') ) {
$formLareq = 'SAMLart=' . $this->param('SAMLart');
}
if ( my $error =
$lassoLogin->initRequest( $formLareq, $lassoHttpMethod ) )
{
$this->_debug(
"libertyArtefactResolution : lassoLogin->initRequest(...) : error = $error"
);
return PE_LA_ARTFAILED;
}
if ( my $error = $lassoLogin->buildRequestMsg() ) {
$this->_debug(
"libertyArtefactResolution : lassoLogin->buildRequestMsg(...) : error = $error"
);
return PE_LA_ARTFAILED;
}
# Check if SSO is OK
# Successed = $soapResponseMsg contains code 200.
my $soapResponseMsg =
$this->_soapRequest( $lassoLogin->{msgUrl}, $lassoLogin->{msgBody} );
if ( my $error = $lassoLogin->processResponseMsg($soapResponseMsg) ) {
$this->_debug(
"libertyArtefactResolution : lassoLogin->processResponseMsg(...) : error = $error"
);
return PE_LA_SOAPFAILED;
}
if ( my $error = $lassoLogin->acceptSso() ) {
$this->_debug(
"libertyArtefactResolution : lassoLogin->acceptSso(...) : error = $error"
);
return PE_LA_SSOFAILED;
}
}
else {
return PE_LA_SSOFAILED;
}
# Backup $lassoLogin object
$this->{laLogin} = $lassoLogin;
# Save RelayState.
if ( $this->param('RelayState') ) {
$this->{urldc} = $this->param('RelayState');
}
return PE_OK;
}
#===============================================================================
# libertyAssertionConsumption
#===============================================================================
#
# Realize assertion.
#
#===============================================================================
sub libertyAssertionConsumer {
my $this = shift;
$this->{laLogin} = lasso::Login->new( $this->{laServer} );
return PE_LA_SSOFAILED
unless ( $this->{laLogin}
and defined( $this->{laLogin} )
and defined( $this->param('SAMLart') ) );
return $this->libertyArtefactResolution(@_);
}
#===============================================================================
# libertyDeletingExistingSession
#===============================================================================
#
# Delete existing Apache session file and Apache session ID <-> nameIdentifier
# association file.
#
#===============================================================================
sub libertyDeletingExistingSession {
my $this = shift;
# Deleting local cache session shared by all Lemonldap::NG::Handler.
if ( $this->{datas} ) {
my $refLocalStorage = undef;
my $localStorage = $this->{localStorage};
my $localStorageOptions = {};
$localStorageOptions->{namespace} ||= "lemonldap";
$localStorageOptions->{default_expires_in} ||= 600;
eval "use $localStorage;";
die("Unable to load $localStorage: $@") if ($@);
eval '$refLocalStorage = new '
. $localStorage
. '($localStorageOptions);';
if ( defined $refLocalStorage ) {
$refLocalStorage->remove( ${ $this->{datas} }{_session_id} );
#$refLocalStorage->remove(substr($this->{userNameIdentifier},1)) ;
$refLocalStorage->purge();
$this->_debug("Deleting apache session succeed");
}
else {
$this->_debug("Deleting apache session failed");
}
}
# Deleting association file, which is created when asserting consumer,
# in store function.
if ( $this->{sessionInfo}->{'laNameIdentifier'}
and $this->{globalStorageOptions}->{Directory} )
{
my $dir = $this->{globalStorageOptions}->{Directory};
$dir =~ s/(.*)\/?$/$1/;
my $file = $dir . '/' . $this->{sessionInfo}->{'laNameIdentifier'};
if ( not unlink $file ) {
$this->_debug("Deleting liberty-apache association file failed");
}
else {
$this->_debug("Deleting liberty-apache association file succeed");
}
}
return PE_OK;
}
#===============================================================================
# libertyExtractFormInfo
#===============================================================================
#
# Verify that user has choose a IDP for authentication.
#
#===============================================================================
sub libertyExtractFormInfo {
my $this = shift;
# If only one IDP -> redirect automatically on this IDP
my $idp;
my @idps = keys %{ $this->{laIdps} };
if ( $#idps >= 0 && $this->param('idpChoice') ) {
$idp = $this->param('idpChoice');
}
return PE_FIRSTACCESS
unless $idp;
$this->{idp}->{id} = $idp;
return PE_OK;
}
#===============================================================================
# libertyFederationTermination
#===============================================================================
#
# Terminate federation.
#
# TO BE DONE.
#
#===============================================================================
sub libertyFederationTermination {
my $this = shift;
$this->_debug("Processing federation termination...");
my $query = $ENV{'QUERY_STRING'};
return PE_LA_QUERYEMPTY
unless $query;
if ( lasso::isLibertyQuery($query) ) {
$this->{lassoDefederation} =
lasso::Defederation->new( $this->{laServer} );
return PE_LA_DEFEDFAILED
unless ( $this->{lassoDefederation}
and defined( $this->{lassoDefederation} )
and $this->{lassoDefederation}->processNotificationMsg($query) );
$this->_debug("lassoDefederation->processNotificationMsg... OK");
# TODO :
# $this->fedTerm();
return PE_OK;
}
return PE_DONE;
}
#===============================================================================
# libertyFederationTerminationReturn
#===============================================================================
#
# Quand cet appel ce produit t'il?
#
# TO BE DONE.
#
#===============================================================================
sub libertyFederationTerminationReturn {
my $this = @_;
$this->_debug("The Return of the federation termination...");
$this->{urldc} = $this->{portal};
return PE_OK;
}
#===============================================================================
# libertyRetrieveExistingSession
#===============================================================================
#
# Try to restore session whithin userNameIdentifier.
#
#===============================================================================
sub libertyRetrieveExistingSession {
my $this = shift;
# To retrieve current Liberty session, there are two ways :
# - 1/ We have Lemonldap::NG cookie. Then we have lassoLoginDump ;
# - 2/ We have a query string that contains the userNameIdentifier.
# Then, we could retrieve apache session from cache files.
return PE_LA_SESSIONERROR
unless ( defined $this->{laStorageOptions}->{Directory} );
# TODO :
# Retrieve NameIdentifier by catching and parsing $ENV{'QUERY_STRING'}
# 2/
if ( $this->param('NameIdentifier') ) {
# Retrieve apache session id from userNameIdentifier.
# $id contains apache session id.
my $dir = $this->{laStorageOptions}->{Directory};
$dir =~ s/(.*)\/?$/$1/;
my $file = $dir . '/' . $this->param('NameIdentifier');
return PE_LA_SESSIONERROR
unless ( open( MYFILE, $file ) );
my $id = readline(*MYFILE);
chomp($id);
close(MYFILE);
# We can not rebuild factice cookie for Lemonldap::NG retrieving
# itself the session. So, we retrieve here directly the session.
# Lemonldap::NG::Simple code of controlExistingSession function.
# Trying to recover session from global session storage
my %h;
eval {
tie %h, $this->{globalStorage}, $id, $this->{globalStorageOptions};
};
if ( $@ or not tied(%h) ) {
# Session not available (expired ?)
print STDERR
"Session $id isn't yet available ($ENV{REMOTE_ADDR})\n";
return PE_OK;
}
$this->{id} = $id;
# A session has been find => calling &existingSession
my $r;
%{ $this->{datas} } = %h;
untie(%h);
if ( $this->{existingSession} ) {
$r = &{ $this->{existingSession} }( $this, $id, $this->{datas} );
}
else {
$r = $this->existingSession( $id, $this->{datas} );
}
# Save datas in sessionInfo.
while ( my ( $k, $v ) = each( %{ $this->{datas} } ) ) {
$this->{sessionInfo}->{$k} = $v;
}
$this->_debug("No existing liberty session found")
unless ( $r == PE_OK );
}
return PE_OK;
}
#===============================================================================
# libertySetSessionInfo
#===============================================================================
#
# This function store in session cache information retrieve from IDP. If
# ID-WSF option is specified, it also store ID-WSF-attributes.
# In all cases, it fixes username of user who is authenticated on IDP.
#
#===============================================================================
sub libertySetSessionInfo {
my $this = shift;
return PE_LA_FAILED
unless ( defined $this->{laLogin} );
my $lassoLogin = $this->{laLogin};
# Store identity in LDAP Directory, if identity not exists. Good
# opportunity to ask user some more informations.
return PE_LA_FAILED
unless (
defined $lassoLogin->{session}
# and defined $lassoLogin->{identity}
and $lassoLogin->{nameIdentifier}->{content}
);
# Here, we store liberty identity and session in Apache session.
# We just store informations in cache, then those are saved by
# store function. Saved nameIdentifier too.
# $this->{sessionInfo}->{laIdentityDump} = $lassoLogin->{identity}->dump() ;
$this->{sessionInfo}->{laSessionDump} = $lassoLogin->{session}->dump();
$this->{sessionInfo}->{laNameIdentifier} =
$lassoLogin->{nameIdentifier}->{content};
# Get username from assertion and restore it in param('user'). Be
# carefull, IDP does not return username but an id for user assertion.
# The Lemonldap::NG search consists to perform a search with a filter
# using nameIdentifier, instead of username.
$this->{userNameIdentifier} = $lassoLogin->{nameIdentifier}->{content};
$this->{user} = $this->{userNameIdentifier};
$this->{password} = 'none';
# Try to retrieve uid in SAML response form assertion statement.
# For the moment, uid have to be unique in LDAP directory.
my @uidValues =
$this->_getAttributeValuesOfSamlAssertion( $lassoLogin->{response},
$this->{laLdapLoginAttribute} );
$this->{user} = $uidValues[0]
if (@uidValues);
return PE_OK;
}
#===============================================================================
# libertySignOn
#===============================================================================
#
# Init SSO request. If successfull, $this->{urldc} contains Liberty IDP Url
# for redirection.
#
#===============================================================================
sub libertySignOn {
my $this = shift;
my $lassoLogin = lasso::Login->new( $this->{laServer} );
return PE_LA_FAILED
unless ( $lassoLogin and defined($lassoLogin) );
# TODO :
# Catching error when retrieving $providerID.
my $providerID = $this->{LAidps}->{ $this->{idp}->{id} }->{url};
if (
my $error = $lassoLogin->initAuthnRequest(
$providerID, $lasso::HTTP_METHOD_REDIRECT
)
)
{
$this->_debug("lassoLogin->initAuthnRequest(...) : error = $error");
return PE_LA_SSOINITFAILED;
}
# We do one time federation, IDP doe not have to store nameIdentifier.
$lassoLogin->{request}->{consent} = $lasso::LIB_CONSENT_OBTAINED;
$lassoLogin->{request}->{nameIdPolicy} =
$lasso::LIB_NAMEID_POLICY_TYPE_ONE_TIME;
#$lassoLogin->{request}->{nameIdPolicy} = $lasso::LIB_NAMEID_POLICY_TYPE_FEDERATED ;
$lassoLogin->{request}->{isPassive} = 0;
if ( $this->param('url') ) {
my $url = decode_base64( $this->param('url') );
chomp $url;
$lassoLogin->{request}->{relayState} = $url;
}
if ( my $error = $lassoLogin->buildAuthnRequestMsg() ) {
$this->_debug("lassoLogin->buildAuthnRequestMsg(..) : error = $error");
return PE_LA_SSOINITFAILED;
}
$this->{urldc} = $lassoLogin->{msgUrl};
return PE_OK;
}
#===============================================================================
# libertySingleLogout
#===============================================================================
#
# Two cases :
# * Portal or applications requiere singleLogout -> SP request ;
# * IDP requiere singleLogout -> IDP request with $ENV{'QUERY_STRING'}
# specified.
#
#===============================================================================
sub libertySingleLogout {
my $this = shift;
my $lassoLogout = lasso::Logout->new( $this->{laServer} );
return PE_LA_FAILED
unless ( $lassoLogout
and defined($lassoLogout)
and defined $ENV{'QUERY_STRING'} );
if ( lasso::isLibertyQuery( $ENV{'QUERY_STRING'} ) ) {
# We retrieve query string and verify it.
# If it is OK, we set lemonldap::ng logout parameter, so we can perform
# it in Lemonldap::NG normal process. Then, we remove our stored liberty
# association file.
$this->param( 'logout' => '1' );
if ( my $error =
$lassoLogout->processRequestMsg( $ENV{'QUERY_STRING'} ) )
{
$this->_debug(
"lassoLogout->processRequestMsg(...) : error = $error");
return PE_LA_SLOFAILED;
}
# my $lassoIdentity = lasso::Identity::newFromDump($this->{sessionInfo}->{laIdentityDump}) ;
# $lassoLogout->{identity} = $lassoIdentity ;
my $lassoSession =
lasso::Session::newFromDump( $this->{sessionInfo}->{laSessionDump} );
$lassoLogout->{session} = $lassoSession;
# Logout by soap call could failed with those two errors.
if ( my $error = $lassoLogout->validateRequest() ) {
if ( $error != $lasso::PROFILE_ERROR_SESSION_NOT_FOUND
and $error != $lasso::PROFILE_ERROR_IDENTITY_NOT_FOUND )
{
$this->_debug(
"lassoLogout->validateRequest(...) : error = $error");
return PE_LA_SLOFAILED;
}
}
if ( my $error = $lassoLogout->buildResponseMsg() ) {
$this->_debug(
"lassoLogout->buildResponseMsg(...) : error = $error");
return PE_LA_SLOFAILED;
}
# Confirm logout by soap request
if ( defined $lassoLogout->{msgBody} ) {
my $soapResponseMsg = $this->_soapRequest( $lassoLogout->{msgUrl},
$lassoLogout->{msgBody} );
}
}
# Fixes redirection.
$this->{urldc} = $lassoLogout->{msgUrl};
# If $this->{urldc} empty, then we try to use HTTP referer if it exists
$this->{urldc} = $ENV{'HTTP_REFERER'}
if ( not $this->{urldc} and $ENV{'HTTP_REFERER'} );
return PE_OK;
}
#===============================================================================
# libertySingleLogoutReturn
#===============================================================================
#
# SP provides singleLogout, it calls IDP which has done Liberty logout. Then IDP
# requests http://portal/liberty/singleLogoutReturn.
# Here, as there is no more liberty session on IDP, we suppress liberty session
# on Lemon.
#
# TODO : Modifier le redirect pour appeler le handler logout configuré dans la
# directive location (portal ou handler dédié, plus besoin du relaystate).
#
# TO BE DONE.
#
#===============================================================================
sub libertySingleLogoutReturn {
my $this = shift;
$this->{lassoLogout} = lasso::Logout->new( $this->{laServer} );
return PE_LA_SLOFAILED
unless ( $this->{lassoLogout} and defined( $this->{lassoLogout} ) );
$this->_debug("Processing single logout return...");
my %cookies = fetch CGI::Cookie;
# Test if Lemonldap::NG cookie is available
if ( $cookies{ $this->{cookieName} }
and my $id = $cookies{ $this->{cookieName} }->value )
{
$this->{session_nb} = $cookies{ $this->{cookieName} };
$this->_debug("Cookie: $this->{cookieName} found...");
$this->_debug("session number: $this->{session_nb}");
my $query = $ENV{'QUERY_STRING'};
return PE_LA_QUERYEMPTY
unless $query;
$this->_debug("Processing response message...");
return PE_LA_SLOFAILED
unless ( $this->{lassoLogout}->processResponseMsg($query) );
$this->_debug("lassoLogout->processResponseMsg... OK");
$this->delLibertySession();
$this->_debug("delete liberty session... OK");
my $formRelayState = $this->param('RelayState');
return PE_LA_SLOFAILED
unless ( $formRelayState and defined($formRelayState) );
$this->_debug("formRelayState: $formRelayState");
$this->{urldc} = $formRelayState . '/logout';
return PE_OK;
}
return PE_LA_SLOFAILED;
}
#===============================================================================
# libertySoapCall
#===============================================================================
#
# IDP request defederation by SOAP calls.
#
# TO BE DONE.
#
#===============================================================================
sub libertySoapCall {
my $this = shift;
$this->_debug("Soap call processing...");
my $contentType = $ENV{'CONTENT_TYPE'};
my $soapMsg = $ENV{'QUERY_STRING'};
return PE_LA_SOAPFAILED
unless ( $contentType and $soapMsg and $contentType eq 'text/xml' );
$this->_debug("contentType: $contentType");
$this->_debug("soapMsg: $soapMsg");
my $requestType = lasso::getRequestTypeFromSoapMsg($soapMsg);
# Logout request
return $this->libertySingleLogout
if ( $requestType eq $lasso::REQUEST_TYPE_LOGOUT );
# Defederation request
return $this->libertyFederationTermination
if ( $requestType eq $lasso::REQUEST_TYPE_DEFEDERATION );
return PE_DONE;
}
#===============================================================================
# libertySoapEndpoint
#===============================================================================
#
# Requests arrive in SOAP. We MUST traited them.
# Work as process function : call other functions.
#
#===============================================================================
sub libertySoapEndpoint {
my $this = shift;
$this->_debug("SoapEndpoint processing...");
my $soapRequest = $this->param('POSTDATA');
return PE_LA_SEPFAILED
unless $soapRequest;
my $soapRequestType = lasso::getRequestTypeFromSoapMsg($soapRequest);
$this->_debug("RequestType = $soapRequestType");
# Logout SOAP request
if ( $soapRequestType == $lasso::REQUEST_TYPE_LOGOUT ) {
$ENV{'QUERY_STRING'} = $soapRequest;
return $this->libertySingleLogout();
# Defederation SOAP request
}
elsif ( $soapRequestType == $lasso::REQUEST_TYPE_DEFEDERATION ) {
# TODO
# Lemonldap do defederation ?
}
return PE_OK;
}
#===============================================================================
#===============================================================================
################################################################################
################################################################################
## ##
## Private functions ##
## ##
################################################################################
################################################################################
#===============================================================================
# _getAttributeValuesOfSamlAssertion
#===============================================================================
#
# Retrieve attribute value from a SAMLP response specified in first parameter.
# Return a table of values corresponding to the attribute name specified in
# second parameter.
#
#===============================================================================
sub _getAttributeValuesOfSamlAssertion {
my $this = shift;
my $samlp = shift;
my $attributeName = shift;
my @tab = ();
# Search the specific attribute.
my $attribute = undef;
for (
my $i = 0 ;
not defined $attribute
and $i < lasso::NodeList::length( $samlp->{assertion} ) ;
$i++
)
{
my $assertion = lasso::NodeList::getItem( $samlp->{assertion}, $i );
my $attributeStatement = $assertion->{attributeStatement};
for (
my $j = 0 ;
not defined $attribute
and $j <
lasso::NodeList::length( $attributeStatement->{attribute} ) ;
$j++
)
{
my $attr =
lasso::NodeList::getItem( $attributeStatement->{attribute}, $j );
if ( $attr->{attributeName} eq $attributeName ) {
$attribute = $attr;
}
}
}
return @tab
unless ( defined $attribute );
# Then get values
# Values are only lasso::MiscTextNode type.
for (
my $k = 0 ;
$k < lasso::NodeList::length( $attribute->{attributeValue} ) ;
$k++
)
{
my $attributeValue =
lasso::NodeList::getItem( $attribute->{attributeValue}, $k );
my $valueList = $attributeValue->{any};
for ( my $l = 0 ; $l < lasso::NodeList::length($valueList) ; $l++ ) {
my $value = lasso::NodeList::getItem( $valueList, $l );
push @tab, $value->{content}
if ( isa( $value, "lasso::MiscTextNode" ) );
}
}
return @tab;
}
#===============================================================================
# _loadXMLIdpFile
#===============================================================================
#
# Load Identity Providers file.
# File is a XML file which contains providers definition.
# Only one service provider for multiple identity provider.
#
#===============================================================================
sub _loadXmlIdpFile {
my $this = shift;
my $file = shift;
my $xml = XML::Simple::XMLin( $this->{laIdpsFile}, ForceArray => ['idp'] );
$this->{laIdps} = $xml->{idp};
# Adding all IDPs in laIdpsFile in laServer.
# Have to be done one time -> no choice : in constructor.
foreach ( keys %{ $this->{laIdps} } ) {
my $hash = $this->{laIdps}->{$_};
$this->{laServer}->addProvider(
$lasso::PROVIDER_ROLE_IDP, $hash->{'metadata'},
$hash->{'pubkey'}, $hash->{'certificate'},
);
}
}
#===============================================================================
# _soapRequest
#===============================================================================
#
# Do soap request with only two parameters :
# - URI string ;
# - Body string ;
# Thus function return body's response message.
#
#===============================================================================
sub _soapRequest {
my $this = shift;
my ( $uri, $body ) = @_;
my $soapHeaders = new HTTP::Headers( Content_Type => "text/xml" );
my $soapRequest = new HTTP::Request( "POST", $uri, $soapHeaders, $body );
my $soapAgent = LWP::UserAgent->new( agent => 'Mozilla/5.0 [en]' );
my $soapResponse = $soapAgent->request($soapRequest);
return $soapResponse->content();
}
#===============================================================================
# _subProcess
#===============================================================================
#
# Externalise functions' execution.
#
#===============================================================================
sub _subProcess {
my $this = shift;
my @subs = @_;
my $err = undef;
foreach my $sub (@subs) {
if ( $this->{$sub} ) {
last if ( $err = &{ $this->{$sub} }($this) );
}
else {
last if ( $err = $this->$sub );
}
}
return $err;
}
#===============================================================================
# _debug
#===============================================================================
#
# Private tracing function.
#
#===============================================================================
sub _debug {
my $this = shift;
my $str = shift;
my (
$package, $filename, $line, $subroutine, $hasargs,
$wantarray, $evaltext, $is_require, $hints, $bitmask
) = caller(1);
print STDERR $subroutine . " : " . $str . "\n";
}
#===============================================================================
#===============================================================================
################################################################################
################################################################################
## ##
## Documentation ##
## ##
################################################################################
################################################################################
1;
__END__
=head1 NAME
Lemonldap::NG::Portal::AuthLA - Provide Liberty Alliance Authentication for
FederID project.
=head1 SYNOPSIS
use Lemonldap::NG::Portal::AuthLA;
my $portal = Lemonldap::NG::Portal::AuthLA->new({
configStorage => {
type => 'DBI',
dbiChain => "dbi:mysql:...",
dbiUser => "lemonldap",
dbiPassword => "password",
dbiTable => "lmConfig",
} ,
# Liberty Parameters
laSp => {
certificate => '/path/to/public/key.pem' ,
metadata => '/path/to/metadata.xml' ,
privkey => '/path/to/private/key.pem' ,
secretkey => '/path/to/private/key.pem' ,
} ,
laIdpsFile => '/path/to/idps/file.xml' ,
laStorage => 'Apache::Session::File',
laStorageOptions => {
Directory => '/path/to/session/directory' ,
LockDirectory => '/path/to/lockedsession/directory' ,
} ,
laDebug => 1 ,
laLdapLoginAttribute => 'uid' ,
# Parameters that permit to access lemonldap::NG::Handler local cache
localStorage => 'Cache::FileCache' ,
localStorageOptions => {} ,
});
if( $portal->process() ) {
# Print protected URLs
print $portal->header ;
print "<a href=\"http://$_\"> $_</a><br/>"
foreach ($portal->getProtectedURLs) ;
} else {
print $portal->header ;
print '...' ;
# Print simple template
print 'Simple Authentication<br/>' ;
print '<input type="hidden" name="url" value="' . $portal->param('url') . '"/>' ;
print 'Login :' ;
if ($portal->param('user')) {
print '<input type="hidden" name="user" value="' . $portal->param('user') . '"/>' ;
} else {
print '<input type="hidden" name="user"/>' ;
}
print 'Password : <input name="password" type="password" autocomplete="off">' ;
# Retrieve IDP list.
my @idps = () ;
foreach ($portal->getIdpIDs) {
my %row_data ;
$row_data{IDPNAME} = $_ ;
push (@idps, \%row_data) ;
}
@idps = sort {$a cmp $b} @idps ;
# Print SSO template
print 'SSO Authentication<br/>' ;
print '<select name="idpChoice"><option value="null">Select IDP</option>' ;
foreach (@idps) {
print '<option value="' . $_ . '">' . $_ . '</option>' ;
}
print '<input type="submit" value="ok" />' ;
print '</form>' ;
}
=head1 DESCRIPTION
Lemonldap::NG::Portal::AuthLA is the base module for building Lemonldap::NG
compatible portals using a authentication mechanism based on Liberty Alliance.
You have to use by inheritance.
=head1 SEE ALSO
L<Lemonldap::NG::Portal::SharedConf>, L<Lemonldap::NG::Portal>,
L<Lemonldap::NG::Handler>, L<Lemonldap::NG::Manager>,
http://wiki.lemonldap.objectweb.org/xwiki/bin/view/NG/Presentation
=head1 AUTHOR
Clement Oudot, E<lt>coudot@linagora.comE<gt>
Mikaël Ates, E<lt>mikael.ates@univ-st-etienne.frE<gt>
Thomas Chemineau, E<lt>tchemineau@linagora.comE<gt>
=head1 BUG REPORT
Use OW2 system to report bug or ask for features:
L<http://forge.objectweb.org/tracker/?group_id=274>
=head1 DOWNLOAD
Lemonldap::NG is available at
L<http://forge.objectweb.org/project/showfiles.php?group_id=274>
=head1 COPYRIGHT AND LICENSE
Copyright (C) 2007 by FederID Consortium, E<lt>mail@FederIDE<gt>
This library is free software; you can redistribute it and/or modify
it under the same terms as Perl itself, either Perl version 5.8.4 or,
at your option, any later version of Perl 5 you may have available.
=cut