SAML.pm 65.9 KB
Newer Older
Xavier Guimard's avatar
Xavier Guimard committed
1 2 3 4
package Lemonldap::NG::Portal::Issuer::SAML;

use strict;
use Mouse;
Xavier Guimard's avatar
Xavier Guimard committed
5
use Lemonldap::NG::Portal::Lib::SAML;
Xavier Guimard's avatar
Xavier Guimard committed
6 7 8 9 10 11 12 13 14
use Lemonldap::NG::Portal::Main::Constants qw(
  PE_OK
  PE_SAML_ART_ERROR
  PE_SAML_DESTINATION_ERROR
  PE_SAML_SESSION_ERROR
  PE_SAML_SIGNATURE_ERROR
  PE_SAML_SLO_ERROR
  PE_SAML_SSO_ERROR
  PE_SAML_UNKNOWN_ENTITY
15
  PE_SAML_SERVICE_NOT_ALLOWED
Xavier Guimard's avatar
Xavier Guimard committed
16
  PE_UNAUTHORIZEDPARTNER
Xavier Guimard's avatar
Xavier Guimard committed
17 18
);

Christophe Maudoux's avatar
Christophe Maudoux committed
19
our $VERSION = '2.0.3';
Xavier Guimard's avatar
Xavier Guimard committed
20 21 22 23

extends 'Lemonldap::NG::Portal::Main::Issuer',
  'Lemonldap::NG::Portal::Lib::SAML';

Christophe Maudoux's avatar
Christophe Maudoux committed
24
has rule           => ( is => 'rw' );
Xavier Guimard's avatar
Xavier Guimard committed
25
has ssoUrlRe       => ( is => 'rw' );
26
has ssoUrlArtifact => ( is => 'rw' );
Xavier Guimard's avatar
Xavier Guimard committed
27
has ssoGetUrl      => ( is => 'rw' );
28

29 30 31
use constant sessionKind => 'ISAML';
use constant lsDump      => '_lassoSessionDumpI';
use constant liDump      => '_lassoIdentityDumpI';
32

33 34 35 36 37
# INTERFACE

# Simply store SP in $req->env
use constant beforeAuth => 'storeEnv';

Xavier Guimard's avatar
Xavier Guimard committed
38 39 40 41 42
# INITIALIZATION

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

43 44 45
    # Parse activation rule
    my $hd = $self->p->HANDLER;
    $self->logger->debug( "SAML rule -> " . $self->conf->{issuerDBSAMLRule} );
Xavier Guimard's avatar
Xavier Guimard committed
46 47
    my $rule =
      $hd->buildSub( $hd->substitute( $self->conf->{issuerDBSAMLRule} ) );
48 49 50 51 52 53
    unless ($rule) {
        $self->error( "Bad SAML rule -> " . $hd->tsv->{jail}->error );
        return 0;
    }
    $self->{rule} = $rule;

Xavier Guimard's avatar
Xavier Guimard committed
54
    # Prepare SSO URL catching
55 56 57 58 59
    my $saml_sso_get_url = $self->ssoGetUrl(
        $self->getMetaDataURL(
            "samlIDPSSODescriptorSingleSignOnServiceHTTPRedirect", 1
        )
    );
Xavier Guimard's avatar
Xavier Guimard committed
60 61 62 63 64 65 66 67 68 69 70 71
    my $saml_sso_get_url_ret = $self->getMetaDataURL(
        "samlIDPSSODescriptorSingleSignOnServiceHTTPRedirect", 2 );
    my $saml_sso_post_url =
      $self->getMetaDataURL( "samlIDPSSODescriptorSingleSignOnServiceHTTPPost",
        1 );
    my $saml_sso_post_url_ret =
      $self->getMetaDataURL( "samlIDPSSODescriptorSingleSignOnServiceHTTPPost",
        2 );
    my $saml_sso_art_url = $self->getMetaDataURL(
        "samlIDPSSODescriptorSingleSignOnServiceHTTPArtifact", 1 );
    my $saml_sso_art_url_ret = $self->getMetaDataURL(
        "samlIDPSSODescriptorSingleSignOnServiceHTTPArtifact", 2 );
Xavier Guimard's avatar
Xavier Guimard committed
72
    $self->ssoUrlRe(
73
qr/^($saml_sso_get_url|$saml_sso_get_url_ret|$saml_sso_post_url|$saml_sso_post_url_ret|$saml_sso_art_url|$saml_sso_art_url_ret)(?:\?.*)?$/i
Xavier Guimard's avatar
Xavier Guimard committed
74
    );
75 76
    $self->ssoUrlArtifact(
        qr/^($saml_sso_art_url|$saml_sso_art_url_ret)(?:\?.*)?$/i);
Xavier Guimard's avatar
Xavier Guimard committed
77

Xavier Guimard's avatar
Xavier Guimard committed
78
    # Launch parents initialization subroutines, then launch IdP en SP lists
Xavier Guimard's avatar
Xavier Guimard committed
79
    my $res = (
Xavier Guimard's avatar
Xavier Guimard committed
80 81 82 83 84 85 86 87 88 89 90 91
        $self->Lemonldap::NG::Portal::Main::Issuer::init()

          # Load SAML service
          and $self->Lemonldap::NG::Portal::Lib::SAML::init()

          # Load SAML service providers
          and $self->loadSPs()

          # Load SAML identity providers
          # Required to manage SLO in Proxy mode
          and $self->loadIDPs()
    );
Xavier Guimard's avatar
Xavier Guimard committed
92
    return 0 unless ($res);
Xavier Guimard's avatar
Xavier Guimard committed
93

94 95 96 97 98
    if ( $self->conf->{samlOverrideIDPEntityID} ) {
        $self->lassoServer->ProviderID(
            $self->conf->{samlOverrideIDPEntityID} );
    }

99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137
    # Single logout routes
    $self->addUnauthRouteFromMetaDataURL(
        "samlIDPSSODescriptorSingleLogoutServiceSOAP",
        1, 'sloServer', ['POST'] );
    $self->addUnauthRouteFromMetaDataURL(
        "samlIDPSSODescriptorSingleLogoutServiceSOAP",
        2, 'sloServer', ['POST'] );
    $self->addUnauthRouteFromMetaDataURL(
        "samlIDPSSODescriptorSingleLogoutServiceHTTPRedirect",
        1, 'sloServer', ['GET'] );
    $self->addUnauthRouteFromMetaDataURL(
        "samlIDPSSODescriptorSingleLogoutServiceHTTPRedirect",
        2, 'sloServer', ['GET'] );
    $self->addUnauthRouteFromMetaDataURL(
        "samlIDPSSODescriptorSingleLogoutServiceHTTPPost",
        1, 'sloServer', ['POST'] );
    $self->addUnauthRouteFromMetaDataURL(
        "samlIDPSSODescriptorSingleLogoutServiceHTTPPost",
        2, 'sloServer', ['POST'] );

    $self->addAuthRouteFromMetaDataURL(
        "samlIDPSSODescriptorSingleLogoutServiceSOAP",
        1, 'authSloServer', ['POST'] );
    $self->addAuthRouteFromMetaDataURL(
        "samlIDPSSODescriptorSingleLogoutServiceSOAP",
        2, 'authSloServer', ['POST'] );
    $self->addAuthRouteFromMetaDataURL(
        "samlIDPSSODescriptorSingleLogoutServiceHTTPRedirect",
        1, 'authSloServer', ['GET'] );
    $self->addAuthRouteFromMetaDataURL(
        "samlIDPSSODescriptorSingleLogoutServiceHTTPRedirect",
        2, 'authSloServer', ['GET'] );
    $self->addAuthRouteFromMetaDataURL(
        "samlIDPSSODescriptorSingleLogoutServiceHTTPPost",
        1, 'authSloServer', ['POST'] );
    $self->addAuthRouteFromMetaDataURL(
        "samlIDPSSODescriptorSingleLogoutServiceHTTPPost",
        2, 'authSloServer', ['POST'] );

