package Lemonldap::NG::Portal::Lib::SAML;

use strict;
use Mouse;
use Lemonldap::NG::Common::Conf::SAML::Metadata;
use Lemonldap::NG::Common::Session;
use Lemonldap::NG::Common::UserAgent;
use Lemonldap::NG::Common::FormEncode;
use HTML::Entities qw(decode_entities encode_entities);
use XML::LibXML;
use MIME::Base64;
use HTTP::Request;         # SOAP call
use POSIX qw(strftime);    # Convert SAML2 date into timestamp
use Time::Local;           # Convert SAML2 date into timestamp
use Encode;                # Encode attribute values
use URI;                   # Get metadata URL path
use Lemonldap::NG::Portal::Main::Constants qw(
  PE_OK
  PE_REDIRECT
  PE_LOGOUT_OK
  PE_SLO_ERROR
);

with 'Lemonldap::NG::Portal::Lib::LazyLoadedConfiguration';

our $VERSION = '2.22.0';

# PROPERTIES

has lassoServer   => ( is => 'rw' );
has idpList       => ( is => 'rw', default => sub { {} } );
has idpRules      => ( is => 'rw', default => sub { {} } );
has idpAttributes => ( is => 'rw', default => sub { {} } );
has idpOptions    => ( is => 'rw', default => sub { {} } );
has spList        => ( is => 'rw', default => sub { {} } );
has spRules       => ( is => 'rw', default => sub { {} } );
has spLevelRules  => ( is => 'rw', default => sub { {} } );
has spMacros      => ( is => 'rw', default => sub { {} } );
has spAttributes  => ( is => 'rw', default => sub { {} } );
has spOptions     => ( is => 'rw', default => sub { {} } );

# XML parser
has parser => (
    is      => 'rw',
    builder => sub {
        return XML::LibXML->new( load_ext_dtd => 0, expand_entities => 0 );
    }
);

# return LWP::UserAgent object
has ua => (
    is      => 'rw',
    lazy    => 1,
    builder => sub {

        # TODO : LWP options to use a proxy for example
        my $ua = Lemonldap::NG::Common::UserAgent->new( $_[0]->{conf} );
        $ua->env_proxy();
        return $ua;
    }
);

# SAML storage
has aModule => ( is => 'rw' );
has amOpts  => ( is => 'rw' );

# INITIALIZATION

BEGIN {

    # Load Glib if available
    eval 'use Glib;';
    if ($@) {
        print STDERR
          "Glib Lasso messages will not be catched (require Glib module)\n";
        eval "use constant GLIB => 0";
    }
    else {
        eval "use constant GLIB => 1";
    }

    # Load Lasso.pm
    eval 'use Lasso;';
    if ($@) {
        print STDERR "Lasso.pm not loaded: $@";
        eval
'use constant LASSO => 0;use constant BADLASSO => 0;use constant LASSOTHINSESSIONS => 0';
    }
    else {
        no strict 'subs';
        eval 'use constant LASSO => 1';

        # Check Lasso version >= 2.3.0
        my $lasso_check_version_mode =
          eval 'Lasso::Constants::CHECK_VERSION_NUMERIC';
        my $check_version =
          Lasso::check_version( 2, 3, 0, $lasso_check_version_mode );
        unless ($check_version) {
            eval 'use constant BADLASSO => 1';
        }
        else {
            eval 'use constant BADLASSO => 0';
        }

        # Try to set thin-sessions flag
        eval 'Lasso::set_flag("thin-sessions");';
        if ($@) {
            eval 'use constant LASSOTHINSESSIONS => 0';
        }
        else {
            eval 'use constant LASSOTHINSESSIONS => 1';
        }
    }

}

sub init {
    my ($self) = @_;

    # Initialize SAML storage
    my $moduleOptions;
    if ( $self->conf->{samlStorage} ) {
        $moduleOptions = $self->conf->{samlStorageOptions} || {};
        $moduleOptions->{backend} = $self->conf->{samlStorage};
    }
    else {
        $moduleOptions = $self->conf->{globalStorageOptions} || {};
        $moduleOptions->{backend} = $self->conf->{globalStorage};
    }
    $self->aModule( $moduleOptions->{backend} );
    $self->amOpts($moduleOptions);

    # Check for Lasso errors/messages (see BEGIN)
    unless (LASSO) {
        $self->logger->error("Module Lasso not loaded (see below)");
        return 0;
    }

    if (BADLASSO) {
        $self->logger->error('Lasso version >= 2.3.0 required');
        return 0;
    }

    unless (LASSOTHINSESSIONS) {
        $self->logger->warn('Lasso thin-sessions flag could not be set');
    }
    else {
        $self->logger->debug('Lasso thin-sessions flag set');
    }
    if (GLIB) {
        Glib::Log->set_handler(
            "Lasso",
            [qw/ error critical warning message info debug /],
            sub {
                $self->logger->debug(
                    $_[0] . " error " . $_[1] . ": " . $_[2] );
            }
        );
    }

    # Conf initialization

    return 0 unless ( $self->lassoServer( $self->loadService ) );
    $self->addUnauthRoute(
        ( $self->{path} || 'saml' ) =>
          { 'metadata' => { ':type' => 'metadata' } },
        ['GET']
    );
    $self->addAuthRoute(
        ( $self->{path} || 'saml' ) =>
          { 'metadata' => { ':type' => 'metadata' } },
        ['GET']
    );
    return 1;
}

# METHODS

sub loadService {
    my ($self) = @_;

    # Check if certificate is available
    my $signing_key =
      $self->get_private_key( $self->_get_default_signing_key_name );
    unless ($signing_key) {
        $self->logger->error(
            'SAML private and public key not found in configuration');
        return 0;
    }

    my $serviceCertificate;
    if (    $self->conf->{samlServiceUseCertificateInResponse}
        and $signing_key->{public} =~ /CERTIFICATE/ )
    {
        $serviceCertificate = $signing_key->{public};
        $self->logger->debug('Certificate will be used in SAML responses');

    }

    # Get metadata from configuration
    $self->logger->debug("Get Metadata for this service");
    my $metadata = $self->_get_metadata_xml( undef, "all" );

    $self->logger->debug("Create Lasso server");

    my $encryption_key =
      $self->get_private_key( $self->_get_default_encryption_key_name );

    # Create Lasso server with service metadata
    my $server = $self->createServer(
        $metadata,
        $signing_key->{private},
        $signing_key->{password},

        # use signature cert for encryption unless defined
        (
            $encryption_key
            ? ( $encryption_key->{private}, $encryption_key->{password}, )
            : ( $signing_key->{private}, $signing_key->{password}, )
        ),
        $serviceCertificate
    );

    unless ($server) {
        $self->logger->error('Unable to create Lasso server');
        return 0;
    }

    # Signature method
    my $method = $self->getDefaultSignatureMethod();
    $server->signature_method( $self->getSignatureMethod($method) );
    $self->logger->debug("Set $method as SAML server signature method ");

    # Log
    $self->logger->debug("Service created");

    return $server;
}

sub loadIDPs {
    my ($self) = @_;

    # Load identity provider metadata
    # IDP metadata are listed in $self->{samlIDPMetaDataXML}
    # Each key is the IDP name
    # Build IDP list for later use in extractFormInfo
    foreach ( keys %{ $self->conf->{samlIDPMetaDataXML} } ) {
        $self->loadIDP($_);
    }
    return 1;
}

sub loadIDP {
    my ( $self, $idpConfKey ) = @_;

    $self->logger->debug("Get Metadata for IDP $idpConfKey");

    # If XML metadata has been set in conf, load this provider from conf
    my $idp_metadata =
      $self->conf->{samlIDPMetaDataXML}->{$idpConfKey}->{samlIDPMetaDataXML};
    if ($idp_metadata) {
        if ( $self->load_idp_from_conf( $idpConfKey, $idp_metadata ) ) {
            $self->logger->debug("IDP $idpConfKey added");
        }
        else {
            $self->logger->warn("Ignoring IDP $idpConfKey");
        }
    }
}