Xavier Guimard's avatar
Xavier Guimard committed
138 139 140 141
    # SOAP routes (access without authentication)
    $self->addRouteFromMetaDataURL(
        'samlIDPSSODescriptorArtifactResolutionServiceArtifact',
        3, 'artifactServer', ['POST'] );
142 143 144
    $self->addRouteFromMetaDataURL(
        'samlAttributeAuthorityDescriptorAttributeServiceSOAP',
        1, 'attributeServer', ['POST'] );
Xavier Guimard's avatar
Xavier Guimard committed
145 146

    # TODO: @coudot, why this URL isn't managed with a conf param ?
Xavier Guimard's avatar
Xavier Guimard committed
147 148 149 150
    $self->addUnauthRoute(
        $self->path => { relaySingleLogoutSOAP => 'sloRelaySoap' },
        [ 'GET', 'POST' ]
    );
Xavier Guimard's avatar
Xavier Guimard committed
151
    $self->addAuthRoute(
152 153 154 155 156
        $self->path => { relaySingleLogoutPOST => 'sloRelayPost' },
        [ 'GET', 'POST' ]
    );
    $self->addUnauthRoute(
        $self->path => { relaySingleLogoutPOST => 'sloRelayPost' },
Xavier Guimard's avatar
Xavier Guimard committed
157 158 159
        [ 'GET', 'POST' ]
    );
    return $res;
Xavier Guimard's avatar
Xavier Guimard committed
160 161 162 163
}

# RUNNING METHODS

164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181
# "beforeAuth" entry point. Store just SP and SP confKey in $req->env
sub storeEnv {
    my ( $self, $req ) = @_;
    return PE_OK
      if ( $req->uri !~ $self->ssoUrlRe or $req->uri =~ $self->ssoUrlArtifact );
    my ( $request, $response, $method, $relaystate, $artifact ) =
      $self->checkMessage( $req, $req->uri, $req->method, $req->content_type );
    return PE_OK if ( $artifact or !$request );
    my $login = $self->createLogin( $self->lassoServer );
    if ( my $sp = $login->remote_providerID() ) {
        $req->env->{llng_saml_sp} = $sp;
        if ( my $spConfKey = $self->spList->{$sp}->{confKey} ) {
            $req->env->{llng_saml_spconfkey} = $spConfKey;
        }
    }
    return PE_OK;
}

182 183 184 185
sub ssoMatch {
    my ( $self, $req ) = @_;
    my $url = $self->normalize_url( $req->uri, $self->conf->{issuerDBSAMLPath},
        $self->ssoGetUrl );
Xavier Guimard's avatar
Xavier Guimard committed
186 187 188 189 190
    return (
        $url =~ $self->ssoUrlRe or $req->data->{_proxiedRequest}
        ? 1
        : 0
    );
191 192
}

Xavier Guimard's avatar
Xavier Guimard committed
193
# Main method (launched only for authenticated users, see Main/Issuer)
Xavier Guimard's avatar
Xavier Guimard committed
194 195 196 197 198 199 200
sub run {
    my ( $self, $req ) = @_;
    my $login;
    my $protocolProfile;
    my $artifact_method;
    my $authn_context;

201 202 203 204 205
    # Check activation rule
    unless ( $self->rule->( $req, $req->sessionInfo ) ) {
        $self->userLogger->error('SAML service not authorized');
        return PE_SAML_SERVICE_NOT_ALLOWED;
    }
Xavier Guimard's avatar
Xavier Guimard committed
206

Xavier Guimard's avatar
Xavier Guimard committed
207 208 209 210 211
    # Session ID
    my $session_id = $req->{sessionInfo}->{_session_id} || $req->{id};

    # Session creation timestamp
    my $time = $req->{sessionInfo}->{_utime} || time();
Xavier Guimard's avatar
Xavier Guimard committed
212

Xavier Guimard's avatar
Xavier Guimard committed
213
    # Get HTTP request information to know
Xavier Guimard's avatar
Xavier Guimard committed
214
    # if we are receving SAML request or response
Xavier Guimard's avatar
Xavier Guimard committed
215
    my $url                     = $req->uri;
216
    my $request_method          = $req->param('issuerMethod') || $req->method;
Xavier Guimard's avatar
Xavier Guimard committed
217
    my $content_type            = $req->content_type();
Xavier Guimard's avatar
Xavier Guimard committed
218 219 220
    my $idp_initiated           = $req->param('IDPInitiated');
    my $idp_initiated_sp        = $req->param('sp');
    my $idp_initiated_spConfKey = $req->param('spConfKey');
Xavier Guimard's avatar
Xavier Guimard committed
221

222 223 224 225
    # Normalize URL to be tolerant to SAML Path
    $url = $self->normalize_url( $url, $self->conf->{issuerDBSAMLPath},
        $self->ssoGetUrl );

226 227 228 229 230 231 232
    # Get domain GET attribute
    my $domain = $req->param('domain');

    if ($domain) {
        $self->logger->debug("Found domain $domain in SAML GET parameter");
    }

Xavier Guimard's avatar
Xavier Guimard committed
233
    # 1.1. SSO (SSO URL or Proxy Mode)
Xavier Guimard's avatar
Xavier Guimard committed
234
    if ( $url =~ $self->ssoUrlRe or $req->data->{_proxiedRequest} ) {
Xavier Guimard's avatar
Xavier Guimard committed
235

236
        $self->logger->debug("URL $url detected as an SSO request URL");
Xavier Guimard's avatar
Xavier Guimard committed
237 238

        # Check message
Xavier Guimard's avatar
Xavier Guimard committed
239 240
        my ( $request, $response, $method, $relaystate, $artifact ) =
          $self->checkMessage( $req, $url, $request_method, $content_type );
Xavier Guimard's avatar
Xavier Guimard committed
241 242

        # Create Login object
243
        my $login = $self->createLogin( $self->lassoServer );
Xavier Guimard's avatar
Xavier Guimard committed
244 245 246 247

        # Ignore signature verification
        $self->disableSignatureVerification($login);

Xavier Guimard's avatar
Xavier Guimard committed
248
        if ($request) {
Xavier Guimard's avatar
Xavier Guimard committed
249 250 251 252 253
            $req->data->{_proxiedSamlRequest} = $login->request();
            $req->data->{_proxiedRequest}     = $request;
            $req->data->{_proxiedMethod}      = $method;
            $req->data->{_proxiedRelayState}  = $relaystate,
              $req->data->{_proxiedArtifact}  = $artifact;
Xavier Guimard's avatar
Xavier Guimard committed
254 255
        }

Xavier Guimard's avatar
Xavier Guimard committed
256 257 258 259
        # Process the request or use IDP initiated mode
        if ( $request or $idp_initiated ) {

            # Load Session and Identity if they exist
260 261
            my $session  = $req->{sessionInfo}->{ $self->lsDump };
            my $identity = $req->{sessionInfo}->{ $self->liDump };
Xavier Guimard's avatar
Xavier Guimard committed
262 263 264

            if ($session) {
                unless ( $self->setSessionFromDump( $login, $session ) ) {
265
                    $self->logger->error("Unable to load Lasso Session");
Xavier Guimard's avatar
Xavier Guimard committed
266 267
                    return PE_SAML_SSO_ERROR;
                }
268
                $self->logger->debug("Lasso Session loaded");
Xavier Guimard's avatar
Xavier Guimard committed
269 270 271 272
            }

            if ($identity) {
                unless ( $self->setIdentityFromDump( $login, $identity ) ) {
273
                    $self->logger->error("Unable to load Lasso Identity");
Xavier Guimard's avatar
Xavier Guimard committed
274 275
                    return PE_SAML_SSO_ERROR;
                }
276
                $self->logger->debug("Lasso Identity loaded");
Xavier Guimard's avatar
Xavier Guimard committed
277 278 279 280 281 282 283 284 285
            }

            my $result;

            # Create fake request if IDP initiated mode
            if ($idp_initiated) {

                # Need sp or spConfKey parameter
                unless ( $idp_initiated_sp or $idp_initiated_spConfKey ) {
286 287
                    $self->userLogger->warn(
"sp or spConfKey parameter needed to make IDP initiated SSO"
Xavier Guimard's avatar
Xavier Guimard committed
288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305
                    );
                    return PE_SAML_SSO_ERROR;
                }

                unless ($idp_initiated_sp) {

                    # Get SP from spConfKey
                    foreach ( keys %{ $self->spList } ) {
                        if ( $self->spList->{$_}->{confKey} eq
                            $idp_initiated_spConfKey )
                        {
                            $idp_initiated_sp = $_;
                            last;
                        }
                    }
                }
                else {
                    unless ( defined $self->spList->{$idp_initiated_sp} ) {
306 307
                        $self->userLogger->error(
                            "SP $idp_initiated_sp not known");
Xavier Guimard's avatar
Xavier Guimard committed
308 309 310 311 312 313 314 315 316 317 318
                        return PE_SAML_UNKNOWN_ENTITY;
                    }
                    $idp_initiated_spConfKey =
                      $self->spList->{$idp_initiated_sp}->{confKey};
                }

                # Check if IDP Initiated SSO is allowed
                unless ( $self->conf->{samlSPMetaDataOptions}
                    ->{$idp_initiated_spConfKey}
                    ->{samlSPMetaDataOptionsEnableIDPInitiatedURL} )
                {
319 320
                    $self->userLogger->error(
"IDP Initiated SSO not allowed for SP $idp_initiated_spConfKey"
Xavier Guimard's avatar
Xavier Guimard committed
321 322 323 324 325 326 327 328
                    );
                    return PE_SAML_SSO_ERROR;
                }

                $result =
                  $self->initIdpInitiatedAuthnRequest( $login,
                    $idp_initiated_sp );
                unless ($result) {
329 330
                    $self->logger->error(
                        "SSO: Fail to init IDP Initiated authentication request"
Xavier Guimard's avatar
Xavier Guimard committed
331 332 333
                    );
                    return PE_SAML_SSO_ERROR;
                }
334 335 336 337 338 339

                # Force NameID Format
                my $nameIDFormatKey =
                  $self->conf->{samlSPMetaDataOptions}
                  ->{$idp_initiated_spConfKey}
                  ->{samlSPMetaDataOptionsNameIDFormat} || "email";
340 341 342 343
                eval {
                    $login->request()->NameIDPolicy()
                      ->Format( $self->getNameIDFormat($nameIDFormatKey) );
                };
344 345

                # Force AllowCreate to TRUE
346
                eval { $login->request()->NameIDPolicy()->AllowCreate(1); };
Xavier Guimard's avatar
Xavier Guimard committed
347 348 349 350 351 352 353 354 355 356 357
            }

            # Process authentication request
            if ($artifact) {
                $result = $self->processArtResponseMsg( $login, $request );
            }
            else {
                $result = $self->processAuthnRequestMsg( $login, $request );
            }

            unless ($result) {
358 359
                $self->logger->error(
                    "SSO: Fail to process authentication request");
Xavier Guimard's avatar
Xavier Guimard committed
360 361 362 363 364 365
                return PE_SAML_SSO_ERROR;
            }

            # Get SP entityID
            my $sp = $request ? $login->remote_providerID() : $idp_initiated_sp;

366
            $self->logger->debug("Found entityID $sp in SAML message");
367
            $req->env->{llng_saml_sp} = $sp;
Xavier Guimard's avatar
Xavier Guimard committed
368 369 370 371 372

            # SP conf key
            my $spConfKey = $self->spList->{$sp}->{confKey};

            unless ($spConfKey) {
373 374
                $self->userLogger->error(
                    "$sp do not match any SP in configuration");
Xavier Guimard's avatar
Xavier Guimard committed
375 376 377
                return PE_SAML_UNKNOWN_ENTITY;
            }

378
            $self->logger->debug("$sp match $spConfKey SP in configuration");
379
            $req->env->{llng_saml_spconfkey} = $spConfKey;
Xavier Guimard's avatar
Xavier Guimard committed
380

Xavier Guimard's avatar
Xavier Guimard committed
381
            if ( my $rule = $self->spRules->{$sp} ) {
382
                unless ( $rule->( $req, $req->sessionInfo ) ) {
Xavier Guimard's avatar
Xavier Guimard committed
383 384 385 386 387
                    $self->userLogger->warn( 'User '
                          . $req->sessionInfo->{ $self->conf->{whatToTrace} }
                          . "was not authorizated to access to $sp" );
                    return PE_UNAUTHORIZEDPARTNER;
                }
Xavier Guimard's avatar
Xavier Guimard committed
388 389
            }

Xavier Guimard's avatar
Xavier Guimard committed
390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406
            # Do we check signature?
            my $checkSSOMessageSignature =
              $self->conf->{samlSPMetaDataOptions}->{$spConfKey}
              ->{samlSPMetaDataOptionsCheckSSOMessageSignature};

            if ($checkSSOMessageSignature) {

                $self->forceSignatureVerification($login);

                if ($artifact) {
                    $result = $self->processArtResponseMsg( $login, $request );
                }
                else {
                    $result = $self->processAuthnRequestMsg( $login, $request );
                }

                unless ($result) {
407
                    $self->logger->error("Signature is not valid");
Xavier Guimard's avatar
Xavier Guimard committed
408 409 410
                    return PE_SAML_SIGNATURE_ERROR;
                }
                else {
411
                    $self->logger->debug("Signature is valid");
Xavier Guimard's avatar
Xavier Guimard committed
412 413 414
                }
            }
            else {
415
                $self->logger->debug("Message signature will not be checked");
Xavier Guimard's avatar
Xavier Guimard committed
416 417
            }

418 419 420 421 422 423 424 425 426 427 428 429
            # Force AllowCreate to TRUE for transient/persistent NameIDPolicy
            if ( $login->request()->NameIDPolicy ) {
                my $nif = $login->request()->NameIDPolicy->Format();
                if (   $nif eq $self->getNameIDFormat("transient")
                    or $nif eq $self->getNameIDFormat("persistent") )
                {
                    $self->logger->debug(
                        "Force AllowCreate flag in NameIDPolicy");
                    eval { $login->request()->NameIDPolicy()->AllowCreate(1); };
                }
            }

Xavier Guimard's avatar
Xavier Guimard committed
430 431
            # Validate request
            unless ( $self->validateRequestMsg( $login, 1, 1 ) ) {
432
                $self->logger->error("Unable to validate SSO request message");
Xavier Guimard's avatar
Xavier Guimard committed
433 434 435
                return PE_SAML_SSO_ERROR;
            }

436
            $self->logger->debug("SSO: authentication request is valid");
Xavier Guimard's avatar
Xavier Guimard committed
437 438 439 440 441 442

            # Get ForceAuthn flag
            my $force_authn;

            eval { $force_authn = $login->request()->ForceAuthn(); };
            if ($@) {
443 444
                $self->logger->warn(
                    "Unable to get ForceAuthn flag, set it to false");
Xavier Guimard's avatar
Xavier Guimard committed
445 446 447
                $force_authn = 0;
            }

448 449
            $self->logger->debug(
                "Found ForceAuthn flag with value $force_authn");
Xavier Guimard's avatar
Xavier Guimard committed
450

451
            # Force authentication if flag is on, or previous flag still active
Xavier Guimard's avatar
Xavier Guimard committed
452
            if (
453 454 455 456
                $force_authn
                and (
                    time - $req->sessionInfo->{_utime} >
                    $self->conf->{portalForceAuthnInterval} )
Xavier Guimard's avatar
Xavier Guimard committed
457 458 459
              )
            {

460 461 462
                $self->userLogger->info(
                    "SAML SP $sp ask to refresh session of "
                      . $req->sessionInfo->{ $self->conf->{whatToTrace} } );
Xavier Guimard's avatar
Xavier Guimard committed
463 464

                # Replay authentication process
465 466
                return $self->reAuth($req);
            }
Xavier Guimard's avatar
Xavier Guimard committed
467 468

            # Check Destination (only in non proxy mode)
Xavier Guimard's avatar
Xavier Guimard committed
469
            unless ( $req->data->{_proxiedRequest} ) {
Xavier Guimard's avatar
Xavier Guimard committed
470 471 472 473 474 475 476 477 478 479 480
                return PE_SAML_DESTINATION_ERROR
                  unless ( $self->checkDestination( $login->request, $url ) );
            }

            # Map authenticationLevel with SAML2 authentication context
            my $authenticationLevel =
              $req->{sessionInfo}->{authenticationLevel};

            $authn_context =
              $self->authnLevel2authnContext($authenticationLevel);

481
            $self->logger->debug("Authentication context is $authn_context");
Xavier Guimard's avatar
Xavier Guimard committed
482 483 484 485 486 487 488 489 490

            # Get SP options notOnOrAfterTimeout
            my $notOnOrAfterTimeout =
              $self->conf->{samlSPMetaDataOptions}->{$spConfKey}
              ->{samlSPMetaDataOptionsNotOnOrAfterTimeout};

            # Build Assertion
            unless (
                $self->buildAssertion(
Xavier Guimard's avatar
Xavier Guimard committed
491
                    $req, $login, $authn_context, $notOnOrAfterTimeout
Xavier Guimard's avatar
Xavier Guimard committed
492 493 494
                )
              )
            {
495
                $self->logger->error("Unable to build assertion");
Xavier Guimard's avatar
Xavier Guimard committed
496 497 498
                return PE_SAML_SSO_ERROR;
            }

499
            $self->logger->debug("SSO: assertion is built");
Xavier Guimard's avatar
Xavier Guimard committed
500 501 502 503 504 505 506 507 508 509 510

            # Get default NameID Format from configuration
            # Set to "email" if no value in configuration
            my $nameIDFormatKey =
              $self->conf->{samlSPMetaDataOptions}->{$spConfKey}
              ->{samlSPMetaDataOptionsNameIDFormat} || "email";
            my $nameIDFormat;

            # Check NameID Policy in request
            if ( $login->request()->NameIDPolicy ) {
                $nameIDFormat = $login->request()->NameIDPolicy->Format();
511 512
                $self->logger->debug(
                    "Get NameID format $nameIDFormat from request");
Xavier Guimard's avatar
Xavier Guimard committed
513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547
            }

            # NameID unspecified is forced to default NameID format
            if (  !$nameIDFormat
                or $nameIDFormat eq $self->getNameIDFormat("unspecified") )
            {
                $nameIDFormat = $self->getNameIDFormat($nameIDFormatKey);
            }

            # Get session key associated with NameIDFormat
            # Not for unspecified, transient, persistent, entity, encrypted
            my $nameIDFormatConfiguration = {
                $self->getNameIDFormat("email") => 'samlNameIDFormatMapEmail',
                $self->getNameIDFormat("x509")  => 'samlNameIDFormatMapX509',
                $self->getNameIDFormat("windows") =>
                  'samlNameIDFormatMapWindows',
                $self->getNameIDFormat("kerberos") =>
                  'samlNameIDFormatMapKerberos',
            };

            my $nameIDSessionKey =
              $self->conf->{ $nameIDFormatConfiguration->{$nameIDFormat} };

            # Override default NameID Mapping
            if ( $self->conf->{samlSPMetaDataOptions}->{$spConfKey}
                ->{samlSPMetaDataOptionsNameIDSessionKey} )
            {
                $nameIDSessionKey =
                  $self->conf->{samlSPMetaDataOptions}->{$spConfKey}
                  ->{samlSPMetaDataOptionsNameIDSessionKey};
            }

            my $nameIDContent;
            if ( defined $req->{sessionInfo}->{$nameIDSessionKey} ) {
                $nameIDContent =
Xavier Guimard's avatar
Xavier Guimard committed
548
                  $self->p->getFirstValue(
Xavier Guimard's avatar
Xavier Guimard committed
549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577
                    $req->{sessionInfo}->{$nameIDSessionKey} );
            }

            # Manage Entity NameID format
            if ( $nameIDFormat eq $self->getNameIDFormat("entity") ) {
                $nameIDContent = $self->getMetaDataURL( "samlEntityID", 0, 1 );
            }

            # Manage Transient NameID format
            if ( $nameIDFormat eq $self->getNameIDFormat("transient") ) {
                eval {
                    my @assert = $login->response->Assertion;
                    $nameIDContent = $assert[0]->Subject->NameID->content;
                };
            }

            if ( $login->nameIdentifier ) {
                $login->nameIdentifier->Format($nameIDFormat);
                $login->nameIdentifier->content($nameIDContent)
                  if $nameIDContent;
            }
            else {
                my $nameIdentifier = Lasso::Saml2NameID->new();
                $nameIdentifier->Format($nameIDFormat);
                $nameIdentifier->content($nameIDContent)
                  if $nameIDContent;
                $login->nameIdentifier($nameIdentifier);
            }

578 579 580 581
            $self->logger->debug(
                "NameID Format is " . $login->nameIdentifier->Format );
            $self->logger->debug(
                "NameID Content is " . $login->nameIdentifier->content );
Xavier Guimard's avatar
Xavier Guimard committed
582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604

            # Push mandatory attributes
            my @attributes;

            foreach (
                keys %{
                    $self->conf->{samlSPMetaDataExportedAttributes}
                      ->{$spConfKey}
                }
              )
            {

                # Extract fields from exportedAttr value
                my ( $mandatory, $name, $format, $friendly_name ) =
                  split( /;/,
                    $self->conf->{samlSPMetaDataExportedAttributes}
                      ->{$spConfKey}->{$_} );

                # Name is required
                next unless $name;

                # Do not send attribute if not mandatory
                unless ($mandatory) {
605 606
                    $self->logger->debug(
                        "SAML2 attribute $name is not mandatory");
Xavier Guimard's avatar
Xavier Guimard committed
607 608 609 610 611 612
                    next;
                }

                # Error if corresponding attribute is not in user session
                my $value = $req->{sessionInfo}->{$_};
                unless ( defined $value ) {
613 614
                    $self->logger->warn(
                        "Session key $_ is required to set SAML $name attribute"
Xavier Guimard's avatar
Xavier Guimard committed
615 616 617 618
                    );
                    return PE_SAML_SSO_ERROR;
                }

619 620
                $self->logger->debug(
                    "SAML2 attribute $name will be set with $_ session key");
Xavier Guimard's avatar
Xavier Guimard committed
621 622 623 624 625 626

                # SAML2 attribute
                my $attribute =
                  $self->createAttribute( $name, $format, $friendly_name );

                unless ($attribute) {
627 628
                    $self->logger->error(
                        "Unable to create a new SAML attribute");
Xavier Guimard's avatar
Xavier Guimard committed
629 630 631 632 633 634 635 636 637 638 639 640 641 642 643
                    return PE_SAML_SSO_ERROR;
                }

                # Set attribute value(s)
                my @values = split $self->conf->{multiValuesSeparator}, $value;
                my @saml2values;

                foreach (@values) {

                    # SAML2 attribute value
                    my $saml2value = $self->createAttributeValue( $_,
                        $self->conf->{samlSPMetaDataOptions}->{$spConfKey}
                          ->{samlSPMetaDataOptionsForceUTF8} );

                    unless ($saml2value) {
644 645
                        $self->logger->error(
                            "Unable to create a new SAML attribute value");
Xavier Guimard's avatar
Xavier Guimard committed
646 647 648 649 650 651
                        $self->checkLassoError($@);
                        return PE_SAML_SSO_ERROR;
                    }

                    push @saml2values, $saml2value;

652
                    $self->logger->debug("Push $_ in SAML attribute $name");
Xavier Guimard's avatar
Xavier Guimard committed
653 654 655 656 657 658 659 660 661 662 663 664 665 666

                }

                $attribute->AttributeValue(@saml2values);

                # Push attribute in attribute list
                push @attributes, $attribute;

            }

            # Get response assertion
            my @response_assertions = $login->response->Assertion;

            unless ( $response_assertions[0] ) {
667
                $self->logger->error("Unable to get response assertion");
Xavier Guimard's avatar
Xavier Guimard committed
668 669 670
                return PE_SAML_SSO_ERROR;
            }

671 672 673 674 675 676 677 678 679 680
            # Rewrite Issuer with domain
            if ($domain) {
                my $original_issuer = $login->response->Issuer->content;
                $self->logger->debug(
                    "Add domain $domain to Issuer $original_issuer");
                my $new_issuer = $original_issuer . "?domain=$domain";
                $login->response->Issuer->content($new_issuer);
                $login->response->Assertion->Issuer->content($new_issuer);
            }

Xavier Guimard's avatar
Xavier Guimard committed
681 682 683 684 685
            # Set subject NameID
            $response_assertions[0]
              ->set_subject_name_id( $login->nameIdentifier );

            # Set basic conditions
Xavier Guimard's avatar
Xavier Guimard committed
686 687
            my $oneTimeUse = $self->conf->{samlSPMetaDataOptions}->{$spConfKey}
              ->{samlSPMetaDataOptionsOneTimeUse} // 0;
Xavier Guimard's avatar
Xavier Guimard committed
688 689 690 691 692 693 694 695

            my $conditionNotOnOrAfter = $notOnOrAfterTimeout || "86400";
            eval {
                $response_assertions[0]
                  ->set_basic_conditions( 60, $conditionNotOnOrAfter,
                    $oneTimeUse );
            };
            if ($@) {
696
                $self->logger->debug("Basic conditions not set: $@");
Xavier Guimard's avatar
Xavier Guimard committed
697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725
            }

            # Create attribute statement
            if ( scalar @attributes ) {

                my $attribute_statement;

                eval {
                    $attribute_statement =
                      Lasso::Saml2AttributeStatement->new();
                };
                if ($@) {
                    $self->checkLassoError($@);
                    return PE_SAML_SSO_ERROR;
                }

                # Register attributes in attribute statement
                $attribute_statement->Attribute(@attributes);

                # Add attribute statement in response assertion
                my @attributes_statement = ($attribute_statement);
                $response_assertions[0]
                  ->AttributeStatement(@attributes_statement);
            }

            # Get AuthnStatement
            my @authn_statements = $response_assertions[0]->AuthnStatement();

            # Set sessionIndex
726 727 728 729 730 731 732
            my $sessionIndexSession = $self->getSamlSession();
            return PE_SAML_SESSION_ERROR unless $sessionIndexSession;

            $sessionIndexSession->update(
                { '_utime' => time, '_saml_id' => $session_id } );
            my $sessionIndex = $sessionIndexSession->id;

Xavier Guimard's avatar
Xavier Guimard committed
733 734
            $authn_statements[0]->SessionIndex($sessionIndex);

735
            $self->logger->debug(
736 737
                "Set sessionIndex $sessionIndex (linked to session $session_id)"
            );
Xavier Guimard's avatar
Xavier Guimard committed
738 739 740 741 742 743 744 745 746 747

            # Set SessionNotOnOrAfter
            my $sessionNotOnOrAfterTimeout =
              $self->conf->{samlSPMetaDataOptions}->{$spConfKey}
              ->{samlSPMetaDataOptionsSessionNotOnOrAfterTimeout};
            $sessionNotOnOrAfterTimeout ||= $self->conf->{timeout};
            my $timeout             = $time + $sessionNotOnOrAfterTimeout;
            my $sessionNotOnOrAfter = $self->timestamp2samldate($timeout);
            $authn_statements[0]->SessionNotOnOrAfter($sessionNotOnOrAfter);

748 749
            $self->logger->debug(
                "Set sessionNotOnOrAfter $sessionNotOnOrAfter");
Xavier Guimard's avatar
Xavier Guimard committed
750 751 752 753 754 755 756 757 758 759

            # Register AuthnStatement in assertion
            $response_assertions[0]->AuthnStatement(@authn_statements);

            # Set response assertion
            $login->response->Assertion(@response_assertions);

            # Signature
            my $signSSOMessage =
              $self->conf->{samlSPMetaDataOptions}->{$spConfKey}
Xavier Guimard's avatar
Xavier Guimard committed
760
              ->{samlSPMetaDataOptionsSignSSOMessage} // -1;
Xavier Guimard's avatar
Xavier Guimard committed
761 762

            if ( $signSSOMessage == 0 ) {
763
                $self->logger->debug("SSO response will not be signed");
Xavier Guimard's avatar
Xavier Guimard committed
764 765 766
                $self->disableSignature($login);
            }
            elsif ( $signSSOMessage == 1 ) {
767
                $self->logger->debug("SSO response will be signed");
Xavier Guimard's avatar
Xavier Guimard committed
768 769 770
                $self->forceSignature($login);
            }
            else {
771 772
                $self->logger->debug(
                    "SSO response signature according to metadata");
Xavier Guimard's avatar
Xavier Guimard committed
773 774 775
            }

            # log that a SAML authn response is build
Xavier Guimard's avatar
Xavier Guimard committed
776 777
            my $user      = $req->{sessionInfo}->{ $self->conf->{whatToTrace} };
            my $nameIDLog = '';
Xavier Guimard's avatar
Xavier Guimard committed
778 779 780 781 782 783 784 785 786
            foreach my $format (qw(persistent transient)) {
                if ( $login->nameIdentifier->Format eq
                    $self->getNameIDFormat($format) )
                {
                    $nameIDLog =
                      " with $format NameID " . $login->nameIdentifier->content;
                    last;
                }
            }
787
            $self->userLogger->notice(
Xavier Guimard's avatar
Xavier Guimard committed
788 789 790 791 792 793 794
"SAML authentication response sent to SAML SP $spConfKey for $user$nameIDLog"
            );

            # Build SAML response
            $protocolProfile = $login->protocolProfile();

            # Artifact
Xavier Guimard's avatar
Xavier Guimard committed
795
            # Choose method
796 797 798 799 800
            if (   $artifact
                or $protocolProfile ==
                Lasso::Constants::LOGIN_PROTOCOL_PROFILE_BRWS_ART )
            {
                $artifact = 1;
Xavier Guimard's avatar
Xavier Guimard committed
801 802 803
                if (   $method == $self->getHttpMethod("post")
                    || $method == $self->getHttpMethod("artifact-post") )
                {
Xavier Guimard's avatar
Xavier Guimard committed
804
                    $artifact_method = $self->getHttpMethod("artifact-post");
Xavier Guimard's avatar
Xavier Guimard committed
805 806 807 808 809 810 811

                }
                else {
                    $artifact_method = $self->getHttpMethod("artifact-get");
                }
            }

Xavier Guimard's avatar
Xavier Guimard committed
812 813 814 815 816 817
            if ( $protocolProfile ==
                Lasso::Constants::LOGIN_PROTOCOL_PROFILE_BRWS_ART )
            {

                # Build artifact message
                unless ( $self->buildArtifactMsg( $login, $artifact_method ) ) {
818 819
                    $self->logger->error(
                        "Unable to build SSO artifact response message");
Xavier Guimard's avatar
Xavier Guimard committed
820 821 822
                    return PE_SAML_ART_ERROR;
                }

823
                $self->logger->debug("SSO: artifact response is built");
Xavier Guimard's avatar
Xavier Guimard committed
824 825 826 827 828 829 830 831 832 833 834 835 836

                # 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,
                    $session_id );
            }

            # No artifact
            else {

                unless ( $self->buildAuthnResponseMsg($login) ) {
837 838
                    $self->logger->error(
                        "Unable to build SSO response message");
Xavier Guimard's avatar
Xavier Guimard committed
839 840 841
                    return PE_SAML_SSO_ERROR;
                }

842
                $self->logger->debug("SSO: authentication response is built");
Xavier Guimard's avatar
Xavier Guimard committed
843 844 845 846 847 848 849

            }

            # Save Identity and Session
            if ( $login->is_identity_dirty ) {

                # Update session
850
                $self->logger->debug("Save Lasso identity in session");
851
                $self->p->updatePersistentSession( $req,
852
                    { $self->liDump => $login->get_identity->dump },
Xavier Guimard's avatar
Xavier Guimard committed
853 854 855 856
                    undef, $session_id );
            }

            if ( $login->is_session_dirty ) {
857
                $self->logger->debug("Save Lasso session in session");
Xavier Guimard's avatar
Xavier Guimard committed
858
                $self->p->updateSession( $req,
859
                    { $self->lsDump => $login->get_session->dump },
Xavier Guimard's avatar
Xavier Guimard committed
860 861 862 863 864 865
                    $session_id );
            }

            # Keep SAML elements for later queries
            my $nameid = $login->nameIdentifier;

866
            $self->logger->debug( "Store NameID "
Xavier Guimard's avatar
Xavier Guimard committed
867
                  . $nameid->dump
868
                  . " and SessionIndex $sessionIndex for session $session_id" );
Xavier Guimard's avatar
Xavier Guimard committed
869 870 871 872 873 874 875 876 877

            my $infos;

            $infos->{type}          = 'saml';           # Session type
            $infos->{_utime}        = $time;            # Creation time
            $infos->{_saml_id}      = $session_id;      # SSO session id
            $infos->{_nameID}       = $nameid->dump;    # SAML NameID
            $infos->{_sessionIndex} = $sessionIndex;    # SAML SessionIndex

878 879 880
            my $samlSessionInfo = $self->getSamlSession( undef, $infos );

            return PE_SAML_SESSION_ERROR unless $samlSessionInfo;
Xavier Guimard's avatar
Xavier Guimard committed
881 882 883

            my $saml_session_id = $samlSessionInfo->id;

884 885
            $self->logger->debug(
                "Link session $session_id to SAML session $saml_session_id");
Xavier Guimard's avatar
Xavier Guimard committed
886 887 888 889 890 891 892 893 894

            # Send SSO Response

            # Register IDP in Common Domain Cookie if needed
            if (    $self->conf->{samlCommonDomainCookieActivation}
                and $self->conf->{samlCommonDomainCookieWriter} )
            {
                my $cdc_idp = $self->getMetaDataURL( "samlEntityID", 0, 1 );

895 896
                $self->logger->debug(
                    "Will register IDP $cdc_idp in Common Domain Cookie");
Xavier Guimard's avatar
Xavier Guimard committed
897 898 899 900 901 902 903 904 905 906

                # Redirection to CDC Writer page in a hidden iframe
                my $cdc_writer_url =
                  $self->conf->{samlCommonDomainCookieWriter};
                $cdc_writer_url .= (
                    $self->conf->{samlCommonDomainCookieWriter} =~ /\?/
                    ? '&idp=' . $cdc_idp
                    : '?url=' . $cdc_idp
                );

Xavier Guimard's avatar
Xavier Guimard committed
907 908
                my $cdc_iframe =
                    qq'<iframe src="$cdc_writer_url"'
909 910 911
                  . ' alt="Common Dommain Cookie" marginwidth="0"'
                  . ' marginheight="0" scrolling="no" class="hiddenFrame"'
                  . ' width="0" height="0" frameborder="0"></iframe>';
Xavier Guimard's avatar
Xavier Guimard committed
912

Xavier Guimard's avatar
Xavier Guimard committed
913
                $req->info(
914 915 916 917
                    $self->loadTemplate( 'simpleInfo',
                        params => { trspan => 'updateCdc' } )
                      . $cdc_iframe
                );
Xavier Guimard's avatar
Xavier Guimard committed
918 919
            }

Xavier Guimard's avatar
Xavier Guimard committed
920
            # HTTP-POST
Xavier Guimard's avatar
Xavier Guimard committed
921
            if ( (
Xavier Guimard's avatar
Xavier Guimard committed
922 923 924 925 926
                       !$artifact
                    and $protocolProfile eq
                    Lasso::Constants::LOGIN_PROTOCOL_PROFILE_BRWS_POST
                )
                or (    $artifact
927
                    and $artifact_method ==
Xavier Guimard's avatar
Xavier Guimard committed
928
                    $self->getHttpMethod("artifact-post") )
929
              )
Xavier Guimard's avatar
Xavier Guimard committed
930 931 932 933 934 935 936 937
            {

                # Use autosubmit form
                my $sso_url  = $login->msg_url;
                my $sso_body = $login->msg_body;

                $req->postUrl($sso_url);

938 939 940
                if (    $artifact_method
                    and $artifact_method ==
                    $self->getHttpMethod("artifact-post") )
Xavier Guimard's avatar
Xavier Guimard committed
941 942 943 944 945 946 947 948 949 950 951
                {
                    $req->{postFields} = { 'SAMLart' => $sso_body };
                }
                else {
                    $req->{postFields} = { 'SAMLResponse' => $sso_body };
                }

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

Xavier Guimard's avatar
Xavier Guimard committed
952 953
                $req->steps( ['autoPost'] );
                return PE_OK;
Xavier Guimard's avatar
Xavier Guimard committed
954 955
            }

Xavier Guimard's avatar
Xavier Guimard committed
956 957 958 959 960 961 962
            # HTTP-REDIRECT
            if ( $protocolProfile eq
                Lasso::Constants::LOGIN_PROTOCOL_PROFILE_REDIRECT or $artifact )
            {

                # Redirect user to response URL
                my $sso_url = $login->msg_url;
963
                $self->logger->debug("Redirect user to $sso_url");
Xavier Guimard's avatar
Xavier Guimard committed
964 965 966 967 968 969 970 971

                $req->{urldc} = $sso_url;
                $req->mustRedirect(1);
                $req->steps( [] );

                return PE_OK;
            }

Xavier Guimard's avatar
Xavier Guimard committed
972 973 974
        }

        elsif ($response) {
975 976
            $self->logger->debug(
                "Authentication responses are not managed by this module");
Xavier Guimard's avatar
Xavier Guimard committed
977 978 979 980 981 982 983
            return PE_OK;
        }

        else {

            # No request or response
            # This should not happen
984
            $self->logger->debug("No request or response found");
Xavier Guimard's avatar
Xavier Guimard committed
985 986 987 988
            return PE_OK;
        }

    }