sub load_idp_from_conf {
    my ( $self, $idpConfKey, $idp_metadata ) = @_;

    # Check metadata format
    if ( ref $idp_metadata eq "HASH" ) {
        $self->logger->error(
"Metadata for IDP $idpConfKey is in old format. Please reload them from Manager"
        );
        return;
    }

    if ( $self->conf->{samlMetadataForceUTF8} ) {
        $idp_metadata = encode( "utf8", $idp_metadata );
    }

    # Store IDP entityID and Organization Name
    my ( $tmp, $entityID ) = ( $idp_metadata =~ /entityID=(['"])(.+?)\1/si );

    # Decode HTML entities from entityID
    # TODO: see Lasso comment above
    decode_entities($entityID);

    return $self->load_idp_metadata(
        $idpConfKey,
        $idp_metadata,
        $entityID,
        $self->conf->{samlIDPMetaDataExportedAttributes}->{$idpConfKey},
        $self->conf->{samlIDPMetaDataOptions}->{$idpConfKey},
    );
}

sub load_idp_metadata {
    my ( $self, $idpConfKey, $idp_metadata, $entityID, $attributes, $options )
      = @_;

    # Add this IDP to Lasso::Server
    my $result = $self->addIDP( $self->lassoServer, $idp_metadata );

    unless ($result) {
        $self->logger->error("Fail to use IDP $idpConfKey Metadata");
        return;
    }

    my $name = $self->getOrganizationName( $self->lassoServer, $entityID )
      || ucfirst($idpConfKey);
    $self->idpList->{$entityID}->{confKey} = $idpConfKey;
    $self->idpList->{$entityID}->{name}    = $name;

    # Set encryption mode
    my $encryption_mode =
      $options->{samlIDPMetaDataOptionsEncryptionMode} || "none";
    my $lasso_encryption_mode = $self->getEncryptionMode($encryption_mode);

    unless (
        $self->setProviderEncryptionMode(
            $self->lassoServer->get_provider($entityID),
            $lasso_encryption_mode
        )
      )
    {
        $self->logger->error(
            "Unable to set encryption mode $encryption_mode on IDP $idpConfKey"
        );
        delete $self->idpList->{$entityID};
        return;
    }
    $self->logger->debug(
        "Set encryption mode $encryption_mode on IDP $idpConfKey");

    # Set signature method if overridden
    my $signature_method = $options->{samlIDPMetaDataOptionsSignatureMethod};
    my $signature_key    = $options->{samlIDPMetaDataOptionsSignatureKey};
    $self->_override_signature_or_key( $entityID, "IDP $idpConfKey",
        $signature_method, $signature_key );

    # Set display options
    $self->idpList->{$entityID}->{displayName} =
      $options->{samlIDPMetaDataOptionsDisplayName};
    $self->idpList->{$entityID}->{icon} =
      $options->{samlIDPMetaDataOptionsIcon};
    $self->idpList->{$entityID}->{tooltip} =
      $options->{samlIDPMetaDataOptionsTooltip};
    $self->idpList->{$entityID}->{order} =
      $options->{samlIDPMetaDataOptionsSortNumber};

    # Set rule
    my $cond = $options->{samlIDPMetaDataOptionsResolutionRule};
    if ( length $cond ) {
        $cond = $self->p->HANDLER->substitute($cond);
        unless ( $cond = $self->p->HANDLER->buildSub($cond) ) {
            $self->logger->error( 'SAML IdP rule error: '
                  . $self->p->HANDLER->tsv->{jail}->error );
            delete $self->idpList->{$entityID};
            return;
        }
        $self->idpRules->{$entityID} = $cond;
    }

    $self->idpAttributes->{$entityID} = { %{ $attributes || {} } };
    $self->idpOptions->{$entityID}    = { %{ $options    || {} } };

    return 1;
}

sub loadSPs {
    my ($self) = @_;

    # Load service provider metadata
    # SP metadata are listed in $self->{samlSPMetaDataXML}
    # Each key is the SP name
    # Build SP list for later use in extractFormInfo
    foreach ( keys %{ $self->conf->{samlSPMetaDataXML} } ) {
        $self->loadSP($_);
    }
    return 1;
}

# This loads either a single SP or a federation
sub loadSP {
    my ( $self, $spConfKey ) = @_;

    $self->logger->debug("Get Metadata for SP $spConfKey");

    # If XML metadata has been set in conf, load this provider from conf
    my $sp_metadata =
      $self->conf->{samlSPMetaDataXML}->{$spConfKey}->{samlSPMetaDataXML};
    if ($sp_metadata) {
        $self->load_sp_from_conf( $spConfKey, $sp_metadata );
        $self->logger->debug("SP $spConfKey added");
    }
}

sub load_sp_from_conf {
    my ( $self, $spConfKey, $sp_metadata ) = @_;

    # Check metadata format
    if ( ref $sp_metadata eq "HASH" ) {
        $self->logger->error(
"Metadata for SP $spConfKey is in old format. Please reload them from Manager"
        );
        return;
    }

    if ( $self->conf->{samlMetadataForceUTF8} ) {
        $sp_metadata = encode( "utf8", $sp_metadata );
    }

    # Get SP entityID
    # TODO: when Lasso issue #35061 is fixed in all distros,
    # we could load the metadata into a new LassoProvider, extract the
    # parsed-and-decoded entityID from this Provider, and then add the
    # Provider into the server with $server->add_provider2, instead of
    # extracting and decoding HTML entities ourselves here
    my ( $tmp, $entityID ) = ( $sp_metadata =~ /entityID=(['"])(.+?)\1/si );
    decode_entities($entityID);

    return $self->load_sp_metadata(
        $spConfKey,
        $sp_metadata,
        $entityID,
        $self->conf->{samlSPMetaDataExportedAttributes}->{$spConfKey},
        $self->conf->{samlSPMetaDataOptions}->{$spConfKey},
        $self->conf->{samlSPMetaDataMacros}->{$spConfKey},
    );
}

sub load_sp_metadata {
    my ( $self, $spConfKey, $sp_metadata, $entityID, $attributes, $options,
        $macros )
      = @_;

    my $valid = 1;

    # Access Rule
    my $rule = $options->{samlSPMetaDataOptionsRule};
    if ( length $rule ) {
        $rule = $self->p->buildRule( $rule, "access rule for SP $spConfKey" );
        unless ($rule) {
            $valid = 0;
        }
    }

    # Required authentication level rule
    my $levelrule = $options->{samlSPMetaDataOptionsAuthnLevel} || 0;
    $levelrule = $self->p->buildRule( $levelrule,
        "required authentication level rule for SP $spConfKey" );
    unless ($levelrule) {
        $valid = 0;
    }

    # Load per-SP macros
    my $compiledMacros = {};
    for my $macroAttr ( keys %{$macros} ) {
        my $macroRule = $macros->{$macroAttr};
        if ( length $macroRule ) {
            $macroRule = $self->p->HANDLER->substitute($macroRule);
            if ( $macroRule = $self->p->HANDLER->buildSub($macroRule) ) {
                $compiledMacros->{$macroAttr} = $macroRule;
            }
            else {
                $valid = 0;
                $self->logger->error(
                    "Error processing macro $macroAttr for SAML SP $spConfKey"
                      . $self->p->HANDLER->tsv->{jail}->error );
            }
        }
    }

    if ($valid) {
        $self->spRules->{$spConfKey}      = $rule;
        $self->spLevelRules->{$spConfKey} = $levelrule;
        $self->spMacros->{$spConfKey}     = $compiledMacros;
    }
    else {
        $self->logger->error(
            "SAML SP $spConfKey has errors and will be ignored");
        return;
    }

    $self->spAttributes->{$entityID} = { %{ $attributes || {} } };

    $self->spOptions->{$entityID} = { %{ $options || {} } };

    # Add this SP to Lasso::Server
    my $result = $self->addSP( $self->lassoServer, $sp_metadata );

    unless ($result) {
        $self->logger->error("Fail to use SP $spConfKey Metadata");
        return;
    }

    # Store Org name
    my $name = $self->getOrganizationName( $self->lassoServer, $entityID )
      || ucfirst($spConfKey);
    $self->spList->{$entityID}->{confKey} = $spConfKey;
    $self->spList->{$entityID}->{name}    = $name;

    # Set encryption mode
    my $encryption_mode =
      $options->{samlSPMetaDataOptionsEncryptionMode} || "none";
    my $lasso_encryption_mode = $self->getEncryptionMode($encryption_mode);

    unless (
        $self->setProviderEncryptionMode(
            $self->lassoServer->get_provider($entityID),
            $lasso_encryption_mode
        )
      )
    {
        $self->logger->error(
            "Unable to set encryption mode $encryption_mode on SP $spConfKey");
        return;
    }
    $self->logger->debug(
        "Set encryption mode $encryption_mode on SP $spConfKey");

    # Set signature method if overridden
    my $signature_method = $options->{samlSPMetaDataOptionsSignatureMethod};
    my $signature_key    = $options->{samlSPMetaDataOptionsSignatureKey};
    $self->_override_signature_or_key( $entityID, "SP $spConfKey",
        $signature_method, $signature_key );
}

sub lazy_load_message {
    my ( $self, $message ) = @_;
    return unless $message;
    my $entityID = $self->getIssuer($message);
    if ($entityID) {
        $self->logger->debug("Found entityID $entityID in message");
        return $self->lazy_load_entityid($entityID);
    }
}

sub lazy_load_entityid {
    my ( $self, $entityID ) = @_;
    return unless $entityID;
    my $provider = $self->lassoServer->get_provider($entityID);
    unless ($provider) {
        $self->logger->debug("Lazy loading provider $entityID");

        return $self->lazy_load_config($entityID);
    }
}

sub load_config {
    my ( $self, $entityID ) = @_;

    my $config = {};
    my $res = $self->p->processHook( {}, 'getSamlConfig', $entityID, $config );
    if ( $res != PE_OK ) {
        return;
    }

    my $info;
    if ( $config->{sp_metadata} ) {
        $info->{sp_confKey} = $config->{sp_confKey};
        $self->load_sp_metadata(
            $config->{sp_confKey}, $config->{sp_metadata},
            $entityID,             $config->{sp_attributes},
            $config->{sp_options}, $config->{sp_macros},
        );

    }

    if ( $config->{idp_metadata} ) {
        $info->{idp_confKey} = $config->{idp_confKey};
        if (
            $self->load_idp_metadata(
                $config->{idp_confKey}, $config->{idp_metadata},
                $entityID,              $config->{idp_attributes},
                $config->{idp_options}
            )
          )
        {
            $self->logger->debug("IDP $config->{idp_confKey} added from hook");
        }
        else {
            $self->logger->warn("Ignoring IDP $config->{idp_confKey}");
        }
    }
    my $ttl = $config->{ttl};
    return { ( $ttl ? ( ttl => $ttl ) : () ),
        ( $info ? ( info => $info ) : () ) };
}

# This method returns the configuration info for the given provider
# Returns a hashref or undef

# Check SAML requests and responses
sub checkMessage {
    my ( $self, $req, $url, $request_method, $content_type, $profile_type ) =
      @_;
    $profile_type ||= "login";

    # Create Profile object
    my $profile;
    $profile = $self->createLogin( $self->lassoServer )
      if ( $profile_type eq "login" );
    $profile = $self->createLogout( $self->lassoServer )
      if ( $profile_type eq "logout" );

    # Get relayState
    my $relaystate = $req->param('RelayState');
    my ( $method, $request, $response, $artifact, $message );

    # 2.1. HTTP REDIRECT
    if ( $request_method eq 'GET' ) {

        $method = Lasso::Constants::HTTP_METHOD_REDIRECT;
        $self->logger->debug("SAML method: HTTP-REDIRECT");

        if ( $req->param('SAMLResponse') ) {

            # Response in query string
            $response = $self->getQueryString($req);
            $self->logger->debug("HTTP-REDIRECT: SAML Response $response");

        }

        if ( $req->param('SAMLRequest') ) {

            # Request in query string
            $request = $self->getQueryString($req);
            $self->logger->debug("HTTP-REDIRECT: SAML Request $request");

        }

        if ( $req->param('SAMLart') ) {

            # Artifact in query string
            $artifact = $self->getQueryString($req);
            $self->logger->debug("HTTP-REDIRECT: SAML Artifact $artifact");

            # Resolve Artifact
            $method  = Lasso::Constants::HTTP_METHOD_ARTIFACT_GET;
            $message = $self->resolveArtifact( $profile, $artifact, $method );

            # Request or response ?
            if ( $self->_isArtifactSamlResponse($message) ) {
                $response = $message;
            }
            else {
                $request = $message;
            }
        }

    }

    # 2.2. HTTP POST AND SOAP
    elsif ( $request_method =~ /^POST$/ ) {

        # 2.2.1. POST
        if ( $content_type !~ /xml/ ) {

            $method = Lasso::Constants::HTTP_METHOD_POST;
            $self->logger->debug("SAML method: HTTP-POST");

            if ( $req->param('SAMLResponse') ) {

                # Response in body part
                $response = $req->param('SAMLResponse');
                $response =~ s/ /+/gs;
                $self->logger->debug("HTTP-POST: SAML Response $response");

            }

            elsif ( $req->param('SAMLRequest') ) {

                # Request in body part
                $request = $req->param('SAMLRequest');
                $self->logger->debug("HTTP-POST: SAML Request $request");

            }

            elsif ( $req->param('SAMLart') ) {

                # Artifact in SAMLart param
                $artifact = $req->param('SAMLart');
                $self->logger->debug("HTTP-POST: SAML Artifact $artifact");

                # Resolve Artifact
                $method = Lasso::Constants::HTTP_METHOD_ARTIFACT_POST;
                $message =
                  $self->resolveArtifact( $profile, $artifact, $method );

                # Request or response ?
                if ( $self->_isArtifactSamlResponse($message) ) {
                    $response = $message;
                }
                else {
                    $request = $message;
                }

            }

        }

        # 2.2.2. SOAP
        else {

            $method = Lasso::Constants::HTTP_METHOD_SOAP;
            $self->logger->debug("SAML method: HTTP-SOAP");

            # SOAP is always a request
            $request = $req->content;
            $self->logger->debug("HTTP-SOAP: SAML Request $request");

        }

    }

    # 4. Return values
    return ( $request, $response, $method, $relaystate, $artifact ? 1 : 0 );
}

sub _isArtifactSamlResponse {
    my ( $self, $message ) = @_;

    my $type = eval {
        my $resp = Lasso::Samlp2ArtifactResponse->new;
        $resp->init_from_message($message);
        $resp->any->get_name;
    };

    if ($@) {
        $self->logger->warn("Could not detect type of Artifact response");
        return;
    }

    $self->logger->debug("Artifact response type is $type");
    if ( $type eq "Response" ) {
        return 1;
    }
    else {
        return 0;
    }
}

## @method boolean checkLassoError(Lasso::Error error, string level)
# Log Lasso error code and message if this is actually a Lasso::Error with code > 0
# @param error Lasso error object
# @param level optional log level (debug by default)
# @return 1 if no error
sub checkLassoError {
    my ( $self, $error, $level ) = @_;
    my ( $lasso_result, $lasso_error_code ) =
      $self->getLassoError( $error, $level );
    return $lasso_result;
}

## @method boolean getLassoError(Lasso::Error error, string level)
# Log Lasso error code and message if this is actually a Lasso::Error with code > 0
# @param error Lasso error object
# @param level optional log level (debug by default)
# @return (success status, lasso error code)
sub getLassoError {
    my ( $self, $error, $level ) = @_;
    $level ||= 'error';

    # If $error is not a Lasso::Error object, display error string
    unless ( ref($error) and $error->isa("Lasso::Error") ) {
        return ( 1, undef ) unless $error;
        $self->p->lmLog( "Lasso error: $error", $level );
        return ( 0, undef );
    }

    # Else check error code and error message
    if ( $error->{code} ) {
        $self->p->lmLog(
            "Lasso error code " . $error->{code} . ": " . $error->{message},
            $level );
        return ( 0, $error->{code} );
    }

    return ( 1, undef );
}

## @method Lasso::Server createServer(string metadata, string private_key, string private_key_password, string private_key_enc, string private_key_enc_password, string certificate)
# Load service metadata and create Lasso::Server object
# @param metadata SAML metadata
# @param private_key private key
# @param private_key_password optional private key password
# @param private_key_enc optional private key for encryption
# @param private_key_enc_password optional private key password for encryption
# @param certificate optional certificate
# @return Lasso::Server object
sub createServer {
    my ( $self, $metadata, $private_key, $private_key_password,
        $private_key_enc, $private_key_enc_password, $certificate )
      = @_;
    my $server;

    # https://dev.entrouvert.org/issues/51415
    my $save_env = $ENV{'SSL_CERT_FILE'};
    $ENV{'SSL_CERT_FILE'} = "/dev/null";

    eval {
        $server = Lasso::Server::new_from_buffers( $metadata, $private_key,
            $private_key_password, $certificate );

        # Set private key for encryption
        if ( $server && $private_key_enc ) {
            Lasso::Server::set_encryption_private_key_with_password( $server,
                $private_key_enc, $private_key_enc_password );
        }
    };

    if ( defined $save_env ) {
        $ENV{'SSL_CERT_FILE'} = $save_env;
    }
    else {
        delete $ENV{'SSL_CERT_FILE'};
    }

    if ($@) {
        $self->checkLassoError($@);
        return;
    }

    return $server;
}

## @method boolean addIDP(Lasso::Server server, string metadata, string public_key, string ca_cert_chain)
# Add IDP to an existing Lasso::Server
# @param server Lasso::Server object
# @param metadata IDP metadata
# @param public_key optional public key
# @param ca_cert_chain optional ca cert chain
# @return boolean result
sub addIDP {
    my ( $self, $server, $metadata, $public_key, $ca_cert_chain ) = @_;

    return 0 unless ( $server->isa("Lasso::Server") and defined $metadata );

    return $self->addProvider( $server, Lasso::Constants::PROVIDER_ROLE_IDP,
        $metadata, $public_key, $ca_cert_chain );
}

## @method boolean addSP(Lasso::Server server, string metadata, string public_key, string ca_cert_chain)
# Add SP to an existing Lasso::Server
# @param server Lasso::Server object
# @param metadata SP metadata
# @param public_key optional public key
# @param ca_cert_chain optional ca cert chain
# @return boolean result
sub addSP {
    my ( $self, $server, $metadata, $public_key, $ca_cert_chain ) = @_;

    return 0 unless ( $server->isa("Lasso::Server") and defined $metadata );

    return $self->addProvider( $server, Lasso::Constants::PROVIDER_ROLE_SP,
        $metadata, $public_key, $ca_cert_chain );
}

## @method boolean addAA(Lasso::Server server, string metadata, string public_key, string ca_cert_chain)
# Add Attribute Authority to an existing Lasso::Server
# @param server Lasso::Server object
# @param metadata AA metadata
# @param public_key optional public key
# @param ca_cert_chain optional ca cert chain
# @return boolean result
sub addAA {
    my ( $self, $server, $metadata, $public_key, $ca_cert_chain ) = @_;

    return 0 unless ( $server->isa("Lasso::Server") and defined $metadata );

    return $self->addProvider( $server,
        Lasso::Constants::PROVIDER_ROLE_ATTRIBUTE_AUTHORITY,
        $metadata, $public_key, $ca_cert_chain );
}

## @method boolean addProvider(Lasso::Server server, int role, string metadata, string public_key, string ca_cert_chain)
# Add provider to an existing Lasso::Server
# @param server Lasso::Server object
# @param role (IDP, SP or Both)
# @param metadata IDP metadata
# @param public_key optional public key
# @param ca_cert_chain optional ca cert chain
# @return boolean result
sub addProvider {
    my ( $self, $server, $role, $metadata, $public_key, $ca_cert_chain ) = @_;

    return 0
      unless ( $server->isa("Lasso::Server")
        and defined $role
        and defined $metadata );

    # https://dev.entrouvert.org/issues/51415
    my $save_env = $ENV{'SSL_CERT_FILE'};
    $ENV{'SSL_CERT_FILE'} = "/dev/null";

    eval {
        Lasso::Server::add_provider_from_buffer( $server, $role, $metadata,
            $public_key, $ca_cert_chain );
    };

    if ( defined $save_env ) {
        $ENV{'SSL_CERT_FILE'} = $save_env;
    }
    else {
        delete $ENV{'SSL_CERT_FILE'};
    }

    return $self->checkLassoError($@);
}

## @method string getOrganizationName(Lasso::Server server, string idp)
# Return name of organization picked up from metadata
#@param server Lasso::Server object
#@param idp entity ID
#@return string organization name
sub getOrganizationName {
    my ( $self, $server, $idp ) = @_;
    my ( $provider, $node );

    # Get provider from server
    eval { $provider = Lasso::Server::get_provider( $server, $idp ); };

    if ($@) {
        $self->checkLassoError($@);
        return;
    }

    # Get organization node
    eval { $node = Lasso::Provider::get_organization($provider); };

    if ($@) {
        $self->checkLassoError($@);
        return;
    }

    # Return if node is empty
    return unless $node;

    # Extract organization name
    my $data = eval { $self->parser->parse_string($node)->documentElement };
    if ( my $exception = $@ ) {
        $self->logger->warn("Could not parse Organization for $idp: $@");
    }
    return unless $data;
    return $data->getElementsByTagNameNS(
        "urn:oasis:names:tc:SAML:2.0:metadata",
        'OrganizationName' )->string_value;
}

## @method string getNextProviderId(Lasso::Logout logout)
# Returns the provider id from providerID_index in list of providerIDs in
# principal session with the exception of initial service provider ID.
# @param logout Lasso::Logout object
# @return string
sub getNextProviderId {
    my $self   = shift;
    my $logout = shift;
    my $providerId;

    eval { $providerId = Lasso::Logout::get_next_providerID($logout); };
    if ($@) {
        $self->checkLassoError($@);
        return;
    }

    return $providerId;
}

## @method boolean resetProviderIdIndex(Lasso::Logout logout)
# Reset the providerID_index attribute in Lasso::Logout object
# @param logout Lasso::Logout object
# @return boolean
sub resetProviderIdIndex {
    my $self   = shift;
    my $logout = shift;

    eval { Lasso::Logout::reset_providerID_index($logout); };
    return $self->checkLassoError($@);
}

## @method Lasso::Login createAuthnRequest(Lasso::Server server, string idp, int method, boolean forceAuthn, boolean isPassive, string nameIDFormat, boolean allowProxiedAuthn, boolean signSSOMessage, string requestedAuthnContext)
# Create authentication request for selected IDP
# @param server Lasso::Server object
# @param idp IDP entityID
# @param method HTTP method
# @param forceAuthn force authentication on IDP
# @param isPassive require passive authentication
# @param nameIDFormat SAML2 NameIDFormat
# @param allowProxiedAuthn allow proxy on IDP // Not used anymore but kept to avoid API break
# @param signSSOMessage sign request
# @param requestedAuthnContext authentication context
# @return Lasso::Login object
sub createAuthnRequest {
    my (
        $self,           $req,          $server,
        $idp,            $method,       $forceAuthn,
        $isPassive,      $nameIDFormat, $allowProxiedAuthn,
        $signSSOMessage, $requestedAuthnContext
    ) = @_;
    my $proxyCount;
    my $proxyRequestedAuthnContext;

    # Create Lasso Login
    my $login = $self->createLogin($server);

    unless ($login) {
        $self->logger->error('Unable to create Lasso login');
        return;
    }

    # Init authentication request
    unless ( $self->initAuthnRequest( $login, $idp, $method ) ) {
        $self->logger->error(
            "Could not initiate authentication request on $idp");
        return;
    }

    # Set RelayState
    if ( my $relaystate = $self->storeRelayState( $req, 'urldc' ) ) {
        $login->msg_relayState($relaystate);
        $self->logger->debug("Set $relaystate in RelayState");
    }

    # Customize request
    my $request = $login->request();

    # Maybe we are in IDP proxy mode (SAML request received on IDP side)
    # In this case:
    # * Check proxy conditions
    # * Forward some authn constraints
    if ( $req->data->{_proxiedSamlRequest} ) {

        $self->logger->debug("IDP Proxy mode detected");

        # Get ProxyCount value
        eval {
            $proxyCount =
              $req->data->{_proxiedSamlRequest}->Scoping()->ProxyCount();
        };

        # Deny request if ProxyCount eq 0
        if ( defined $proxyCount ) {

            $self->logger->debug(
                "Found proxyCount $proxyCount in proxied request");

            if ( $proxyCount eq 0 ) {
                $self->userLogger->error(
                    "SAML request cannot be proxied (ProxyCount 0)");
                return;
            }
            else {

                # Decrease ProxyCount
                my $scoping = $req->data->{_proxiedSamlRequest}->Scoping();
                $scoping->ProxyCount( $proxyCount-- );
                eval { $request->Scoping($scoping); };
            }
        }

        # isPassive
        eval { $isPassive = $req->data->{_proxiedSamlRequest}->IsPassive(); };

        # forceAuthn
        eval { $forceAuthn = $req->data->{_proxiedSamlRequest}->ForceAuthn(); };

        # requestedAuthnContext
        eval {
            $proxyRequestedAuthnContext =
              $req->data->{_proxiedSamlRequest}->RequestedAuthnContext();
        };
    }

    # NameIDFormat
    if ($nameIDFormat) {
        $self->logger->debug("Use NameIDFormat $nameIDFormat");
        $request->NameIDPolicy()->Format($nameIDFormat);
    }

    # Always allow NameID creation
    $request->NameIDPolicy()->AllowCreate(1);

    # Force authentication
    if ($forceAuthn) {
        $self->logger->debug("Force authentication on IDP");
        $request->ForceAuthn(1);
    }

    # Passive authentication
    if ($isPassive) {
        $self->logger->debug("Passive authentication on IDP");
        $request->IsPassive(1);
    }

    # Signature
    if ( $signSSOMessage == 0 ) {
        $self->logger->debug("SSO request will not be signed");
        $self->disableSignature($login);
    }
    elsif ( $signSSOMessage == 1 ) {
        $self->logger->debug("SSO request will be signed");
        $self->forceSignature($login);
    }
    else {
        $self->logger->debug("SSO request signature according to metadata");
    }

    # Requested authentication context
    if ($proxyRequestedAuthnContext) {
        $self->logger->debug("Use RequestedAuthnContext from proxied request");
        $request->RequestedAuthnContext($proxyRequestedAuthnContext);
    }
    elsif ($requestedAuthnContext) {
        $self->logger->debug("Request $requestedAuthnContext context");
        eval {
            my $context = Lasso::Samlp2RequestedAuthnContext->new();
            $context->AuthnContextClassRef($requestedAuthnContext);
            $context->Comparison("minimum");
            $request->RequestedAuthnContext($context);
        };
        if ($@) {
            $self->checkLassoError($@);
            return;
        }
    }

    # Call samlGenerateAuthnRequest hook
    my $h =
      $self->p->processHook( $req, 'samlGenerateAuthnRequest', $idp, $login );
    return if ( $h != PE_OK );

    # Build authentication request
    unless ( $self->buildAuthnRequestMsg($login) ) {
        $self->logger->error("Could not build authentication request on $idp");
        return;
    }

    # Artifact
    if (   $method == $self->getHttpMethod("artifact-get")
        or $method == $self->getHttpMethod("artifact-post") )
    {

        # Get artifact ID and Content, and store them
        my $artifact_id      = $login->get_artifact;
        my $artifact_message = $login->get_artifact_message;

        $self->storeArtifact( $artifact_id, $artifact_message );
    }
    return $login;
}

## @method Lasso::Login createLogin(Lasso::Server server, string dump)
# Create Lasso::Login object
# @param server Lasso::Server object
# @param dump optional XML dump
# @return Lasso::Login object
sub createLogin {
    my ( $self, $server, $dump ) = @_;
    my $login;

    if ($dump) {
        eval { $login = Lasso::Login::new_from_dump( $server, $dump ); };
    }
    else {
        eval { $login = Lasso::Login->new($server); };
    }

    if ($@) {
        $self->checkLassoError($@);
        return;
    }

    return $login;
}

## @method boolean initAuthnRequest(Lasso::Login login, string idp, int method)
# Init authentication request
# @param login Lasso::Login
# @param idp entityID
# @param method HTTP method
# @return boolean result
sub initAuthnRequest {
    my ( $self, $login, $idp, $method ) = @_;

    eval { Lasso::Login::init_authn_request( $login, $idp, $method ); };

    return $self->checkLassoError($@);
}

## @method boolean initIdpInitiatedAuthnRequest(Lasso::Login login, string idp)
# Init authentication request
# @param login Lasso::Login
# @param idp entityID
# @return boolean result
sub initIdpInitiatedAuthnRequest {
    my ( $self, $login, $idp ) = @_;

    eval { Lasso::Login::init_idp_initiated_authn_request( $login, $idp ); };

    return $self->checkLassoError($@);
}

## @method boolean buildAuthnRequestMsg(Lasso::Login login)
# Build authentication request message
# @param login Lasso::Login
# @return boolean result
sub buildAuthnRequestMsg {
    my ( $self, $login ) = @_;

    eval { Lasso::Login::build_authn_request_msg($login); };

    return $self->checkLassoError($@);
}

## @method boolean processAuthnRequestMsg(Lasso::Login login, string request)
# Process authentication request message
# processAuthnRequestMsgWithError is preferred, but this function is kept for
# API compatibility in 2.0
# @param login Lasso::Login object
# @param request SAML request
# @return result
sub processAuthnRequestMsg {
    my ( $self, $login, $request, $level ) = @_;

    $self->lazy_load_message($request);

    eval { Lasso::Login::process_authn_request_msg( $login, $request ); };

    return $self->checkLassoError( $@, $level );
}

## @method boolean processAuthnRequestMsgWithError(Lasso::Login login, string request)
# Process authentication request message
# @param login Lasso::Login object
# @param request SAML request
# @return result, Lasso error code
sub processAuthnRequestMsgWithError {
    my ( $self, $login, $request, $level ) = @_;

    $self->lazy_load_message($request);

    eval { Lasso::Login::process_authn_request_msg( $login, $request ); };

    return $self->getLassoError( $@, $level );
}

## @method boolean validateRequestMsg(Lasso::Login login, boolean auth, boolean consent)
# Validate request message
# @param login Lasso::Login object
# @param auth is user authenticated?
# @param consent is consent obtained?
# @return result
sub validateRequestMsg {
    my ( $self, $login, $auth, $consent ) = @_;

    eval { Lasso::Login::validate_request_msg( $login, $auth, $consent ); };

    return $self->checkLassoError($@);
}

## @method boolean buildAuthnResponseMsg(Lasso::Login login)
# Build authentication response message
# @param login Lasso::Login object
# @return boolean result
sub buildAuthnResponseMsg {
    my ( $self, $login ) = @_;

    eval { Lasso::Login::build_authn_response_msg($login); };

    return $self->checkLassoError($@);
}

## @method boolean buildArtifactMsg(Lasso::Login login, int method)
# Build artifact message
# @param login Lasso::Login object
# @param method HTTP method
# @return boolean result
sub buildArtifactMsg {
    my ( $self, $login, $method ) = @_;

    eval { Lasso::Login::build_artifact_msg( $login, $method ); };

    return $self->checkLassoError($@);
}

## @method boolean buildAssertion(Lasso::Login login, string authn_context, int notOnOrAfterTimeout)
# Build assertion
# @param login Lasso::Login object
# @param authn_context SAML2 authentication context
# @param notOnOrAfterTimeout Timeout to apply to notOnOrAfter
# @return boolean result
sub buildAssertion {
    my ( $self, $req, $login, $authn_context, $notOnOrAfterTimeout ) = @_;
    $notOnOrAfterTimeout ||= $self->conf->{timeout};

    # Dates
    my $time                    = $req->sessionInfo->{_utime} || time();
    my $timeout                 = $time + $notOnOrAfterTimeout;
    my $authenticationInstant   = $self->timestamp2samldate($time);
    my $reauthenticateOnOrAfter = $self->timestamp2samldate($timeout);
    my $issued_time             = time;
    my $notBefore               = $self->timestamp2samldate($issued_time);
    my $notOnOrAfter =
      $self->timestamp2samldate( $issued_time + $notOnOrAfterTimeout );

    eval {
        Lasso::Login::build_assertion( $login, $authn_context,
            $authenticationInstant, $reauthenticateOnOrAfter, $notBefore,
            $notOnOrAfter );
    };

    return $self->checkLassoError($@);
}

## @method boolean processAuthnResponseMsg(Lasso::Login login, string response)
# Process authentication response message
# @param login Lasso::Login object
# @param response SAML response
# @return result
sub processAuthnResponseMsg {
    my ( $self, $login, $response ) = @_;

    $self->lazy_load_message($response);

    eval { Lasso::Login::process_authn_response_msg( $login, $response ); };

    return $self->checkLassoError($@);
}

# UNUSED
### @method Lasso::Saml2NameID getNameIdentifer(Lasso::Profile profile)
## Get NameID from Lasso Profile
## @param profile Lasso::Profile object
## @return result or NULL if error
#sub getNameIdentifier {
#    my ( $self, $profile ) = @_;
#    my $nameid;
#
#    eval { $nameid = Lasso::Profile::get_nameIdentifier($profile); };
#
#    if ($@) {
#        $self->checkLassoError($@);
#        return;
#    }
#
#    return $nameid;
#}

# UNUSED
### @method Lasso::Identity createIdentity(string dump)
## Create Lasso::Identity object
## @param dump optional Identity dump
## @return Lasso::Identity object
#sub createIdentity {
#    my ( $self, $dump ) = @_;
#    my $identity;
#
#    if ($dump) {
#        eval { $identity = Lasso::Identity::new_from_dump($dump); };
#    }
#    else {
#        eval { $identity = Lasso::Identity->new(); };
#    }
#
#    if ($@) {
#        $self->checkLassoError($@);
#        return;
#    }
#
#    return $identity;
#}

## UNUSED
### @method Lasso::Session createSession(string dump)
## Create Lasso::Session object
## @param dump optional Session dump
## @return Lasso::Session object
#sub createSession {
#    my ( $self, $dump ) = @_;
#    my $session;
#
#    if ($dump) {
#        eval { $session = Lasso::Session::new_from_dump($dump); };
#    }
#    else {
#        eval { $session = Lasso::Session->new(); };
#    }
#
#    if ($@) {
#        $self->checkLassoError($@);
#        return;
#    }
#
#    return $session;
#}

## @method boolean acceptSSO(Lasso::Login login)
# Accept SSO from IDP
# @param login Lasso::Login object
# @return result
sub acceptSSO {
    my ( $self, $login ) = @_;

    eval { Lasso::Login::accept_sso($login); };

    return $self->checkLassoError($@);
}

## @method string getIssuer(string msg)
# Get issuer from message
# @param msg SAML message
# @return issuer
sub getIssuer {
    my ( $self, $message ) = @_;

    # Work around lasso bug (https://dev.entrouvert.org/issues/97575)
    $message =~ s/&RelayState=$//g;
    my $issuer = eval { Lasso::profile_get_issuer($message) };
    if ($@) {
        $self->logger->debug("lasso_profile_get_issuer: $@");
    }
    if ( !$issuer ) {
        $self->logger->debug(
                "lasso_profile_get_issuer could not find entityID"
              . " in incoming message" );
        return;
    }
    return $issuer;
}

## @method string storeRelayState(hashref infos)
# Store information in relayState database and return
# corresponding session_id
# @param infos HASH reference of information
sub storeRelayState {
    my ( $self, $req, @data ) = @_;

    # check if there are data to store
    my $infos;
    foreach (@data) {
        $infos->{$_} = $req->{$_} if $req->{$_};
    }
    return unless ($infos);

    # Session type
    $infos->{_type} = "relaystate";

    # Set _utime for session autoremove
    # Use default session timeout and relayState session timeout to compute it
    my $time    = time();
    my $timeout = $self->conf->{timeout};
    my $samlRelayStateTimeout =
      $self->conf->{samlRelayStateTimeout} || $timeout;

    $infos->{_utime} = $time + ( $samlRelayStateTimeout - $timeout );

    # Create relaystate session and store infos in relaystate session
    my $samlSessionInfo = $self->getSamlSession( undef, $infos )
      or return undef;

    # Session ID
    my $relaystate_id = $samlSessionInfo->id;

    # Return session ID
    return $relaystate_id;
}

## @method boolean extractRelayState(string relaystate, boolean relayStateURL)
# Extract RelayState information into $self
# @param relaystate Relay state value
# @param relayStateURL Allow Relay state value to be the redirection URL
# @return result
sub extractRelayState {
    my ( $self, $req, $relaystate, $relayStateURL ) = @_;

    return 0 unless $relaystate;
    if ( $relayStateURL and $relaystate =~ /^https?:\/\// ) {
        $self->logger->debug("RelayState is a redirection URL: $relaystate");
        $req->{urldc} = $relaystate;
        return 1;
    }
    else {

        # Open relaystate session
        my $samlSessionInfo = $self->getSamlSession($relaystate);

        return 0 unless $samlSessionInfo;

        # Push values in $self
        foreach ( keys %{ $samlSessionInfo->data } ) {
            next if $_ =~ /(type|_session_id|_utime|issuerNextUrl)/;
            $req->{$_} = $samlSessionInfo->data->{$_};
        }

        # issuerNextUrl has priority over urldc if both are set
        if ( $samlSessionInfo->data->{issuerNextUrl} ) {
            $req->urldc( $samlSessionInfo->data->{issuerNextUrl} );
        }

        # delete relaystate session
        if ( $samlSessionInfo->remove ) {
            $self->logger->debug("Relaystate $relaystate was deleted");
        }
        else {
            $self->logger->error("Unable to delete relaystate $relaystate");
            $self->logger->error( $samlSessionInfo->error );
        }

    }

    return 1;
}

## @method Lasso::Node getAssertion(Lasso::Login login)
# Get assertion in Lasso::Login object
# @param login Lasso::Login object
# @return assertion Lasso::Node object
sub getAssertion {
    my ( $self, $login ) = @_;
    my $assertion;

    eval { $assertion = Lasso::Login::get_assertion($login); };

    if ($@) {
        $self->checkLassoError($@);
        return;
    }

    return $assertion;
}

## @method string getAttributeValue(string name, string format, string friendly_name, arrayref attributes, boolean force_utf8)
# Get SAML attribute value corresponding to name, format and friendly_name
# Multivaluated values are separated by multiValuesSeparator
# If force_utf8 flag is set, value is encoded in UTF-8
# @param name SAML attribute name
# @param format optional SAML attribute format
# @param friendly_name optional SAML attribute friendly name
# @param attributes Attributes
# @param force_utf8 optional flag to force value in UTF-8
# @return attribute value
sub getAttributeValue {
    my ( $self, $name, $format, $friendly_name, $attributes, $force_utf8 ) = @_;
    my $value;

    # Loop on attributes
    foreach (@$attributes) {
        my $attr_name   = $_->Name();
        my $attr_format = $_->NameFormat();
        my $attr_fname  = $_->FriendlyName();

        # Skip if name does not correspond to attribute name
        next if ( $name ne $attr_name );

        # Verify format and friendly name if given
        next if ( $format        and $format ne $attr_format );
        next if ( $friendly_name and $friendly_name ne $attr_fname );

        # Attribute is found, return its content
        my @attr_values = $_->AttributeValue();

        $value = join(
            $self->{conf}->{multiValuesSeparator},
            map {
                eval { $_->any->content }
                  || ()
            } @attr_values
        );
    }

    return $value;
}

## @method boolean validateConditions(Lasso::Saml2::Assertion assertion, string entityID, boolean checkTime, boolean checkAudience)
# Validate conditions
# @param assertion SAML2 assertion
# @param entityID relying party entity ID
# @param checkTime Enable time conditions check
# @param checkAudience Enable audience conditions check
# @return result
sub validateConditions {
    my ( $self, $assertion, $entityID, $checkTime, $checkAudience ) = @_;
    my $tolerance = 10;
    my $status;
    $checkTime     = 1 unless defined $checkTime;
    $checkAudience = 1 unless defined $checkAudience;

    # Time
    if ($checkTime) {
        eval {
            $status = Lasso::Saml2Assertion::validate_time_checks( $assertion,
                $tolerance );
        };

        if ($@) {
            $self->checkLassoError($@);
            return 0;
        }

        unless ( $status eq Lasso::Constants::SAML2_ASSERTION_VALID ) {
            $self->logger->error("Time conditions validation failed ($status)");
            return 0;
        }

        $self->logger->debug("Time conditions validated");
    }
    else {
        $self->logger->debug("Time conditions not checked");
    }

    # Audience
    if ($checkAudience) {
        eval {
            $status =
              Lasso::Saml2Assertion::validate_audience( $assertion, $entityID );
        };

        if ($@) {
            $self->checkLassoError($@);
            return 0;
        }

        unless ( $status eq Lasso::Constants::SAML2_ASSERTION_VALID ) {
            $self->logger->error(
                "Audience conditions validations result: $status");
            return 0;
        }

        $self->logger->debug("Audience conditions validated");
    }
    else {
        $self->logger->debug("Audience conditions not checked");
    }

    return 1;
}

## @method Lasso::Logout createLogoutRequest(Lasso::Server server, string session_dump, int method, boolean signSLOMessage)
# Create logout request for selected entity
# @param server Lasso::Server object
# @param session_dump Lasso::Session dump
# @param method HTTP method
# @param signSLOMessage sign request
# @return Lasso::Login object
sub createLogoutRequest {
    my ( $self, $req, $server, $session_dump, $method, $signSLOMessage ) = @_;
    my $session;

    # Create Lasso Logout
    my $logout = $self->createLogout($server);

    unless ( $self->setSessionFromDump( $logout, $session_dump ) ) {
        $self->logger->error("Could not fill Lasso::Logout with session dump");
        return;
    }

    # Init logout request
    unless ( $self->initLogoutRequest( $logout, undef, $method ) ) {
        $self->logger->error("Could not initiate logout request");
        return;
    }

    # Set RelayState
    if ( my $relaystate =
        $self->storeRelayState( $req, 'urldc', 'issuerNextUrl' ) )
    {
        $logout->msg_relayState($relaystate);
        $self->logger->debug("Set $relaystate in RelayState");
    }

    # Signature
    if ( $signSLOMessage == 0 ) {
        $self->logger->debug("SLO request will not be signed");
        $self->disableSignature($logout);
    }
    elsif ( $signSLOMessage == 1 ) {
        $self->logger->debug("SLO request will be signed");
        $self->forceSignature($logout);
    }
    else {
        $self->logger->debug("SLO request signature according to metadata");
    }

    # Build logout request
    unless ( $self->buildLogoutRequestMsg($logout) ) {
        $self->logger->error("Could not build logout request");
        return;
    }

    return $logout;

}

## @method Lasso::Logout createLogout(Lasso::Server server, string dump)
# Create Lasso::Logout object
# @param server Lasso::Server object
# @param dump optional XML dump
# @return Lasso::Logout object
sub createLogout {
    my ( $self, $server, $dump ) = @_;
    my $logout;

    if ($dump) {
        eval { $logout = Lasso::Logout::new_from_dump( $server, $dump ); };
    }
    else {
        eval { $logout = Lasso::Logout->new($server); };
    }

    if ($@) {
        $self->checkLassoError($@);
        return;
    }

    return $logout;
}

## @method boolean initLogoutRequest(Lasso::Logout logout, string entityID, int method)
# Init logout request
# @param logout Lasso::Logout object
# @param entityID Entity ID
# @param method HTTP method
# @return result
sub initLogoutRequest {
    my ( $self, $logout, $entityID, $method ) = @_;

    eval { Lasso::Logout::init_request( $logout, $entityID, $method ); };

    return $self->checkLassoError($@);
}

## @method boolean buildLogoutRequestMsg(Lasso::Logout logout)
# Build logout request message
# @param logout Lasso::Logout object
# @return result
sub buildLogoutRequestMsg {
    my ( $self, $logout ) = @_;

    eval { Lasso::Logout::build_request_msg($logout); };

    return $self->checkLassoError($@);
}

## @method boolean setSessionFromDump(Lasso::Profile profile, string dump)
# Set session from dump in Lasso::Profile object
# @param profile Lasso::Profile object
# @param dump Lasso::Session XML dump
# @return result
sub setSessionFromDump {
    my ( $self, $profile, $dump ) = @_;

    $self->logger->debug("Loading Session dump: $dump") if $dump;

    eval { Lasso::Profile::set_session_from_dump( $profile, $dump ); };

    return $self->checkLassoError($@);
}

## @method boolean setIdentityFromDump(Lasso::Profile profile, string dump)
# Set identity from dump in Lasso::Profile object
# @param profile Lasso::Profile object
# @param dump Lasso::Identity XML dump
# @return result
sub setIdentityFromDump {
    my ( $self, $profile, $dump ) = @_;

    eval { Lasso::Profile::set_identity_from_dump( $profile, $dump ); };

    return $self->checkLassoError($@);
}

## @method string getMetaDataURL(string key, int index)
# Get URL stored in a service metadata configuration key
# Replace #PORTAL# macro
# @param key Metadata configuration key
# @param index field index containing URL
# @param full Return full URL instead of path
# @return url
sub getMetaDataURL {
    my ( $self, $key, $index, $full ) = @_;
    $index = 3 unless defined $index;
    $full  = 0 unless defined $full;

    return '' unless defined $self->conf->{$key};

    my $url = ( split( /;/, $self->conf->{$key} ) )[$index] || '';

    # Get default portal value
    my $portal = $self->p->buildUrl;
    $portal =~ s/\/$//;

    # Replace #PORTAL# macro
    $url =~ s/#PORTAL#/$portal/g;

    # Return Full URL
    return $url if $full;

    # Return only path
    my $uri = URI->new($url);
    return $uri->path();
}

sub getRouteFromMetaDataURL {
    my ( $self, $key, $index, $sub ) = @_;
    my $uri = $self->getMetaDataURL( $key, $index, 0 );
    unless ( $uri =~ m#^/\w# ) {
        $self->logger->debug("$key has no index $index");
        return ();
    }
    my @t = grep /\w/, split( /\//, $uri );
    my $h = { pop(@t) => $sub };
    while ( my $s = pop @t ) {
        $h = { $s => $h };
    }
    return %$h;
}

sub addRouteFromMetaDataURL {
    my ( $self, @args ) = @_;
    $self->addAuthRouteFromMetaDataURL(@args);
    $self->addUnauthRouteFromMetaDataURL(@args);
}

sub addAuthRouteFromMetaDataURL {
    my ( $self, $key, $index, $sub, $methods ) = @_;
    my %route = $self->getRouteFromMetaDataURL( $key, $index, $sub );
    return unless (%route);
    $self->addAuthRoute( %route, $methods );
}

sub addUnauthRouteFromMetaDataURL {
    my ( $self, $key, $index, $sub, $methods ) = @_;
    my %route = $self->getRouteFromMetaDataURL( $key, $index, $sub );
    return unless (%route);
    $self->addUnauthRoute( %route, $methods );
}

## @method boolean processLogoutResponseMsg(Lasso::Logout logout, string response)
# Process logout response message
# @param logout Lasso::Logout object
# @param response SAML response
# @return result
sub processLogoutResponseMsg {
    my ( $self, $logout, $response ) = @_;

    $self->lazy_load_message($response);

    eval { Lasso::Logout::process_response_msg( $logout, $response ); };

    return $self->checkLassoError($@);
}

## @method boolean processLogoutRequestMsg(Lasso::Logout logout, string request)
# Process logout request message
# @param logout Lasso::Logout object
# @param request SAML request
# @return result
sub processLogoutRequestMsg {
    my ( $self, $logout, $request ) = @_;

    $self->lazy_load_message($request);

    # Process the request
    eval { Lasso::Logout::process_request_msg( $logout, $request ); };

    return 0 unless $self->checkLassoError($@);

    # Check NotOnOrAfter optional attribute
    my $notOnOrAfter;

    eval { $notOnOrAfter = $logout->request()->NotOnOrAfter(); };

    return 1 if ( $@ or !$notOnOrAfter );

    $self->logger->debug("Found NotOnOrAfter $notOnOrAfter in logout request");

    my $expirationTime = $self->samldate2timestamp($notOnOrAfter);

    return ( time < $expirationTime );
}

## @method boolean validateLogoutRequest(Lasso::Logout logout)
# Validate logout request
# @param logout Lasso::Logout object
# @return result
sub validateLogoutRequest {
    my ( $self, $logout ) = @_;

    eval { Lasso::Logout::validate_request($logout); };

    return $self->checkLassoError($@);
}

## @method boolean buildLogoutResponseMsg(Lasso::Logout logout)
# Build logout response message
# @param logout Lasso::Logout object
# @return boolean result
sub buildLogoutResponseMsg {
    my ( $self, $logout ) = @_;

    eval { Lasso::Logout::build_response_msg($logout); };

    return $self->checkLassoError($@);
}

## @method boolean storeReplayProtection(string samlID, string samlData)
# Store ID of an SAML message in Replay Protection base
# @param samlID ID of SAML message
# @param samlData Optional data to store
# @return result
sub storeReplayProtection {
    my ( $self, $samlID, $samlData ) = @_;
    my $infos = {
        type       => 'assertion',    # Session type
        _utime     => time(),         # Creation time
        _assert_id => $samlID,
    };

    if ( defined $samlData && $samlData ) {
        $infos->{data} = $samlData;
    }

    my $samlSessionInfo = $self->getSamlSession( undef, $infos );
    return 0 unless $samlSessionInfo;

    my $session_id = $samlSessionInfo->id;

    $self->logger->debug(
        "Keep request ID $samlID in assertion session $session_id");

    return 1;
}

## @method boolean replayProtection(string samlID)
# Check if SAML message do not correspond to a previously responded message
# @param samlID ID of initial SAML message
# @return result
sub replayProtection {
    my ( $self, $samlID ) = @_;

    unless ($samlID) {
        $self->userLogger->error(
            "Cannot verify replay because no SAML ID given");
        return 0;
    }

    my $sessions =
      Lemonldap::NG::Common::Apache::Session->searchOn( $self->amOpts,
        "_assert_id", $samlID );

    if (
        my @keys =
        grep { $sessions->{$_}->{_session_kind} eq $self->sessionKind }
        keys %$sessions
      )
    {

        # A session was found
        foreach (@keys) {
            next
              unless ( $sessions->{$_}->{_session_kind} eq $self->sessionKind );
            my $session = $_;
            my $result  = 1;

            # Delete it
            my $samlSessionInfo = $self->getSamlSession($_);

            return 0 unless $samlSessionInfo;

            if ( defined $samlSessionInfo->data->{data} ) {
                $result = $samlSessionInfo->data->{data};
            }

            if ( $samlSessionInfo->remove ) {
                $self->logger->debug(
"Assertion session $session (Message ID $samlID) was deleted"
                );
                return $result;
            }
            else {
                $self->logger->error(
"Unable to delete assertion session $session (Message ID $samlID)"
                );
                $self->logger->error( $samlSessionInfo->error );
                return 0;
            }
        }
    }
    else {
        $self->logger->warn(
            "No assertion session found for request ID " . $samlID );
    }

    return 0;
}

## @method string resolveArtifact(Lasso::Profile profile, string artifact, int method)
# Resolve artifact to get real SAML message
# @param profile Lasso::Profile object
# @param artifact Artifact message
# @param method HTTP method
# @return SAML message
sub resolveArtifact {
    my ( $self, $profile, $artifact, $method ) = @_;
    my $message;

    # Login profile
    if ( $profile->isa("Lasso::Login") ) {

        # Init request message
        eval { Lasso::Login::init_request( $profile, $artifact, $method ); };
        return unless $self->checkLassoError($@);

        # Build request message
        eval { Lasso::Login::build_request_msg($profile); };
        return unless $self->checkLassoError($@);

        unless ( $profile->msg_url ) {
            $self->logger->error("No artifact resolution URL found");
            return;
        }

        my $request = HTTP::Request->new( 'POST' => $profile->msg_url );
        $request->content_type('text/xml');
        $request->header( Accept => 'text/xml' );
        $request->content( $profile->msg_body );

        $self->logger->debug(
            "Send message " . $profile->msg_body . " to " . $profile->msg_url );

        # SOAP call
        my $soap_answer = $self->ua->request($request);
        if ( $soap_answer->code() == "200" ) {
            $message = $soap_answer->content();
            $self->logger->debug("Get message $message");
        }
        else {
            $self->logger->error(
                "Error while sending message: " . $soap_answer->status_line );
        }
    }

    return $message;
}

## @method boolean storeArtifact(string id, string message, string session_id)
# Store artifact
# @param id Artifact ID
# @param message Artifact content
# @param session_id Session ID
# @return result
sub storeArtifact {
    my ( $self, $id, $message, $session_id ) = @_;

    my $infos = {
        type    => 'artifact',    # Session type
        _utime  => time(),        # Creation time
        _art_id => $id,
        message => $message,
    };
    $infos->{_saml_id} = $session_id if $session_id;

    my $samlSessionInfo = $self->getSamlSession( undef, $infos ) or return 0;
    return 0 unless $samlSessionInfo;

    my $art_session_id = $samlSessionInfo->id;

    $self->logger->debug("Keep artifact $id in session $art_session_id");

    return 1;
}

## @method hashRef loadArtifact(string id)
# Load artifact
# @param id Artifact ID
# @return Artifact session content
sub loadArtifact {
    my ( $self, $id ) = @_;
    my $art_session;

    unless ($id) {
        $self->logger->error("Cannot load artifact because no id given");
        return;
    }

    my $sessions =
      Lemonldap::NG::Common::Apache::Session->searchOn( $self->amOpts,
        "_art_id", $id );

    if (
        my @keys =
        grep { $sessions->{$_}->{_session_kind} eq $self->sessionKind }
        keys %$sessions
      )
    {

        my $nb_sessions = $#keys + 1;

        $self->logger->debug("Found $nb_sessions sessions for artifact $id");

        # There should only be 1 result
        return if ( $nb_sessions != 1 );

        my $session_id = shift @keys;
        my $session    = $session_id;

        # Open session
        my $samlSessionInfo = $self->getSamlSession($session_id);

        return unless $samlSessionInfo;

        # Get session contents
        foreach ( keys %{ $samlSessionInfo->data } ) {
            $art_session->{$_} = $samlSessionInfo->data->{$_};
        }

        # Delete session
        if ( $samlSessionInfo->remove ) {
            $self->logger->debug(
                "Artifact session $session (ID $id) was deleted");

            return $art_session;
        }
        else {
            $self->logger->error(
                "Unable to delete artifact session $session (ID $id)");
            $self->logger->error( $samlSessionInfo->error );
            return;
        }
    }

    return;
}

## @method string createArtifactResponse(Lasso::Login login)
# Create artifact response
# @param login Lasso::Login object
# @return Artifact response
sub createArtifactResponse {
    my ( $self, $req, $login ) = @_;

    my $artifact_id = $login->assertionArtifact();

    # Load artifact message into login response
    my $art_session = $self->loadArtifact($artifact_id);
    utf8::decode( $art_session->{message} );
    eval { $login->set_artifact_message( $art_session->{message} ); };
    if ($@) {
        $self->checkLassoError($@);
        $self->logger->error("Cannot load artifact message");
        return;
    }

    $self->logger->debug("Response loaded");

    # Try to get Lasso session
    my $session_id = $art_session->{_saml_id};
    if ($session_id) {
        $self->logger->debug("Find session_id $session_id in artifact session");

        my $session = $self->p->getApacheSession($session_id);
        unless ($session) {
            $self->logger->error("Unable to open session $session_id");
            return;
        }

        my $lassoSession = $session->data->{ $self->lsDump };

        if ($lassoSession) {
            unless ( $self->setSessionFromDump( $login, $lassoSession ) ) {
                $self->logger->error("Unable to load Lasso Session");
                return;
            }
            $self->logger->debug("Lasso Session loaded");
        }

    }
    else {
        $self->logger->debug("No session_id in artifact session");
    }

    # Build artifact response
    eval { Lasso::Login::build_response_msg($login); };
    if ($@) {
        $self->checkLassoError($@);
        $self->logger->error("Cannot build artifact response");
        return;
    }
    $self->logger->debug("Artifact response built");

    # Store Lasso session if session opened
    if ( $session_id and $login->is_session_dirty ) {
        $self->logger->debug("Save Lasso session in session");
        $self->updateSession( $req,
            { $self->lsDump => $login->get_session->dump }, $session_id );
    }

    # Return artifact message
    return $login->msg_body;
}

## @method boolean processArtRequestMsg(Lasso::Profile profile, string request)
# Process artifact request message
# @param profile Lasso::Profile object
# @param request SAML request
# @return result
sub processArtRequestMsg {
    my ( $self, $profile, $request ) = @_;

    # Login profile
    if ( $profile->isa("Lasso::Login") ) {

        eval { Lasso::Login::process_request_msg( $profile, $request ); };
        return $self->checkLassoError($@);

    }

    return 0;
}

## @method boolean processArtResponseMsg(Lasso::Profile profile, string response)
# Process artifact response message
# @param profile Lasso::Profile object
# @param response SAML response
# @return result
sub processArtResponseMsg {
    my ( $self, $profile, $response ) = @_;

    # Login profile
    if ( $profile->isa("Lasso::Login") ) {

        eval { Lasso::Login::process_response_msg( $profile, $response ); };
        return $self->checkLassoError($@);

    }

    return 0;
}

## @method string sendSOAPMessage(string endpoint, string message)
# Send SOAP message and get response
# @param endpoint SOAP End Point
# @param message SOAP message
# @return SOAP response
sub sendSOAPMessage {
    my ( $self, $endpoint, $message ) = @_;
    my $response;

    my $request = HTTP::Request->new( 'POST' => $endpoint );
    $request->content_type('text/xml');
    $request->header( Accept => 'text/xml' );
    $request->content($message);

    $self->logger->debug("Send SOAP message $message to $endpoint");

    # SOAP call
    my $soap_answer = $self->ua()->request($request);
    if ( $soap_answer->code() == "200" ) {
        $response = $soap_answer->content();
        $self->logger->debug("Get response $response");
    }
    else {
        $self->logger->debug("No response to SOAP request");
        return;
    }

    return $response;
}

## @method Lasso::AssertionQuery createAssertionQuery(Lasso::Server server)
# Create a new assertion query
# @param server Lasso::Server object
# @return assertion query
sub createAssertionQuery {
    my ( $self, $server ) = @_;
    my $query;

    # Create assertion query
    eval { $query = Lasso::AssertionQuery->new($server); };
    if ($@) {
        $self->checkLassoError($@);
        return;
    }

    return $query;
}

## @method Lasso::AssertionQuery createAttributeRequest(Lasso::Server server, string idp, hashref attributes, Lasso::Saml2NameID nameid)
# Create an attribute request
# @param server Lasso::Server object
# @param idp IDP entityID
# @param attributes List of requested attributes
# @param nameid Subject NameID
# @return attribute request
sub createAttributeRequest {
    my ( $self, $server, $idp, $attributes, $nameid ) = @_;
    my $query;

    # Create assertion query
    return unless ( $query = $self->createAssertionQuery($server) );

    $self->logger->debug("Assertion query created");

    # Init request
    my $method = Lasso::Constants::HTTP_METHOD_SOAP;
    my $type   = Lasso::Constants::ASSERTION_QUERY_REQUEST_TYPE_ATTRIBUTE;
    eval {
        Lasso::AssertionQuery::init_request( $query, $idp, $method, $type );
    };
    if ($@) {
        $self->checkLassoError($@);
        return;
    }

    $self->logger->debug("Assertion query request initiated");

    # Set NameID
    eval { $query->request()->Subject()->NameID($nameid); };
    if ($@) {
        $self->checkLassoError($@);
        return;
    }

    $self->logger->debug(
        "Set NameID " . $nameid->dump . " in assertion query" );

    # Store attributes in request
    my @requested_attributes;
    foreach ( keys %$attributes ) {

        # Create SAML2 Attribute
        my $attribute;

        eval { $attribute = Lasso::Saml2Attribute->new(); };
        if ($@) {
            $self->checkLassoError($@);
            return;
        }

        # Set attribute properties
        my ( $mandatory, $name, $format, $friendly_name ) =
          split( /;/, $attributes->{$_} );

        $attribute->Name($name)                  if defined $name;
        $attribute->NameFormat($format)          if defined $format;
        $attribute->FriendlyName($friendly_name) if defined $friendly_name;

        # Store attribute
        push @requested_attributes, $attribute;
    }

    # Set attributes in request
    eval { $query->request()->Attribute(@requested_attributes); };
    if ($@) {
        $self->checkLassoError($@);
        return;
    }

    # Build message
    eval { Lasso::AssertionQuery::build_request_msg($query); };
    if ($@) {
        $self->checkLassoError($@);
        return;
    }

    # Return query
    return $query;
}

## @method boolean validateAttributeRequest(Lasso::AssertionQuery query)
# Validate an attribute request
# @param query Lasso::AssertionQuery object
# @return result
sub validateAttributeRequest {
    my ( $self, $query ) = @_;

    eval { Lasso::AssertionQuery::validate_request($query); };

    return $self->checkLassoError($@);
}

## @method Lasso::AssertionQuery processAttributeRequest(Lasso::Server server, string request)
# Process an attribute request
# @param server Lasso::Server object
# @param request Request content
# @return assertion query
sub processAttributeRequest {
    my ( $self, $server, $request ) = @_;
    my $query;

    # Create assertion query
    return unless ( $query = $self->createAssertionQuery($server) );

    $self->logger->debug("Assertion query created");

    # Process response
    eval { Lasso::AssertionQuery::process_request_msg( $query, $request ); };
    if ($@) {
        $self->checkLassoError($@);
        return;
    }

    $self->logger->debug("Attribute request is valid");

    return $query;
}

## @method string buildAttributeResponse(Lasso::AssertionQuery query)
# Build attribute response
# @param query Lasso::AssertionQuery object
# @return attribute response
sub buildAttributeResponse {
    my ( $self, $query ) = @_;

    eval { Lasso::AssertionQuery::build_response_msg($query); };

    if ($@) {
        $self->checkLassoError($@);
        return;
    }

    return $query->msg_body;
}

## @method Lasso::AssertionQuery processAttributeResponse(Lasso::Server server, string response)
# Process an attribute response
# @param server Lasso::Server object
# @param response Response content
# @return assertion query
sub processAttributeResponse {
    my ( $self, $server, $response ) = @_;
    my $query;

    # Create assertion query
    return unless ( $query = $self->createAssertionQuery($server) );

    $self->logger->debug("Assertion query created");

    # Process response
    eval { Lasso::AssertionQuery::process_response_msg( $query, $response ); };
    if ($@) {
        $self->checkLassoError($@);
        return;
    }

    $self->logger->debug("Attribute response is valid");

    return $query;
}

## @method string getNameIDFormat(string format)
# Convert configuration string into SAML2 NameIDFormat string
# @param format configuration string
# @return SAML2 NameIDFormat string
sub getNameIDFormat {
    my ( $self, $format ) = @_;

    return Lasso::Constants::SAML2_NAME_IDENTIFIER_FORMAT_UNSPECIFIED
      if ( $format =~ /unspecified/i );
    return Lasso::Constants::SAML2_NAME_IDENTIFIER_FORMAT_EMAIL
      if ( $format =~ /email/i );
    return Lasso::Constants::SAML2_NAME_IDENTIFIER_FORMAT_X509
      if ( $format =~ /x509/i );
    return Lasso::Constants::SAML2_NAME_IDENTIFIER_FORMAT_WINDOWS
      if ( $format =~ /windows/i );
    return Lasso::Constants::SAML2_NAME_IDENTIFIER_FORMAT_KERBEROS
      if ( $format =~ /kerberos/i );
    return Lasso::Constants::SAML2_NAME_IDENTIFIER_FORMAT_ENTITY
      if ( $format =~ /entity/i );
    return Lasso::Constants::SAML2_NAME_IDENTIFIER_FORMAT_PERSISTENT
      if ( $format =~ /persistent/i );
    return Lasso::Constants::SAML2_NAME_IDENTIFIER_FORMAT_TRANSIENT
      if ( $format =~ /transient/i );
    return Lasso::Constants::SAML2_NAME_IDENTIFIER_FORMAT_ENCRYPTED
      if ( $format =~ /encrypted/i );

    return;
}

## @method int getHttpMethod(string method)
# Convert configuration string into Lasso HTTP Method integer
# @param method configuration string
# @return Lasso HTTP Method integer
sub getHttpMethod {
    my ( $self, $method ) = @_;

    $method ||= '';
    return Lasso::Constants::HTTP_METHOD_POST
      if ( $method =~ /^(http)?[-_]?post$/i );
    return Lasso::Constants::HTTP_METHOD_REDIRECT
      if ( $method =~ /^(http)?[-_]?redirect$/i );
    return Lasso::Constants::HTTP_METHOD_SOAP
      if ( $method =~ /^(http)?[-_]?soap$/i );
    return Lasso::Constants::HTTP_METHOD_ARTIFACT_GET
      if ( $method =~ /^(artifact)[-_]get$/i );
    return Lasso::Constants::HTTP_METHOD_ARTIFACT_POST
      if ( $method =~ /^(artifact)[-_]post$/i );

    return;
}

## @method int getHttpMethodString(int method)
# Convert configuration Lasso HTTP Method integer into string
# @param method Lasso HTTP Method
# @return method string
sub getHttpMethodString {
    my ( $self, $method ) = @_;

    return "POST" if ( $method == Lasso::Constants::HTTP_METHOD_POST );
    return "REDIRECT"
      if ( $method == Lasso::Constants::HTTP_METHOD_REDIRECT );
    return "SOAP" if ( $method == Lasso::Constants::HTTP_METHOD_SOAP );
    return "ARTIFACT GET"
      if ( $method == Lasso::Constants::HTTP_METHOD_ARTIFACT_GET );
    return "ARTIFACT POST"
      if ( $method == Lasso::Constants::HTTP_METHOD_ARTIFACT_POST );

    return "UNDEFINED";
}
## @method int getFirstHttpMethod(Lasso::Server server, string entityID, int protocolType)
# Find a suitable HTTP method for an entity with a given protocol
# @param server Lasso::Server object
# @param entityID entity ID
# @param protocolType Lasso protocol type
# @return Lasso HTTP Method
sub getFirstHttpMethod {
    my ( $self, $server, $entityID, $protocolType ) = @_;
    my $entity_provider;
    my $method;

    # Get Lasso::Provider object
    eval {
        $entity_provider = Lasso::Server::get_provider( $server, $entityID );
    };
    if ($@) {
        $self->checkLassoError($@);
        return;
    }

    # Find HTTP method
    eval {
        $method =
          Lasso::Provider::get_first_http_method( $server, $entity_provider,
            $protocolType );
    };
    if ($@) {
        $self->checkLassoError($@);
        return;
    }

    return $method;
}

## @method boolean disableSignature(Lasso::Profile profile)
# Modify Lasso signature hint to disable signature
# @param profile Lasso profile object
# @return result
sub disableSignature {
    my ( $self, $profile ) = @_;

    eval {
        Lasso::Profile::set_signature_hint( $profile,
            Lasso::Constants::PROFILE_SIGNATURE_HINT_FORBID );
    };

    return $self->checkLassoError($@);
}

## @method boolean forceSignature(Lasso::Profile profile)
# Modify Lasso signature hint to force signature
# @param profile Lasso profile object
# @return result
sub forceSignature {
    my ( $self, $profile ) = @_;

    eval {
        Lasso::Profile::set_signature_hint( $profile,
            Lasso::Constants::PROFILE_SIGNATURE_HINT_FORCE );
    };

    return $self->checkLassoError($@);
}

## @method boolean disableSignatureVerification(Lasso::Profile profile)
# Modify Lasso signature hint to disable signature verification
# @param profile Lasso profile object
# @return result
sub disableSignatureVerification {
    my ( $self, $profile ) = @_;

    eval {
        Lasso::Profile::set_signature_verify_hint( $profile,
            Lasso::Constants::PROFILE_SIGNATURE_VERIFY_HINT_IGNORE );
    };

    return $self->checkLassoError($@);
}

## @method boolean forceSignatureVerification(Lasso::Profile profile)
# Modify Lasso signature hint to force signature verification
# @param profile Lasso profile object
# @return result
sub forceSignatureVerification {
    my ( $self, $profile ) = @_;

    eval {
        Lasso::Profile::set_signature_verify_hint( $profile,
            Lasso::Constants::PROFILE_SIGNATURE_VERIFY_HINT_MAYBE );
    };

    return $self->checkLassoError($@);
}

## @method string getAuthnContext(string context)
# Convert configuration string into SAML2 AuthnContextClassRef string
# @param context configuration string
# @return SAML2 AuthnContextClassRef string
sub getAuthnContext {
    my ( $self, $context ) = @_;

    return Lasso::Constants::SAML2_AUTHN_CONTEXT_KERBEROS
      if ( $context =~ /^kerberos$/i );
    return Lasso::Constants::SAML2_AUTHN_CONTEXT_PASSWORD_PROTECTED_TRANSPORT
      if ( $context =~ /^password[-_ ]protected[-_ ]transport$/i );
    return Lasso::Constants::SAML2_AUTHN_CONTEXT_PASSWORD
      if ( $context =~ /^password$/i );
    return Lasso::Constants::SAML2_AUTHN_CONTEXT_X509
      if ( $context =~ /^x509$/i );
    return Lasso::Constants::SAML2_AUTHN_CONTEXT_TLS_CLIENT
      if ( $context =~ /^tls[-_ ]client$/i );
    return Lasso::Constants::SAML2_AUTHN_CONTEXT_UNSPECIFIED
      if ( $context =~ /^unspecified$/i );

    return;
}

## @method string timestamp2samldate(string timestamp)
# Convert timestamp into SAML2 date format
# @param timestamp UNIX timestamp
# @return SAML2 date
sub timestamp2samldate {
    my ( $self, $timestamp ) = @_;

    my @t        = gmtime($timestamp);
    my $samldate = strftime( "%Y-%m-%dT%TZ", @t );

    $self->logger->debug(
        "Convert timestamp $timestamp in SAML2 date: $samldate");

    return $samldate;
}

## @method string samldate2timestamp(string samldate)
# Convert SAML2 date format into timestamp
# @param samldate SAML2 date format
# @return UNIX timestamp
sub samldate2timestamp {
    my ( $self, $samldate ) = @_;

    my ( $year, $mon, $mday, $hour, $min, $sec, $msec, $ztime ) = ( $samldate =~
          /(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(\.\d+)?(Z)?/ );

    my $timestamp =
      timegm( $sec, $min, $hour, $mday, $mon - 1, $year - 1900, 0 );

    $self->logger->debug(
        "Convert SAML2 date $samldate in timestamp: $timestamp");

    return $timestamp;
}

## @pmethod int sendLogoutResponseToServiceProvider(Lasso::Logout $logout, int $method)
# Send logout response issue from a logout request.
# @param $logout Lasso Logout object
# @param $method Method to use
# @return boolean False if failed.
sub sendLogoutResponseToServiceProvider {
    my ( $self, $req, $logout, $method ) = @_;

    # Logout response
    unless ( $self->buildLogoutResponseMsg($logout) ) {
        $self->logger->warn( "Could not build a logout response for provider "
              . $logout->remote_providerID
              . ", staying on portal" );
        return $self->p->doPE( $req, PE_LOGOUT_OK );
    }

    # Send response depending on request method
    # HTTP-REDIRECT
    if ( $method == Lasso::Constants::HTTP_METHOD_REDIRECT ) {

        # Redirect user to response URL
        my $slo_url = $logout->msg_url;
        return [ 302, [ Location => URI->new($slo_url)->as_string ], [] ];
    }

    # HTTP-POST
    elsif ( $method == Lasso::Constants::HTTP_METHOD_POST ) {

        # Use autosubmit form
        my $slo_url    = $logout->msg_url;
        my $slo_body   = $logout->msg_body;
        my $relaystate = $logout->msg_relayState;

        # TODO: insert postUrl in $req
        $req->postUrl($slo_url);
        $req->{postFields} = { 'SAMLResponse' => $slo_body };

        # RelayState
        if ($relaystate) {
            $req->{postFields}->{'RelayState'} = encode_entities($relaystate);
            $req->data->{safeHiddenFormValues}->{RelayState} = 1;
        }

        return $self->p->do( $req, ['autoPost'] );
    }
    elsif ( $method == Lasso::Constants::HTTP_METHOD_SOAP ) {
        return $self->sendSLOSoapErrorResponse( $req, $logout, $method );
    }
    return $self->p->sendError( $req,
        "Lasso method '$method' should not be handle here...", 400 );
}

## @pmethod int sendLogoutRequestToProvider(Lasso::Logout $logout, string $providerID, int $method, boolean $relay, string $relayState)
# Send a logout request to a provider
# If information have to be displayed to users, such as iframe to send
# HTTP-Redirect or HTTP-POST logout request, then $self->{_info} will be
# updated.
# @param $logout Lasso Logout object
# @param $providerID The concerned service provider
# @param $method The method used to send the logout request
# @param $relay If SOAP method, build a relay logout request
# @param $relayState Relay State for SLO status
# @return int Number of concerned providers.
sub sendLogoutRequestToProvider {
    my ( $self, $req, $logout, $providerID, $method, $relay, $relayState ) = @_;
    my $server = $self->lassoServer;
    my $info;

    # Test if provider is mentionned
    if ( !$providerID ) {
        return ( 0, undef, undef );
    }

    # Lazy load provider
    $self->lazy_load_entityid($providerID);

    my $type = defined $self->spList->{$providerID} ? "SP" : "IDP";

    # Find EntityID in spList or idpList
    unless ( defined $self->{ lc($type) . 'List' }->{$providerID} ) {
        $self->logger->error("$providerID does not match any known $type");
        return ( 0, undef, undef );
    }

    # Get Provider Name and Conf Key from EntityID
    my $providerName =
      $self->{ lc($type) . 'List' }->{$providerID}->{name};
    my $confKey =
      $self->{ lc($type) . 'List' }->{$providerID}->{confKey};

    # Get first HTTP method
    my $protocolType = Lasso::Constants::MD_PROTOCOL_TYPE_SINGLE_LOGOUT;
    if ( !$method ) {
        $method =
          $self->getFirstHttpMethod( $server, $providerID, $protocolType );
    }

    # Fix a default value for the relay parameter
    $relay = 0 unless ( defined $relay );

    # Signature
    my $signSLOMessage =
      $self->{ lc($type) . 'Options' }->{$providerID}
      ->{ 'saml' . $type . 'MetaDataOptionsSignSLOMessage' } // -1;

    if ( $signSLOMessage == 0 ) {
        $self->logger->debug("SLO request will not be signed");
        $self->disableSignature($logout);
    }
    elsif ( $signSLOMessage == 1 ) {
        $self->logger->debug("SLO request will be signed");
        $self->forceSignature($logout);
    }
    else {
        $self->logger->debug("SLO request signature according to metadata");
    }

    # Relay State
    if ($relayState) {
        eval { $logout->msg_relayState($relayState); };
        if ($@) {
            $self->logger->error(
"Unable to set Relay State $relayState in SLO request for $confKey"
            );
            return ( 0, $method, undef );
        }
        $self->logger->debug('Relay state set');
    }

    # Skip SLO if no method found (#2746)
    unless ( defined $method and $method != -1 ) {
        $self->logger->debug("No HTTP SLO method found for $providerID");
        return ( 0, $method, undef );
    }

    # Initiate the logout request
    unless ( $self->initLogoutRequest( $logout, $providerID, $method ) ) {
        $self->logger->error("Initiate logout request failed for $providerID");
        return ( 0, $method, undef );
    }

    # Build request message
    unless ( $self->buildLogoutRequestMsg($logout) ) {
        $self->logger->error("Build logout request failed for $providerID");
        return ( 0, $method, undef );
    }

    $self->logger->debug("Request built for $providerID");

    # Keep message ID in memory to prevent replay
    my $samlID = $logout->request()->ID;
    unless ( $self->storeReplayProtection($samlID) ) {
        $self->logger->error("Unable to store message ID");
        return ( 0, $method, undef );
    }

    # Get portal value
    my $portal = $self->p->buildUrl;
    $portal =~ s/\/$//;

    # Send logout request to the provider depending of the request method
    # HTTP-REDIRECT
    if ( $method == Lasso::Constants::HTTP_METHOD_REDIRECT ) {

        $self->logger->debug(
            "Send HTTP-REDIRECT logout request to $providerID");

        # Redirect user to response URL
        my $slo_url = $logout->msg_url;

        # Create iFrame
        $info .= $self->loadTemplate(
            $req,
            'samlSpLogout',
            params => {
                url  => $slo_url,
                name => $providerName,
            }
        );
    }

    # HTTP-POST
    elsif ( $method == Lasso::Constants::HTTP_METHOD_POST ) {

        $self->logger->debug("Build POST relay logout request to $providerID");

        my $infos;

        # Store infos
        $infos->{type}       = 'relay';
        $infos->{_utime}     = time;
        $infos->{url}        = $logout->msg_url;
        $infos->{body}       = $logout->msg_body;
        $infos->{relayState} = $logout->msg_relayState;

        # Create a new relay session
        my $relayInfos = $self->getSamlSession( undef, $infos );

        my $relayID = $relayInfos->id;

        # Build the URL that could be used to play this logout request
        my $slo_url =
            $portal
          . '/saml/relaySingleLogoutPOST?'
          . build_urlencoded( relay => $relayID );

        # Create iFrame
        $info .= $self->loadTemplate(
            $req,
            'samlSpLogout',
            params => {
                url  => $slo_url,
                name => $providerName,
            }
        );
        $req->data->{cspChildSrc}->{ $self->p->cspGetHost( $logout->msg_url ) }
          = 1;
    }

    # HTTP-SOAP
    elsif ( $method == Lasso::Constants::HTTP_METHOD_SOAP ) {

        # Build a relay request, to be used after SLO process is done
        if ($relay) {

            $self->logger->debug(
                "Build SOAP relay logout request for $providerID");

            my $infos;
            $infos->{type}            = 'relay';
            $infos->{_utime}          = time;
            $infos->{ $self->lsDump } = $req->sessionInfo->{ $self->lsDump };
            $infos->{ $self->liDump } = $req->sessionInfo->{ $self->liDump };
            $infos->{_providerID}     = $providerID;
            $infos->{_relayState}     = $logout->msg_relayState;

            # Create a new relay session
            my $relayInfos = $self->getSamlSession( undef, $infos );

            my $relayID = $relayInfos->id;

            # Build the URL that could be used to play this logout request
            my $slo_url =
                $portal
              . '/saml/relaySingleLogoutSOAP?'
              . build_urlencoded( relay => $relayID );

            # Display information to the user
            $info .= $self->loadTemplate(
                $req,
                'samlSpSoapLogout',
                params => {
                    imgUrl => $slo_url,
                    name   => $providerName,
                }
            );
        }

        # Send the request directly
        else {

            $self->logger->debug("Send SOAP logout request to $providerID");

            my $slo_url  = $logout->msg_url;
            my $slo_body = $logout->msg_body;

            # Send SOAP request and manage response
            my $sp_response = $self->sendSOAPMessage( $slo_url, $slo_body );

            unless ($sp_response) {
                $self->logger->error("No logout response to SOAP request");
                return ( 0, $method, undef );
            }

            # Process logout response
            my $sp_result =
              $self->processLogoutResponseMsg( $logout, $sp_response );

            unless ($sp_result) {
                $self->logger->error("Fail to process logout response");
                return ( 0, $method, undef );
            }

            if ($relayState) {

                # Store success status for this SLO request
                my $sloStatusSessionInfos = $self->getSamlSession($relayState);

                if ($sloStatusSessionInfos) {
                    $sloStatusSessionInfos->update( { $confKey => 1 } );
                    $self->logger->debug(
                        "Store SLO status for $confKey in session $relayState");
                }
                else {
                    $self->logger->warn(
"Unable to store SLO status for $confKey in session $relayState"
                    );
                }
            }

            $self->logger->debug("Logout response is valid");

        }

    }

    return ( 1, $method, $info );

}

## @pmethod int sendLogoutRequestToProviders(Lasso::Logout logout, string relayState )
# Send logout response issue from a logout request to all other
# providers. If information have to be displayed to users, such as
# iframe to send HTTP-Redirect or HTTP-POST logout request, then
# $self->{_info} will be updated.
# @param logout Lasso Logout object
# @param relayState Relay State for SLO status
# @return int Number of concerned providers.
sub sendLogoutRequestToProviders {
    my ( $self, $req, $logout, $relayState ) = @_;
    my $server         = $self->lassoServer;
    my $providersCount = 0;
    my $content        = '';

    # Reset providerID into Lasso::Logout object
    $self->resetProviderIdIndex($logout);

    # Foreach SP found in session, get it from configuration, and send the
    # appropriate logout request (HTTP,POST,SOAP).
    while ( my $providerID = $self->getNextProviderId($logout) ) {

        # Send logout request
        my ( $rstatus, $rmethod, $rinfo ) =
          $self->sendLogoutRequestToProvider( $req, $logout, $providerID,
            undef, 1, $relayState );

        next unless ($rstatus);

        # Count providers that have to be request by HTTP redirect
        $providersCount++;

        # Add information if necessary
        if ($rinfo) {
            $content .= $rinfo;
        }

    }

    # Print some information to the user.
    $req->info(
        $self->loadTemplate(
            $req, 'samlSpsLogout', params => { content => $content }
        )
    ) if $providersCount;

    return $providersCount;
}

## @method boolean checkSignatureStatus(Lasso::Profile profile)
# Check signature status
# @param profile Lasso::Profile object
# @return result
sub checkSignatureStatus {
    my ( $self, $profile ) = @_;

    eval { Lasso::Profile::get_signature_status($profile); };

    return $self->checkLassoError($@);
}

## @method int authnContext2authnLevel(string authnContext)
# Return authentication level corresponding to authnContext
# @param authnContext SAML authentication context
# return authentication level
sub authnContext2authnLevel {
    my ( $self, $authnContext ) = @_;

    # Standard contexts
    return $self->conf->{samlAuthnContextMapPassword}
      if ( $authnContext eq $self->getAuthnContext("password") );
    return $self->conf->{samlAuthnContextMapPasswordProtectedTransport}
      if (
        $authnContext eq $self->getAuthnContext("password-protected-transport")
      );
    return $self->conf->{samlAuthnContextMapKerberos}
      if ( $authnContext eq $self->getAuthnContext("kerberos") );
    return $self->conf->{samlAuthnContextMapTLSClient}
      if ( $authnContext eq $self->getAuthnContext("tls-client") );

    # Extras contexts
    if (    defined $self->conf->{samlAuthnContextMapExtra}
        and defined $self->conf->{samlAuthnContextMapExtra}->{$authnContext} )
    {
        return $self->conf->{samlAuthnContextMapExtra}->{$authnContext};
    }

    return 0;
}

## @method int authnLevel2authnContext(int authnLevel)
# Return SAML authentication context corresponding to authnLevel
# @param authnLevel internal authentication level
# return SAML authentication context
sub authnLevel2authnContext {
    my ( $self, $authnLevel ) = @_;

    # Standard contexts
    return $self->getAuthnContext("password")
      if ( $authnLevel == $self->conf->{samlAuthnContextMapPassword} );
    return $self->getAuthnContext("password-protected-transport")
      if ( $authnLevel ==
        $self->conf->{samlAuthnContextMapPasswordProtectedTransport} );
    return $self->getAuthnContext("kerberos")
      if ( $authnLevel == $self->conf->{samlAuthnContextMapKerberos} );
    return $self->getAuthnContext("tls-client")
      if ( $authnLevel == $self->conf->{samlAuthnContextMapTLSClient} );

    # Extras contexts
    if ( defined $self->conf->{samlAuthnContextMapExtra} ) {
        foreach
          my $authnContext ( keys %{ $self->conf->{samlAuthnContextMapExtra} } )
        {
            return $authnContext
              if ( $authnLevel ==
                $self->conf->{samlAuthnContextMapExtra}->{$authnContext} );
        }
    }

    return $self->getAuthnContext("unspecified");
}

## @method boolean checkDestination(Lasso::Node message, string url)
# If SAML Destination attribute is present, check it
# @param message SAML request or response
# @param url Requested URL
# @return Result
sub checkDestination {
    my ( $self, $message, $url ) = @_;
    my $destination;

    # Read Destination
    eval { $destination = $message->Destination(); };

    # Ok if no Destination
    if ( $@ or !$destination ) {
        $self->logger->debug("No Destination in SAML message");
        return 1;
    }

    $self->logger->debug("Destination $destination found in SAML message");

    # Retrieve full URL
    my $portal = $self->p->buildUrl;
    $portal =~ s#^(https?://[^/]+)/.*#$1#;    # remove path of portal URL
    $url = $portal . $url;
    $url =~ s/\?.*//;

    # Compare Destination and URL
    if ( $destination eq $url ) {
        $self->logger->debug("Destination match URL $url");
        return 1;
    }

    $self->logger->error("Destination does not match URL $url");
    return 0;
}

## @method hashref getSamlSession(string id)
# Try to recover the SAML session corresponding to id and return session
# If id is set to undef, return a new session
# @param id session reference
# @return Lemonldap::NG::Common::Session object
sub getSamlSession {
    my ( $self, $id, $info ) = @_;

    my $samlSession = Lemonldap::NG::Common::Session->new( {
            storageModule        => $self->aModule,
            storageModuleOptions => $self->amOpts,
            cacheModule          => $self->conf->{localSessionStorage},
            cacheModuleOptions   => $self->conf->{localSessionStorageOptions},
            id                   => $id,
            kind                 => $self->sessionKind,
            ( $info ? ( info => $info ) : () ),
        }
    );

    if ( $samlSession->error ) {
        if ($id) {
            $self->userLogger->warn("SAML session $id isn't yet available");
        }
        else {
            $self->logger->error("Unable to create new SAML session");
            $self->logger->error( $samlSession->error );
        }
        return undef;
    }

    return $samlSession;
}

## @method Lasso::Saml2Attribute createAttribute(string name, string format, string friendly_name)
# Create a new SAML attribute
# @param name Attribute name
# @param format optional Attribute format
# @param friendly_name optional Attribute friendly name
# @return SAML attribute
sub createAttribute {
    my ( $self, $name, $format, $friendly_name ) = @_;
    my $attribute;

    # Name is required
    return unless defined $name;

    # SAML2 attribute
    eval { $attribute = Lasso::Saml2Attribute->new(); };
    if ($@) {
        $self->checkLassoError($@);
        return;
    }

    # Default values
    $friendly_name ||= $name;
    $format        ||= Lasso::Constants::SAML2_ATTRIBUTE_NAME_FORMAT_BASIC;

    # Set attribute properties
    $attribute->Name($name);
    $attribute->NameFormat($format);
    $attribute->FriendlyName($friendly_name);

    return $attribute;
}

## @method Lasso::Saml2AttributeValue createAttributeValue(string value, boolean force_utf8)
# Create a new SAML attribute value
# @param value Value to store
# @param force_utf8 set to 1 to decode UTF8 value
# @return SAML attribute value
sub createAttributeValue {
    my ( $self, $value, $force_utf8 ) = @_;
    my $saml2value;
    $force_utf8 = 1 unless defined($force_utf8);

    # Value is required
    return unless defined $value;

    # Decode UTF-8
    $self->logger->debug("Decode UTF8 value $value") if $force_utf8;
    $value = decode( "utf8", $value )                if $force_utf8;
    $self->logger->debug("Create attribute value $value");

    # SAML2 attribute value
    eval { $saml2value = Lasso::Saml2AttributeValue->new(); };
    if ($@) {
        $self->checkLassoError($@);
        return;
    }

    my @any;

    # Text node
    my $textNode;
    eval { $textNode = Lasso::MiscTextNode->new(); };
    if ($@) {
        $self->checkLassoError($@);
        return;
    }

    $textNode->text_child(1);
    $textNode->content($value);

    push @any, $textNode;

    $saml2value->any(@any);

    return $saml2value;
}

## @method int getEncryptionMode(string encryption_mode)
# Return Lasso encryption mode
# @param encryption_mode Encryption mode string
# @return Lasso encryption mode
sub getEncryptionMode {
    my ( $self, $encryption_mode ) = @_;

    return Lasso::Constants::ENCRYPTION_MODE_NAMEID
      if ( $encryption_mode =~ /^nameid$/i );
    return Lasso::Constants::ENCRYPTION_MODE_ASSERTION
      if ( $encryption_mode =~ /^assertion$/i );
    return Lasso::Constants::ENCRYPTION_MODE_NONE;
}

## @method boolean setProviderEncryptionMode(Lasso::Provider provider, int encryption_mode)
# Set encryption mode on a provider
# @param provider Lasso::Provider object
# @param encryption_mode Lasso encryption mode
# @return result
sub setProviderEncryptionMode {
    my ( $self, $provider, $encryption_mode ) = @_;

    eval {
        Lasso::Provider::set_encryption_mode( $provider, $encryption_mode );
    };

    return $self->checkLassoError($@);

}

## @method boolean updateSAMLSecondarySessions(string old_session_id, string new_session_id)
# Update _saml_id of secondary sessions when the session ID is changed (upgrade)
sub updateSAMLSecondarySessions {
    my ( $self, $req, $old_session_id, $new_session_id ) = @_;

    # Find SAML sessions
    my $saml_sessions =
      Lemonldap::NG::Common::Apache::Session->searchOn( $self->amOpts,
        "_saml_id", $old_session_id );

    if (
        my @saml_sessions_keys =
        grep { $saml_sessions->{$_}->{_session_kind} eq $self->sessionKind }
        keys %$saml_sessions
      )
    {

        foreach my $saml_session (@saml_sessions_keys) {

            # Get session
            $self->logger->debug("Retrieve SAML session $saml_session");

            my $samlSessionInfo = $self->getSamlSession($saml_session);

            # Update session
            if ($samlSessionInfo) {
                $samlSessionInfo->update( { '_saml_id' => $new_session_id } );
            }
        }
    }
    return;
}

## @method boolean deleteSAMLSecondarySessions(string session_id)
# Find and delete SAML sessions bounded to a primary session
# @param session_id Primary session ID
# @return result
sub deleteSAMLSecondarySessions {
    my ( $self, $session_id ) = @_;
    my $result = 1;

    # Find SAML sessions
    my $saml_sessions =
      Lemonldap::NG::Common::Apache::Session->searchOn( $self->amOpts,
        "_saml_id", $session_id );

    if (
        my @saml_sessions_keys =
        grep { $saml_sessions->{$_}->{_session_kind} eq $self->sessionKind }
        keys %$saml_sessions
      )
    {

        foreach my $saml_session (@saml_sessions_keys) {

            # Get session
            $self->logger->debug("Retrieve SAML session $saml_session");

            my $samlSessionInfo = $self->getSamlSession($saml_session);

            # Delete session
            if ( $samlSessionInfo->remove ) {
                $self->logger->debug("SAML session $saml_session deleted");
            }
            else {
                $self->logger->error(
                    "Unable to delete SAML session $saml_session");
                $self->logger->error( $samlSessionInfo->error );
                $result = 0;
            }
        }
    }
    elsif ($session_id) {
        $self->logger->debug("No SAML session found for session $session_id");
    }

    return $result;
}

## @method void sendSLOErrorResponse(Lasso::Logout logout, string method)
# Send an SLO error response
# @param logout Lasso::Logout object
# @param method HTTP method
sub sendSLOErrorResponse {
    my ( $self, $req, $logout, $method ) = @_;

    # Load empty session
    my $session =
      '<Session xmlns="http://www.entrouvert.org/namespaces/lasso/0.0"/>';

    unless ( $self->setSessionFromDump( $logout, $session ) ) {
        return $self->p->sendError( $req,
            "Could not set empty session in logout object", 500 );
    }

    # Send invalidated SLO response
    return $self->sendLogoutResponseToServiceProvider( $req, $logout, $method );
}

## @method void sendSLOSoapErrorResponse(Lasso::Logout logout, string method)
# Send an SLO error response
# @param logout Lasso::Logout object
# @param method HTTP method
# @return PSGI response
sub sendSLOSoapErrorResponse {
    my ( $self, $req, $logout, $method ) = @_;

    # Load empty session
    my $session =
      '<Session xmlns="http://www.entrouvert.org/namespaces/lasso/0.0"/>';

    unless ( $self->setSessionFromDump( $logout, $session ) ) {
        return $self->p->sendError( $req,
            "Could not set empty session in logout object" );
    }
    my $slo_body = $logout->msg_body;
    $self->logger->debug("SOAP response $slo_body");
    return [
        200,
        [
            'Content-Type'   => 'text/xml',
            'Content-Length' => length($slo_body)
        ],
        [$slo_body]
    ];
}

## @method string getQueryString()
# Return query string with or without CGI query_string() method
# @return query string
sub getQueryString {
    my ( $self, $req ) = @_;

    my $query_string;

    if ( $self->conf->{samlUseQueryStringSpecific} ) {
        my @pairs =
          split( /&/, $req->param('issuerQuery') || $req->query_string );
        $query_string = join( ';', @pairs );

    }
    else {
        # TODO: verify this
        $query_string = $req->param('issuerQuery') || $req->query_string;
    }

    return $query_string;
}

sub importRealSession {
    my ( $self, $req, $ssoSession ) = @_;
    $req->sessionInfo( $ssoSession->data );
    $req->id( $ssoSession->id );
    $req->user( $ssoSession->data->{ $self->conf->{whatToTrace} } );
}

sub _get_metadata_xml {
    my ( $self, $req_or_undef, $type, $sp, $idp ) = @_;

    require Lemonldap::NG::Common::Conf::SAML::Metadata;

    my @signing_keys;
    if ($sp) {
        $self->lazy_load_entityid($sp);
        if ( my $key_list =
            $self->spOptions->{$sp}->{samlSPMetaDataOptionsSignatureKey} )
        {
            @signing_keys = $self->_get_keys_from_config($key_list);
        }

    }
    elsif ($idp) {
        $self->lazy_load_entityid($idp);
        if ( my $key_list =
            $self->idpOptions->{$idp}->{samlIDPMetaDataOptionsSignatureKey} )
        {
            @signing_keys = $self->_get_keys_from_config($key_list);
        }
    }
    if ( !@signing_keys ) {
        @signing_keys =
          $self->_get_keys_from_config( $self->conf->{samlServiceSignatureKey}
              || "default-saml-sig" );
    }

    my @encryption_keys =
      $self->_get_keys_from_config( $self->conf->{samlServiceEncryptionKey}
          || "default-saml-enc" );

    if ( my $metadata = Lemonldap::NG::Common::Conf::SAML::Metadata->new() ) {
        my $portal =
            $req_or_undef
          ? $req_or_undef->portal
          : $self->p->HANDLER->tsv->{portal}->();
        return $metadata->serviceToXML(
            $self->conf,
            $type,
            portal       => $portal,
            signing_keys => \@signing_keys,
            (
                @encryption_keys ? ( encryption_keys => \@encryption_keys ) : ()
            ),
        );
    }
    return;
}

sub _get_keys_from_config {
    my ( $self, $list_of_keys ) = @_;
    my @keys;

    if ($list_of_keys) {
        for my $key_id ( split( /\s*,\s*/, $list_of_keys ) ) {
            my $key = $self->get_public_key($key_id);
            if ($key) {
                push @keys, $key;
            }
            else {
                $self->logger->debug("Could not publish $key_id in metadata");
            }
        }
    }
    return @keys;
}

sub metadata {
    my ( $self, $req ) = @_;
    my $type = $req->param('type') || 'all';
    my $idp  = $req->param('idp');
    my $sp   = $req->param('sp');
    if ( my $metadata = $self->_get_metadata_xml( $req, $type, $sp, $idp ) ) {
        return [
            200,
            [
                'Content-Type'   => 'application/xml',
                'Content-Length' => length($metadata),
            ],
            [$metadata]
        ];
    }
    return $self->p->sendError( $req, 'Unable to build Metadata', 500 );
}

## @method int getSignatureMethod(string signature_method)
# Return Lasso signature method
# @param signature_method Signature method string
# @return Lasso signature method
sub getSignatureMethod {
    my ( $self, $signature_method ) = @_;

    $signature_method ||= $self->getDefaultSignatureMethod();

    if ( my $const =
        Lasso::Constants->can( "SIGNATURE_METHOD_" . uc($signature_method) ) )
    {
        return $const->();
    }
    else {
        return 0;
    }
}

sub getDefaultSignatureMethod {
    my ($self) = @_;

    my $signature_method =
      $self->conf->{samlServiceSignatureMethod} || 'RSA_SHA1';
    return $signature_method;
}

## @method boolean setProviderSignatureMethod(Lasso::Provider provider, int signature_method)
# Set signature method on a provider
# @param provider Lasso::Provider object
# @param signature_method Lasso signature method
# @return result
sub setProviderSignatureMethod {
    my ( $self, $provider, $signature_method ) = @_;

    my $key = $self->get_private_key( $self->_get_default_signing_key_name );
    return $self->setProviderSignatureOptions( $provider, $signature_method,
        $key );
}

sub setProviderSignatureOptions {
    my ( $self, $provider, $signature_method, $key ) = @_;

    # We have to use an intermediate variable to avoid interferences between
    # Lasso binding and tied hashes (#3105)
    my $priv = $key->{private};
    my $pass = $key->{password};
    my $cert = $key->{public};

    eval {
        my $key = Lasso::Key::new_for_signature_from_file( $priv, $pass,
            $signature_method, $cert, );
        $provider->set_server_signing_key($key);
    };

    return $self->checkLassoError($@);
}

sub _override_signature_or_key {
    my ( $self, $entityID, $log_description, $signature_method,
        $signature_key_list )
      = @_;
    if ( $signature_method or $signature_key_list ) {
        $signature_key_list //= "";
        my $signature_key = ( split( /\s*,\s*/, $signature_key_list ) )[0];

        $signature_method ||= $self->getDefaultSignatureMethod;
        my $lasso_signature_method =
          $self->getSignatureMethod($signature_method);

        $signature_key ||= $self->_get_default_signing_key_name;
        my $key = $self->get_private_key($signature_key);

        # Fallback to default key if custom key was not found
        if ( !$key ) {
            $signature_key = $self->_get_default_signing_key_name;
            $key           = $self->get_private_key($signature_key);
        }

        $self->logger->debug( "Setting signature key $signature_key"
              . " and signature method $signature_method"
              . " on $log_description" );

        unless (
            $self->setProviderSignatureOptions(
                $self->lassoServer->get_provider($entityID),
                $lasso_signature_method, $key
            )
          )
        {
            $self->logger->error(
                "Unable to set signature options on $log_description");
            return;
        }
    }
}

# The default signing key is the first key listed in samlServiceSignatureKey, with
# fallback to default-saml-sig
sub _get_default_signing_key_name {
    my ($self) = @_;
    my @key_list =
      split( /\s*,\s*/, ( $self->conf->{samlServiceSignatureKey} || "" ) );

    return ( $key_list[0] || "default-saml-sig" );
}

# The default encryption key is the first key listed in samlServiceEncryptionKey, with
# fallback to default-saml-enc
sub _get_default_encryption_key_name {
    my ($self) = @_;
    my @key_list =
      split( /\s*,\s*/, ( $self->conf->{samlServiceEncryptionKey} || "" ) );

    return ( $key_list[0] || "default-saml-enc" );
}

1;

__END__

=head1 NAME

=encoding utf8

Lemonldap::NG::Portal::Lib::SAML - Common SAML functions

=head1 SYNOPSIS

use Lemonldap::NG::Portal::Lib::SAML;

=head1 DESCRIPTION

This module contains common methods for SAML authentication
and user information loading

=head1 METHODS

=head2 loadLasso

Load Lasso module

=head2 loadService

Load SAML service by creating a Lasso::Server

=head2 loadIDPs

Load SAML identity providers

=head2 loadSPs

Load SAML service providers

=head2 checkMessage

Check SAML requests and responses

=head2 checkLassoError

Log Lasso error code and message if this is actually a Lasso::Error with code > 0

=head2 createServer

Load service metadata and create Lasso::Server object

=head2 addIDP

Add IDP to an existing Lasso::Server

=head2 addSP

Add SP to an existing Lasso::Server

=head2 addAA

Add Attribute Authority to an existing Lasso::Server

=head2 addProvider

Add provider to an existing Lasso::Server

=head2 getOrganizationName

Return name of organization picked up from metadata

=head2 createAuthnRequest

Create authentication request for selected IDP

=head2 createLogin

Create Lasso::Login object

=head2 initAuthnRequest

Init authentication request

=head2 initIdpInitiatedAuthnRequest

Init authentication request for IDP initiated

=head2 buildAuthnRequestMsg

Build authentication request message

=head2 processAuthnRequestMsg

Process authentication request message

=head2 processAuthnRequestMsgWithError

Process authentication request message and return Lasso error

=head2 validateRequestMsg

Validate request message

=head2 buildAuthnResponseMsg

Build authentication response message

=head2 buildArtifactMsg

Build artifact message

=head2 buildAssertion

Build assertion

=head2 processAuthnResponseMsg

Process authentication response message

=head2 getNameIdentifier

Get NameID from Lasso Profile

=head2 createIdentity

Create Lasso::Identity object

=head2 createSession

Create Lasso::Session object

=head2 acceptSSO

Accept SSO from IDP

=head2 storeRelayState

Store information in relayState database and return

=head2 extractRelayState

Extract RelayState information into $self

=head2 getAssertion

Get assertion in Lasso::Login object

=head2 getAttributeValue

Get SAML attribute value corresponding to name, format and friendly_name
Multivaluated values are separated by ';'

=head2 validateConditions

Validate conditions

=head2 createLogoutRequest

Create logout request for selected entity

=head2 createLogout

Create Lasso::Logout object

=head2 initLogoutRequest

Init logout request

=head2 buildLogoutRequestMsg

Build logout request message

=head2 setSessionFromDump

Set session from dump in Lasso::Profile object

=head2 setIdentityFromDump

Set identity from dump in Lasso::Profile object

=head2 getMetaDataURL

Get URL stored in a service metadata configuration key

=head2 processLogoutResponseMsg

Process logout response message

=head2 processLogoutRequestMsg

Process logout request message

=head2 validateLogoutRequest

Validate logout request

=head2 buildLogoutResponseMsg

Build logout response msg

=head2 storeReplayProtection

Store ID of an SAML message in Replay Protection base

=head2 replayProtection

Check if SAML message do not correspond to a previously responded message

=head2 resolveArtifact

Resolve artifact to get the real SAML message

=head2 storeArtifact

Store artifact

=head2 loadArtifact

Load artifact

=head2 createArtifactResponse

Create artifact response

=head2 processArtRequestMsg

Process artifact response message

=head2 processArtResponseMsg

Process artifact response message

=head2 sendSOAPMessage

Send SOAP message and get response

=head2 createAssertionQuery

Create a new assertion query

=head2 createAttributeRequest

Create an attribute request

=head2 validateAttributeRequest

Validate an attribute request

=head2 processAttributeRequest

Process an attribute request

=head2 buildAttributeResponse

Build attribute response

=head2 processAttributeResponse

Process an attribute response

=head2 getNameIDFormat

Convert configuration string into SAML2 NameIDFormat string

=head2 getHttpMethod

Convert configuration string into Lasso HTTP Method integer

=head2 getHttpMethodString

Convert configuration Lasso HTTP Method integer into string

=head2 getFirstHttpMethod

Find a suitable HTTP method for an entity with a given protocol

=head2 disableSignature

Modify Lasso signature hint to disable signature

=head2 forceSignature

Modify Lasso signature hint to force signature

=head2 disableSignatureVerification

Modify Lasso signature hint to disable signature verification

=head2 forceSignatureVerification

Modify Lasso signature hint to force signature verification

=head2 getAuthnContext

Convert configuration string into SAML2 AuthnContextClassRef string

=head2 timestamp2samldate

Convert timestamp into SAML2 date format

=head2 samldate2timestamp

Convert SAML2 date format into timestamp

=head2 sendLogoutResponseToServiceProvider

Send logout response issue from a logout request

=head2 sendLogoutRequestToProvider

Send logout request to a provider

=head2 sendLogoutRequestToProviders

Send logout response issue from a logout request to all other
providers. If information have to be displayed to users, such as
iframe to send HTTP-Redirect or HTTP-POST logout request, then
$self->{_info} will be updated.

=head2 checkSignatureStatus

Check signature status

=head2 authnContext2authnLevel

Return authentication level corresponding to authnContext

=head2 authnLevel2authnContext

Return SAML authentication context corresponding to authnLevel

=head2 checkDestination

If SAML Destination attribute is present, check it

=head2 getSamlSession

Try to recover the SAML session corresponding to id and return session data

=head2 createAttribute

Create a new SAML attribute

=head2 createAttributeValue

Create a new SAML attribute value

=head2 getEncryptionMode

Return Lasso encryption mode

=head2 setProviderEncryptionMode

Set encryption mode on a provider

=head2 deleteSAMLSecondarySessions

Find and delete SAML sessions bounded to a primary session

=head2 sendSLOErrorResponse

Send an SLO error response

=head2 getQueryString

Get query string with or without CGI query_string() method

=head2 getSignatureMethod

Return Lasso signature method

=head2 setProviderSignatureMethod

Set signature method on a provider

=head1 SEE ALSO

L<Lemonldap::NG::Portal::Auth::SAML>, L<Lemonldap::NG::Portal::UserDBSAML>

=head1 AUTHORS

=over

=item LemonLDAP::NG team L<http://lemonldap-ng.org/team>

=back

=head1 BUG REPORT

Use OW2 system to report bug or ask for features:
L<https://gitlab.ow2.org/lemonldap-ng/lemonldap-ng/issues>

=head1 DOWNLOAD

Lemonldap::NG is available at
L<https://lemonldap-ng.org/download>

=head1 COPYRIGHT AND LICENSE

See COPYING file for details.

This library is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2, or (at your option)
any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program.  If not, see L<http://www.gnu.org/licenses/>.

=cut