989
    $self->logger->debug("Not an issuer request $url");
Xavier Guimard's avatar
Xavier Guimard committed
990 991 992
    return PE_OK;
}

993 994
sub artifactServer {
    my ( $self, $req ) = @_;
995 996 997
    $self->logger->debug( "URL "
          . $req->uri
          . " detected as an artifact resolution service URL" );
998 999

    # Artifact request are sent with SOAP trough POST
1000
    my $art_request = $req->content;
1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017
    my $art_response;

    # Create Login object
    my $login = $self->createLogin( $self->lassoServer );

    # Process request message
    unless ( $self->processArtRequestMsg( $login, $art_request ) ) {
        return $self->p->sendError( $req,
            'Unable to process artifact request message', 400 );
    }

    # Check Destination
    unless ( $self->checkDestination( $login->request, $req->uri ) ) {
        return $self->p->sendError( $req, 'Bad request', 400 );
    }

    # Create artifact response
Xavier Guimard's avatar
Xavier Guimard committed
1018
    unless ( $art_response = $self->createArtifactResponse( $req, $login ) ) {
1019 1020 1021 1022 1023 1024 1025
        return $self->p->sendError( $req,
            "Unable to create artifact response message", 400 );
    }

    $self->{SOAPMessage} = $art_response;

    # Return SOAP message
1026
    $self->logger->debug("Send SOAP Message: $art_response");
1027 1028 1029 1030 1031 1032 1033 1034 1035 1036
    return [
        200,
        [
            'Content-Type'   => 'application/xml',
            'Content-Length' => length($art_response)
        ],
        [$art_response]
    ];
}

1037 1038 1039 1040
sub soapSloServer {
    my ( $self, $req ) = @_;
    my $url            = $req->uri;
    my $request_method = $req->param('issuerMethod') || $req->method;
Xavier Guimard's avatar
Xavier Guimard committed
1041
    my $content_type   = $req->content_type();
1042

1043
    $self->logger->debug("URL $url detected as an SLO URL");
1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063

    # Check SAML Message
    my ( $request, $response, $method, $relaystate, $artifact ) =
      $self->checkMessage( $req, $url, $request_method, $content_type,
        "logout" );

    # Create Logout object
    my $logout = $self->createLogout( $self->lassoServer );

    # Ignore signature verification
    $self->disableSignatureVerification($logout);

    if ($request) {

        # Process logout request
        unless ( $self->processLogoutRequestMsg( $logout, $request ) ) {
            return $self->p->sendError( $req,
                "SLO: Fail to process logout request", 400 );
        }

1064
        $self->logger->debug("SLO: Logout request is valid");
1065 1066 1067 1068 1069 1070 1071 1072 1073 1074

        # We accept only SOAP here
        unless ( $method eq $self->getHttpMethod('soap') ) {
            return $self->p->sendError( $req,
                "Only SOAP requests allowed here", 400 );
        }

        # Get SP entityID
        my $sp = $logout->remote_providerID();

1075
        $self->logger->debug("Found entityID $sp in SAML message");
1076 1077 1078 1079 1080

        # SP conf key
        my $spConfKey = $self->spList->{$sp}->{confKey};

        unless ($spConfKey) {
1081
            return $self->p->sendError( $req,
1082 1083 1084
                "$sp do not match any SP in configuration", 400 );
        }

1085
        $self->logger->debug("$sp match $spConfKey SP in configuration");
1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100

        # Do we check signature?
        my $checkSLOMessageSignature =
          $self->conf->{samlSPMetaDataOptions}->{$spConfKey}
          ->{samlSPMetaDataOptionsCheckSLOMessageSignature};

        if ($checkSLOMessageSignature) {

            $self->forceSignatureVerification($logout);

            unless ( $self->processLogoutRequestMsg( $logout, $request ) ) {
                return $self->p->sendError( $req, "Signature is not valid",
                    400 );
            }
            else {
1101
                $self->logger->debug("Signature is valid");
1102 1103 1104
            }
        }
        else {
1105
            $self->logger->debug("Message signature will not be checked");
1106 1107 1108 1109 1110 1111 1112 1113 1114 1115 1116 1117 1118 1119 1120 1121
        }

        # Get SAML request
        my $saml_request = $logout->request();
        unless ($saml_request) {
            return $self->p->sendError( $req, "No SAML request found", 400 );
        }

        # Check Destination
        return $self->sendSLOSoapErrorResponse( $req, $logout, $method )
          unless ( $self->checkDestination( $saml_request, $url ) );

        # Get session index
        my $session_index;
        eval { $session_index = $logout->request()->SessionIndex; };

1122
        # SLO requests without session index are not accepted in SOAP mode
1123 1124 1125 1126 1127
        unless ( defined $session_index ) {
            $self->p->sendError( $req,
                "No session index in SLO request from $spConfKey SP", 400 );
        }

1128 1129
        # Get session index
        my $sessionIndexSession = $self->getSamlSession($session_index);
1130 1131
        return $self->p->sendError( $req, 'SAML session not found', 400 )
          unless $sessionIndexSession;
1132 1133 1134 1135

        my $local_session_id = $sessionIndexSession->data->{_saml_id};

        $sessionIndexSession->remove;
1136

1137
        $self->logger->debug(
1138 1139
"Get session id $local_session_id (from session index $session_index)"
        );
1140 1141

        # Open local session
Xavier Guimard's avatar
Xavier Guimard committed
1142
        my $local_session = $self->p->getApacheSession($local_session_id);
1143 1144 1145 1146 1147 1148

        unless ($local_session) {
            return $self->p->sendError( $req, "No local session found", 400 );
        }

        # Load Session and Identity if they exist
1149 1150
        my $session  = $local_session->data->{ $self->lsDump };
        my $identity = $local_session->data->{ $self->liDump };
1151 1152 1153

        if ($session) {
            unless ( $self->setSessionFromDump( $logout, $session ) ) {
1154 1155
                return $self->p->sendError( $req,
                    "Unable to load Lasso Session", 400 );
1156
            }
1157
            $self->logger->debug("Lasso Session loaded");
1158 1159 1160 1161
        }

        if ($identity) {
            unless ( $self->setIdentityFromDump( $logout, $identity ) ) {
1162 1163
                return $self->p->sendError( $req,
                    "Unable to load Lasso Identity", 400 );
1164
            }
1165
            $self->logger->debug("Lasso Identity loaded");
1166 1167 1168 1169
        }

        # Close SAML sessions
        unless ( $self->deleteSAMLSecondarySessions($local_session_id) ) {
1170
            return $self->p->sendError( $req, "Fail to delete SAML sessions",
1171 1172 1173 1174 1175
                400 );
        }

        # Close local session
        unless ( $self->p->_deleteSession( $req, $local_session ) ) {
1176
            return $self->p->sendError( $req,
1177 1178 1179 1180 1181 1182 1183 1184 1185 1186 1187 1188 1189 1190 1191 1192 1193 1194
                "Fail to delete session $local_session_id", 400 );
        }

        # Validate request if no previous error
        unless ( $self->validateLogoutRequest($logout) ) {
            return $self->p->sendError( $req, "SLO request is not valid", 400 );
        }

        # Try to send SLO request trough SOAP
        $self->resetProviderIdIndex($logout);
        while ( my $providerID = $self->getNextProviderId($logout) ) {

            # Send logout request
            my ( $rstatus, $rmethod, $rinfo ) =
              $self->sendLogoutRequestToProvider( $logout, $providerID,
                $self->getHttpMethod('soap'), 0 );

            if ($rstatus) {
1195
                $self->logger->debug("SOAP SLO successful on $providerID");
1196 1197
            }
            else {
1198
                $self->logger->debug("SOAP SLO error on $providerID");
1199 1200 1201 1202 1203 1204
            }
        }

        # Set RelayState
        if ($relaystate) {
            $logout->msg_relayState($relaystate);
1205
            $self->logger->debug("Set $relaystate in RelayState");
1206 1207 1208
        }

        # Signature
1209
        my $signSLOMessage = $self->{samlSPMetaDataOptions}->{$spConfKey}
Xavier Guimard's avatar
Xavier Guimard committed
1210
          ->{samlSPMetaDataOptionsSignSLOMessage} // 0;
1211 1212

        if ( $signSLOMessage == 0 ) {
1213
            $self->logger->debug("SLO response will not be signed");
1214 1215 1216
            $self->disableSignature($logout);
        }
        elsif ( $signSLOMessage == 1 ) {
1217
            $self->logger->debug("SLO response will be signed");
1218 1219 1220
            $self->forceSignature($logout);
        }
        else {
1221 1222
            $self->logger->debug(
                "SLO response signature according to metadata");
1223 1224 1225
        }

        # Send logout response
Xavier Guimard's avatar
Xavier Guimard committed
1226
        unless ( $self->buildLogoutResponseMsg($logout) ) {
1227
            $self->logger->error("Unable to build SLO response");
Xavier Guimard's avatar
Xavier Guimard committed
1228 1229
            return $self->p->sendError( $req, 'Unable to build SLO response',
                400 );
1230
        }
Xavier Guimard's avatar
Xavier Guimard committed
1231 1232 1233 1234 1235 1236 1237 1238 1239
        my $slo_body = $logout->msg_body;
        return [
            200,
            [
                'Content-Type'   => 'application/xml',
                'Content-Length' => length($slo_body)
            ],
            [$slo_body]
        ];
1240 1241 1242 1243
    }

}

Xavier Guimard's avatar
Xavier Guimard committed
1244
sub logout {
Xavier Guimard's avatar
Xavier Guimard committed
1245
    my ( $self, $req ) = @_;
Xavier Guimard's avatar
Xavier Guimard committed
1246
    return PE_OK if ( $req->data->{samlSLOCalled} );
Xavier Guimard's avatar
Xavier Guimard committed
1247 1248 1249 1250 1251 1252

    # Session ID
    my $session_id = $req->{sessionInfo}->{_session_id} || $req->{id};

    # Close SAML sessions
    unless ( $self->deleteSAMLSecondarySessions($session_id) ) {
1253
        $self->logger->error("Fail to delete SAML sessions");
Xavier Guimard's avatar
Xavier Guimard committed
1254 1255 1256 1257 1258 1259
    }

    # Create Logout object
    my $logout = $self->createLogout( $self->lassoServer );

    # Load Session and Identity if they exist
1260 1261
    my $session  = $req->{sessionInfo}->{ $self->lsDump };
    my $identity = $req->{sessionInfo}->{ $self->liDump };
Xavier Guimard's avatar
Xavier Guimard committed
1262 1263 1264

    if ($session) {
        unless ( $self->setSessionFromDump( $logout, $session ) ) {
1265
            $self->logger->error("Unable to load Lasso Session");
Xavier Guimard's avatar
Xavier Guimard committed
1266 1267
            return PE_SAML_SLO_ERROR;
        }
1268
        $self->logger->debug("Lasso Session loaded");
Xavier Guimard's avatar
Xavier Guimard committed
1269 1270 1271 1272 1273
    }

    # No need to initiate logout requests on SP, if no SAML session is
    # available into the session.
    else {
1274
        $self->logger->debug('No SAML session available into this session');
Xavier Guimard's avatar
Xavier Guimard committed
1275 1276 1277 1278 1279
        return PE_OK;
    }

    if ($identity) {
        unless ( $self->setIdentityFromDump( $logout, $identity ) ) {
1280
            $self->logger->error("Unable to load Lasso Identity");
Xavier Guimard's avatar
Xavier Guimard committed
1281 1282
            return PE_SAML_SLO_ERROR;
        }
1283
        $self->logger->debug("Lasso Identity loaded");
Xavier Guimard's avatar
Xavier Guimard committed
1284 1285 1286 1287 1288 1289 1290
    }

    # Proceed to logout on all others SP.
    # Verify that logout response is correctly sent. If we have to wait for
    # providers during HTTP-REDIRECT process, return PE_INFO to notify to wait
    # for them.
    # Redirect on logout page when all is done.
1291
    if ( $self->sendLogoutRequestToProviders( $req, $logout ) ) {
Xavier Guimard's avatar
Xavier Guimard committed
1292
        $self->{urldc} = $req->script_name . "?logout=1";
Xavier Guimard's avatar
Xavier Guimard committed
1293
        return PE_OK;
Xavier Guimard's avatar
Xavier Guimard committed
1294 1295 1296
    }

    return PE_OK;
Xavier Guimard's avatar
Xavier Guimard committed
1297 1298
}

Xavier Guimard's avatar
Xavier Guimard committed
1299 1300
sub sloRelaySoap {
    my ( $self, $req ) = @_;
1301 1302
    $self->logger->debug(
        "URL " . $req->uri . " detected as a SOAP relay service URL" );
Xavier Guimard's avatar
Xavier Guimard committed
1303 1304 1305 1306

    # Check if relay parameter is present (mandatory)
    my $relayID;
    unless ( $relayID = $req->param('relay') ) {
1307
        $self->logger->error("No relayID detected");
Xavier Guimard's avatar
Xavier Guimard committed
1308 1309 1310 1311 1312 1313
        return $self->imgnok($req);
    }

    # Retrieve the corresponding data from samlStorage
    my $relayInfos = $self->getSamlSession($relayID);
    unless ($relayInfos) {
1314
        $self->logger->error("Could not get relay session $relayID");
Xavier Guimard's avatar
Xavier Guimard committed
1315 1316 1317
        return $self->imgnok($req);
    }

1318
    $self->logger->debug("Found relay session $relayID");
Xavier Guimard's avatar
Xavier Guimard committed
1319 1320 1321 1322

    # Rebuild the logout object
    my $logout;
    unless ( $logout = $self->createLogout( $self->lassoServer ) ) {
1323
        $self->logger->error("Could not rebuild logout object");
Xavier Guimard's avatar
Xavier Guimard committed
1324 1325 1326 1327
        return $self->imgnok($req);
    }

    # Load Session and Identity if they exist
1328 1329
    my $session    = $relayInfos->data->{ $self->lsDump };
    my $identity   = $relayInfos->data->{ $self->liDump };
Xavier Guimard's avatar
Xavier Guimard committed
1330
    my $providerID = $relayInfos->data->{_providerID};
Xavier Guimard's avatar
Xavier Guimard committed
1331
    my $relayState = $relayInfos->data->{_relayState} // '';
Xavier Guimard's avatar
Xavier Guimard committed
1332 1333 1334 1335
    my $spConfKey  = $self->spList->{$providerID}->{confKey};

    if ($session) {
        unless ( $self->setSessionFromDump( $logout, $session ) ) {
1336
            $self->logger->error("Unable to load Lasso Session");
Xavier Guimard's avatar
Xavier Guimard committed
1337 1338
            return $self->imgnok($req);
        }
1339
        $self->logger->debug("Lasso Session loaded");
Xavier Guimard's avatar
Xavier Guimard committed
1340 1341 1342 1343
    }

    if ($identity) {
        unless ( $self->setIdentityFromDump( $logout, $identity ) ) {
1344
            $self->logger->error("Unable to load Lasso Identity");
Xavier Guimard's avatar
Xavier Guimard committed
1345 1346
            return $self->imgnok($req);
        }
1347
        $self->logger->debug("Lasso Identity loaded");
Xavier Guimard's avatar
Xavier Guimard committed
1348 1349 1350 1351 1352
    }

    # Send the logout request
    my ( $rstatus, $rmethod, $rinfo ) =
      $self->sendLogoutRequestToProvider( $req, $logout, $providerID,
Xavier Guimard's avatar
Xavier Guimard committed
1353 1354
        Lasso::Constants::HTTP_METHOD_SOAP,
        undef, $relayState );
Xavier Guimard's avatar
Xavier Guimard committed
1355
    unless ($rstatus) {
1356 1357
        $self->logger->error(
            "Fail to process SOAP logout request to $providerID");
Xavier Guimard's avatar
Xavier Guimard committed
1358 1359 1360 1361
        return $self->imgnok($req);
    }

    # Store success status for this SLO request
1362 1363
    my $sloStatusSessionInfos =
      $self->getSamlSession( $relayState, { $spConfKey => 1 } );
Xavier Guimard's avatar
Xavier Guimard committed
1364 1365

    if ($sloStatusSessionInfos) {
1366 1367
        $self->logger->debug(
            "Store SLO status for $spConfKey in session $relayState");
Xavier Guimard's avatar
Xavier Guimard committed
1368 1369
    }
    else {
1370 1371
        $self->logger->warn(
            "Unable to store SLO status for $spConfKey in session $relayState");
Xavier Guimard's avatar
Xavier Guimard committed
1372 1373 1374 1375 1376 1377
    }

    # Delete relay session
    $relayInfos->remove();

    # SLO response is OK
1378
    $self->logger->debug("Display OK status for SLO on $spConfKey");
Xavier Guimard's avatar
Xavier Guimard committed
1379 1380 1381
    return $self->imgok($req);
}

1382 1383
sub sloRelayPost {
    my ( $self, $req ) = @_;
1384 1385
    $self->logger->debug(
        "URL " . $req->uri . " detected as a POST relay service URL" );
1386 1387 1388 1389 1390 1391 1392 1393 1394 1395 1396 1397 1398 1399

    # Check if relay parameter is present (mandatory)
    my $relayID;
    unless ( $relayID = $req->param('relay') ) {
        return $self->p->sendError( $req, 'No relayID detected' );
    }

    # Retrieve the corresponding data from samlStorage
    my $relayInfos = $self->getSamlSession($relayID);
    unless ($relayInfos) {
        return $self->p->sendError( $req,
            "Could not get relay session