AuthSAML.pm 50.6 KB
Newer Older
Yadd's avatar
Yadd committed
1
## @file
Clément OUDOT's avatar
Clément OUDOT committed
2
# SAML Service Provider - Authentication
Yadd's avatar
Yadd committed
3 4

## @class
Clément OUDOT's avatar
Clément OUDOT committed
5
# SAML Service Provider - Authentication
Yadd's avatar
Yadd committed
6 7 8
package Lemonldap::NG::Portal::AuthSAML;

use strict;
9
use MIME::Base64;
Yadd's avatar
Yadd committed
10
use Lemonldap::NG::Portal::Simple;
Clément OUDOT's avatar
Clément OUDOT committed
11
use Lemonldap::NG::Portal::_SAML;    #inherits
Clément OUDOT's avatar
SAML:  
Clément OUDOT committed
12
use Lemonldap::NG::Common::Conf::SAML::Metadata;
Yadd's avatar
Yadd committed
13

Yadd's avatar
Yadd committed
14
our $VERSION = '2.0.0';
Yadd's avatar
Yadd committed
15
our @ISA     = qw(Lemonldap::NG::Portal::_SAML);
16

Yadd's avatar
Yadd committed
17
## @apmethod int authInit()
Clément OUDOT's avatar
Clément OUDOT committed
18
# Load Lasso and metadata
Yadd's avatar
Yadd committed
19 20 21
# @return Lemonldap::NG::Portal error code
sub authInit {
    my $self = shift;
Clément OUDOT's avatar
Clément OUDOT committed
22

23
    # Load SAML service
Clément OUDOT's avatar
Clément OUDOT committed
24
    return PE_SAML_LOAD_SERVICE_ERROR unless $self->loadService();
Clément OUDOT's avatar
Clément OUDOT committed
25

26
    # Load SAML identity providers
Clément OUDOT's avatar
Clément OUDOT committed
27
    return PE_SAML_LOAD_IDP_ERROR unless $self->loadIDPs();
28

Clément OUDOT's avatar
Clément OUDOT committed
29
    PE_OK;
Yadd's avatar
Yadd committed
30 31 32
}

## @apmethod int extractFormInfo()
Clément OUDOT's avatar
Clément OUDOT committed
33
# Check authentication statement or create authentication request
Yadd's avatar
Yadd committed
34 35
# @return Lemonldap::NG::Portal error code
sub extractFormInfo {
Yadd's avatar
Yadd committed
36
    my $self   = shift;
37
    my $server = $self->{_lassoServer};
Yadd's avatar
Yadd committed
38

Yadd's avatar
Yadd committed
39
    # 1. Get HTTP request information to know
Clément OUDOT's avatar
Clément OUDOT committed
40
    # if we are receving SAML request or response
41
    my $url            = $self->url( -absolute => 1 );
Clément OUDOT's avatar
Clément OUDOT committed
42 43 44
    my $request_method = $self->request_method();
    my $content_type   = $self->content_type();

Clément OUDOT's avatar
SAML:  
Clément OUDOT committed
45 46 47 48 49 50
    my $saml_acs_art_url = $self->getMetaDataURL(
        "samlSPSSODescriptorAssertionConsumerServiceHTTPArtifact");
    my $saml_acs_post_url = $self->getMetaDataURL(
        "samlSPSSODescriptorAssertionConsumerServiceHTTPPost");
    my $saml_acs_get_url = $self->getMetaDataURL(
        "samlSPSSODescriptorAssertionConsumerServiceHTTPRedirect");
Clément OUDOT's avatar
Clément OUDOT committed
51 52 53 54
    my $saml_slo_soap_url =
      $self->getMetaDataURL( "samlSPSSODescriptorSingleLogoutServiceSOAP", 1 );
    my $saml_slo_soap_url_ret =
      $self->getMetaDataURL( "samlSPSSODescriptorSingleLogoutServiceSOAP", 2 );
55 56 57 58 59 60 61 62 63 64
    my $saml_slo_get_url = $self->getMetaDataURL(
        "samlSPSSODescriptorSingleLogoutServiceHTTPRedirect", 1 );
    my $saml_slo_get_url_ret = $self->getMetaDataURL(
        "samlSPSSODescriptorSingleLogoutServiceHTTPRedirect", 2 );
    my $saml_slo_post_url =
      $self->getMetaDataURL( "samlSPSSODescriptorSingleLogoutServiceHTTPPost",
        1 );
    my $saml_slo_post_url_ret =
      $self->getMetaDataURL( "samlSPSSODescriptorSingleLogoutServiceHTTPPost",
        2 );
65 66
    my $saml_ars_url = $self->getMetaDataURL(
        "samlSPSSODescriptorArtifactResolutionServiceArtifact");
Clément OUDOT's avatar
Clément OUDOT committed
67 68

    # 1.1 SSO assertion consumer
69
    if ( $url =~
Clément OUDOT's avatar
SAML:  
Clément OUDOT committed
70
/^(\Q$saml_acs_art_url\E|\Q$saml_acs_post_url\E|\Q$saml_acs_get_url\E)$/io
71
      )
Clément OUDOT's avatar
SAML:  
Clément OUDOT committed
72
    {
Clément OUDOT's avatar
Clément OUDOT committed
73
        $self->lmLog( "URL $url detected as an SSO assertion consumer URL",
Clément OUDOT's avatar
SAML:  
Clément OUDOT committed
74 75
            'debug' );

76 77
        # Check SAML Message
        my ( $request, $response, $method, $relaystate, $artifact ) =
Clément OUDOT's avatar
SAML:  
Clément OUDOT committed
78 79 80
          $self->checkMessage( $url, $request_method, $content_type, "login" );

        # Create Login object
Clément OUDOT's avatar
SAML:  
Clément OUDOT committed
81
        my $login = $self->createLogin($server);
Clément OUDOT's avatar
SAML:  
Clément OUDOT committed
82

Clément OUDOT's avatar
SAML:  
Clément OUDOT committed
83
        # Ignore signature verification
Yadd's avatar
Yadd committed
84
        $self->disableSignatureVerification($login);
Clément OUDOT's avatar
SAML:  
Clément OUDOT committed
85 86

        if ($response) {
Clément OUDOT's avatar
SAML:  
Clément OUDOT committed
87 88

            # Process authentication response
89 90 91 92 93 94 95
            my $result;
            if ($artifact) {
                $result = $self->processArtResponseMsg( $login, $response );
            }
            else {
                $result = $self->processAuthnResponseMsg( $login, $response );
            }
Clément OUDOT's avatar
SAML:  
Clément OUDOT committed
96 97

            unless ($result) {
98
                $self->lmLog( "SSO: Fail to process authentication response",
Clément OUDOT's avatar
SAML:  
Clément OUDOT committed
99
                    'error' );
Clément OUDOT's avatar
Clément OUDOT committed
100
                return PE_SAML_SSO_ERROR;
Clément OUDOT's avatar
SAML:  
Clément OUDOT committed
101 102
            }

103
            $self->lmLog( "SSO: authentication response is valid", 'debug' );
Clément OUDOT's avatar
SAML:  
Clément OUDOT committed
104

Clément OUDOT's avatar
SAML:  
Clément OUDOT committed
105
            # Get IDP entityID
Clément OUDOT's avatar
SAML:  
Clément OUDOT committed
106
            my $idp = $login->remote_providerID();
Clément OUDOT's avatar
SAML:  
Clément OUDOT committed
107 108 109 110

            $self->lmLog( "Found entityID $idp in SAML message", 'debug' );

            # IDP conf key
Clément OUDOT's avatar
SAML:  
Clément OUDOT committed
111
            my $idpConfKey = $self->{_idpList}->{$idp}->{confKey};
Clément OUDOT's avatar
SAML:  
Clément OUDOT committed
112 113 114 115

            unless ($idpConfKey) {
                $self->lmLog( "$idp do not match any IDP in configuration",
                    'error' );
Clément OUDOT's avatar
Clément OUDOT committed
116
                return PE_SAML_UNKNOWN_ENTITY;
Clément OUDOT's avatar
SAML:  
Clément OUDOT committed
117 118 119 120 121 122 123 124 125 126 127
            }

            $self->lmLog( "$idp match $idpConfKey IDP in configuration",
                'debug' );

            # Do we check signature?
            my $checkSSOMessageSignature =
              $self->{samlIDPMetaDataOptions}->{$idpConfKey}
              ->{samlIDPMetaDataOptionsCheckSSOMessageSignature};

            if ($checkSSOMessageSignature) {
128 129 130 131 132 133 134 135 136 137 138 139

                $self->forceSignatureVerification($login);

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

                unless ($result) {
140
                    $self->lmLog( "Signature is not valid", 'error' );
Clément OUDOT's avatar
Clément OUDOT committed
141
                    return PE_SAML_SIGNATURE_ERROR;
142 143 144 145
                }
                else {
                    $self->lmLog( "Signature is valid", 'debug' );
                }
Clément OUDOT's avatar
SAML:  
Clément OUDOT committed
146 147 148 149 150 151
            }
            else {
                $self->lmLog( "Message signature will not be checked",
                    'debug' );
            }

Yadd's avatar
Yadd committed
152 153 154 155
            # Get SAML response
            my $saml_response = $login->response();
            unless ($saml_response) {
                $self->lmLog( "No SAML response found", 'error' );
Clément OUDOT's avatar
Clément OUDOT committed
156
                return PE_SAML_SSO_ERROR;
Yadd's avatar
Yadd committed
157
            }
Clément OUDOT's avatar
Clément OUDOT committed
158

159
            # Check Destination
Clément OUDOT's avatar
Clément OUDOT committed
160
            return PE_SAML_DESTINATION_ERROR
161 162
              unless ( $self->checkDestination( $saml_response, $url ) );

Yadd's avatar
Yadd committed
163 164 165 166
            # Replay protection if this is a response to a created authn request
            my $assertion_responded = $saml_response->InResponseTo;
            if ($assertion_responded) {
                unless ( $self->replayProtection($assertion_responded) ) {
167

Yadd's avatar
Yadd committed
168 169 170
                    # Assertion was already consumed or is expired
                    # Force authentication replay
                    $self->lmLog(
Clément OUDOT's avatar
SAML:  
Clément OUDOT committed
171
"Message $assertion_responded already used or expired, replay authentication",
Yadd's avatar
Yadd committed
172 173 174 175
                        'error'
                    );
                    delete $self->{urldc};
                    $self->{mustRedirect} = 1;
176
                    return $self->_subProcess(qw(autoRedirect));
Yadd's avatar
Yadd committed
177
                }
178
            }
Yadd's avatar
Yadd committed
179 180
            else {
                $self->lmLog(
181
"Assertion is not a response to a created authentication request, do not control replay",
Yadd's avatar
Yadd committed
182 183 184
                    'debug'
                );
            }
185

Yadd's avatar
Yadd committed
186 187
            # Get SAML assertion
            my $assertion = $self->getAssertion($login);
Clément OUDOT's avatar
Clément OUDOT committed
188

Yadd's avatar
Yadd committed
189 190
            unless ($assertion) {
                $self->lmLog( "No assertion found", 'error' );
Clément OUDOT's avatar
Clément OUDOT committed
191
                return PE_SAML_SSO_ERROR;
Yadd's avatar
Yadd committed
192
            }
Clément OUDOT's avatar
Clément OUDOT committed
193

Clément OUDOT's avatar
SAML:  
Clément OUDOT committed
194
            # Do we check conditions?
195
            my $checkTime =
Clément OUDOT's avatar
SAML:  
Clément OUDOT committed
196
              $self->{samlIDPMetaDataOptions}->{$idpConfKey}
197 198 199 200
              ->{samlIDPMetaDataOptionsCheckTime};
            my $checkAudience =
              $self->{samlIDPMetaDataOptions}->{$idpConfKey}
              ->{samlIDPMetaDataOptionsCheckAudience};
Clément OUDOT's avatar
SAML:  
Clément OUDOT committed
201

Yadd's avatar
Yadd committed
202
            # Check conditions - time and audience
203 204 205 206
            unless (
                $self->validateConditions(
                    $assertion, $self->getMetaDataURL( "samlEntityID", 0, 1 ),
                    $checkTime, $checkAudience
207
                )
Clément OUDOT's avatar
SAML:  
Clément OUDOT committed
208
              )
Yadd's avatar
Yadd committed
209
            {
210
                $self->lmLog( "Conditions not validated", 'error' );
Clément OUDOT's avatar
Clément OUDOT committed
211
                return PE_SAML_CONDITIONS_ERROR;
Yadd's avatar
Yadd committed
212
            }
Clément OUDOT's avatar
Clément OUDOT committed
213

214 215 216 217
            my $relayStateURL =
              $self->{samlIDPMetaDataOptions}->{$idpConfKey}
              ->{samlIDPMetaDataOptionsRelayStateURL};

Yadd's avatar
Yadd committed
218
            #  Extract RelayState information
219
            if ( $self->extractRelayState( $relaystate, $relayStateURL ) ) {
220
                $self->lmLog( "RelayState $relaystate extracted", 'debug' );
Yadd's avatar
Yadd committed
221
            }
222

223 224
            # Check if we accept direct login from IDP
            my $allowLoginFromIDP =
Clément OUDOT's avatar
SAML:  
Clément OUDOT committed
225
              $self->{samlIDPMetaDataOptions}->{$idpConfKey}
226 227
              ->{samlIDPMetaDataOptionsAllowLoginFromIDP};
            if ( !$assertion_responded and !$allowLoginFromIDP ) {
Clément OUDOT's avatar
SAML:  
Clément OUDOT committed
228 229
                $self->lmLog(
                    "Direct login from IDP $idpConfKey is not allowed",
230
                    'error' );
Clément OUDOT's avatar
Clément OUDOT committed
231
                return PE_SAML_IDPSSOINITIATED_NOTALLOWED;
232 233
            }

234
            # Check authentication context
Yadd's avatar
Yadd committed
235 236 237 238 239 240 241 242 243
            my $responseAuthnContext;
            eval {
                $responseAuthnContext =
                  $assertion->AuthnStatement()->AuthnContext()
                  ->AuthnContextClassRef();
            };
            if ($@) {
                $self->lmLog(
                    "Unable to get authentication context from $idpConfKey",
244 245
                    'debug' );
                $responseAuthnContext = $self->getAuthnContext("unspecified");
Yadd's avatar
Yadd committed
246
            }
247
            else {
248 249 250 251 252
                $self->lmLog(
                    "Found authentication context: $responseAuthnContext",
                    'debug' );
            }

253 254 255 256
            # Map authentication context to authentication level
            $self->{sessionInfo}->{authenticationLevel} =
              $self->authnContext2authnLevel($responseAuthnContext);

Yadd's avatar
Yadd committed
257 258 259
            # Force redirection to portal if no urldc found
            # (avoid displaying the whole SAML URL in user browser URL field)
            $self->{mustRedirect} = 1 unless ( $self->{urldc} );
260

Clément OUDOT's avatar
 
Clément OUDOT committed
261 262 263 264 265 266 267 268 269 270 271 272 273
            # Get SessionIndex
            my $session_index;

            eval {
                $session_index = $assertion->AuthnStatement()->SessionIndex();
            };
            if ( $@ or !defined($session_index) ) {
                $self->lmLog( "No SessionIndex found", 'debug' );
            }
            else {
                $self->lmLog( "Found SessionIndex $session_index", 'debug' );
            }

Yadd's avatar
Yadd committed
274 275
            # Get NameID
            my $nameid = $login->nameIdentifier;
Clément OUDOT's avatar
Clément OUDOT committed
276

Yadd's avatar
Yadd committed
277 278
            # Set user
            my $user = $nameid->content;
Clément OUDOT's avatar
Clément OUDOT committed
279

Yadd's avatar
Yadd committed
280 281
            unless ($user) {
                $self->lmLog( "No NameID value found", 'error' );
Clément OUDOT's avatar
Clément OUDOT committed
282
                return PE_SAML_SSO_ERROR;
Yadd's avatar
Yadd committed
283
            }
284

Clément OUDOT's avatar
 
Clément OUDOT committed
285
            $self->lmLog( "Found NameID: $user", 'debug' );
Yadd's avatar
Yadd committed
286
            $self->{user} = $user;
Clément OUDOT's avatar
Clément OUDOT committed
287

Yadd's avatar
Yadd committed
288
            # Store Lasso objects
Yadd's avatar
Yadd committed
289 290 291
            $self->{_lassoLogin}   = $login;
            $self->{_idp}          = $idp;
            $self->{_idpConfKey}   = $idpConfKey;
292
            $self->{_nameID}       = $nameid;
Clément OUDOT's avatar
 
Clément OUDOT committed
293
            $self->{_sessionIndex} = $session_index;
Clément OUDOT's avatar
Clément OUDOT committed
294

295 296 297 298 299 300 301
            # Store Token
            my $saml_token = $assertion->export_to_xml;

            $self->lmLog( "SAML Token: $saml_token", 'debug' );

            $self->{_samlToken} = $saml_token;

302
            # Restore initial SAML request in case of proxying
303 304 305 306
            my $moduleOptions = $self->{samlStorageOptions} || {};
            $moduleOptions->{backend} = $self->{samlStorage};
            my $module = "Lemonldap::NG::Common::Apache::Session";

307
            my $saml_sessions =
308 309
              $module->searchOn( $moduleOptions, "ProxyID",
                $assertion_responded );
310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331

            if ( my @saml_sessions_keys = keys %$saml_sessions ) {

                # Warning if more than one session found
                if ( $#saml_sessions_keys > 0 ) {
                    $self->lmLog(
"More than one SAML proxy session found for ID $assertion_responded",
                        'warn'
                    );
                }

                # Take the first session
                my $saml_session = shift @saml_sessions_keys;

                # Get session
                $self->lmLog(
"Retrieve SAML proxy session $saml_session for ID $assertion_responded",
                    'debug'
                );

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

332 333 334 335 336
                $self->{_proxiedRequest} = $samlSessionInfo->data->{Request};
                $self->{_proxiedMethod}  = $samlSessionInfo->data->{Method};
                $self->{_proxiedRelayState} =
                  $samlSessionInfo->data->{RelayState};
                $self->{_proxiedArtifact} = $samlSessionInfo->data->{Artifact};
337 338 339 340 341 342 343 344 345 346

               # Save values in hidden fields in case of other user interactions
                $self->setHiddenFormValue( 'SAMLRequest',
                    $self->{_proxiedRequest} );
                $self->setHiddenFormValue( 'Method', $self->{_proxiedMethod} );
                $self->setHiddenFormValue( 'RelayState',
                    $self->{_proxiedRelayState} );
                $self->setHiddenFormValue( 'SAMLart',
                    $self->{_proxiedArtifact} );

347
                # Delete session
348
                $samlSessionInfo->remove();
349 350
            }

Yadd's avatar
Yadd committed
351 352
            return PE_OK;
        }
Clément OUDOT's avatar
SAML:  
Clément OUDOT committed
353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368
        elsif ($request) {

            # Do nothing
            $self->lmLog(
                "This module do not manage SSO request, see IssuerDBSAML",
                'debug' );

            return PE_OK;
        }
        else {

            # This should not happen
            $self->lmLog( "SSO request or response was not found", 'error' );

            # Redirect user
            $self->{mustRedirect} = 1;
369
            return $self->_subProcess(qw(autoRedirect));
Clément OUDOT's avatar
SAML:  
Clément OUDOT committed
370 371
        }

Clément OUDOT's avatar
SAML:  
Clément OUDOT committed
372
    }
Yadd's avatar
Yadd committed
373

Clément OUDOT's avatar
Clément OUDOT committed
374
    # 1.2 SLO
Yadd's avatar
Yadd committed
375
    elsif ( $url =~
376
/^(\Q$saml_slo_soap_url\E|\Q$saml_slo_soap_url_ret\E|\Q$saml_slo_get_url\E|\Q$saml_slo_get_url_ret\E|Q$saml_slo_post_url\E|\Q$saml_slo_post_url_ret\E)$/io
Clément OUDOT's avatar
Clément OUDOT committed
377 378 379 380
      )
    {
        $self->lmLog( "URL $url detected as an SLO URL", 'debug' );

Clément OUDOT's avatar
SAML:  
Clément OUDOT committed
381 382 383 384
        # Check SAML Message
        my ( $request, $response, $method, $relaystate, $artifact ) =
          $self->checkMessage( $url, $request_method, $content_type, "logout" );

385
        # Create Logout object
Clément OUDOT's avatar
SAML:  
Clément OUDOT committed
386
        my $logout = $self->createLogout($server);
387

Clément OUDOT's avatar
SAML:  
Clément OUDOT committed
388
        # Ignore signature verification
Yadd's avatar
Yadd committed
389
        $self->disableSignatureVerification($logout);
Clément OUDOT's avatar
Clément OUDOT committed
390 391 392 393 394 395 396 397

        if ($response) {

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

            unless ($result) {
                $self->lmLog( "Fail to process logout response", 'error' );
Clément OUDOT's avatar
Clément OUDOT committed
398
                return PE_SAML_SLO_ERROR;
Clément OUDOT's avatar
Clément OUDOT committed
399 400 401 402
            }

            $self->lmLog( "Logout response is valid", 'debug' );

403
            # Check Destination
Clément OUDOT's avatar
Clément OUDOT committed
404
            return PE_SAML_DESTINATION_ERROR
405 406
              unless ( $self->checkDestination( $logout->response, $url ) );

Clément OUDOT's avatar
SAML:  
Clément OUDOT committed
407
            # Get IDP entityID
Clément OUDOT's avatar
SAML:  
Clément OUDOT committed
408
            my $idp = $logout->remote_providerID();
Clément OUDOT's avatar
SAML:  
Clément OUDOT committed
409 410 411 412

            $self->lmLog( "Found entityID $idp in SAML message", 'debug' );

            # IDP conf key
Clément OUDOT's avatar
SAML:  
Clément OUDOT committed
413
            my $idpConfKey = $self->{_idpList}->{$idp}->{confKey};
Clément OUDOT's avatar
SAML:  
Clément OUDOT committed
414 415 416 417

            unless ($idpConfKey) {
                $self->lmLog( "$idp do not match any IDP in configuration",
                    'error' );
Clément OUDOT's avatar
Clément OUDOT committed
418
                return PE_SAML_UNKNOWN_ENTITY;
Clément OUDOT's avatar
SAML:  
Clément OUDOT committed
419 420 421 422 423 424 425 426 427 428 429
            }

            $self->lmLog( "$idp match $idpConfKey IDP in configuration",
                'debug' );

            # Do we check signature?
            my $checkSLOMessageSignature =
              $self->{samlIDPMetaDataOptions}->{$idpConfKey}
              ->{samlIDPMetaDataOptionsCheckSLOMessageSignature};

            if ($checkSLOMessageSignature) {
430 431 432 433 434 435

                $self->forceSignatureVerification($logout);

                $result = $self->processLogoutResponseMsg( $logout, $response );

                unless ($result) {
436
                    $self->lmLog( "Signature is not valid", 'error' );
Clément OUDOT's avatar
Clément OUDOT committed
437
                    return PE_SAML_SIGNATURE_ERROR;
438 439 440 441
                }
                else {
                    $self->lmLog( "Signature is valid", 'debug' );
                }
Clément OUDOT's avatar
SAML:  
Clément OUDOT committed
442 443 444 445 446 447
            }
            else {
                $self->lmLog( "Message signature will not be checked",
                    'debug' );
            }

Clément OUDOT's avatar
SAML:  
Clément OUDOT committed
448
            # Replay protection
Clément OUDOT's avatar
SAML:  
Clément OUDOT committed
449
            my $samlID = $logout->response()->InResponseTo;
Clément OUDOT's avatar
SAML:  
Clément OUDOT committed
450 451 452 453 454 455

            unless ( $self->replayProtection($samlID) ) {

                # Logout request was already consumed or is expired
                $self->lmLog( "Message $samlID already used or expired",
                    'error' );
Clément OUDOT's avatar
Clément OUDOT committed
456
                return PE_SAML_SLO_ERROR;
Clément OUDOT's avatar
SAML:  
Clément OUDOT committed
457
            }
Clément OUDOT's avatar
Clément OUDOT committed
458

459 460 461 462 463 464 465
            # If URL in RelayState, different from portal, redirect user
            if ( $self->extractRelayState($relaystate) ) {
                $self->lmLog( "RelayState $relaystate extracted", 'debug' );
                $self->lmLog( "URL " . $self->{urldc} . " found in RelayState",
                    'debug' );
            }

466
            return $self->_subProcess(qw(autoRedirect))
467 468
              if (  $self->{urldc}
                and $self->{portal} !~ /\Q$self->{urldc}\E\/?/ );
469 470

            # Else, inform user that logout is OK
Clément OUDOT's avatar
Clément OUDOT committed
471 472 473 474 475 476 477 478 479 480 481 482 483 484
            return PE_LOGOUT_OK;
        }

        elsif ($request) {

            # Logout error
            my $logout_error = 0;

            # Lasso::Session dump
            my $session_dump;

            # Process logout request
            unless ( $self->processLogoutRequestMsg( $logout, $request ) ) {
                $self->lmLog( "Fail to process logout request", 'error' );
Clément OUDOT's avatar
SAML:  
Clément OUDOT committed
485
                $logout_error = 1;
Clément OUDOT's avatar
Clément OUDOT committed
486 487 488 489
            }

            $self->lmLog( "Logout request is valid", 'debug' );

490
            # Check Destination
Clément OUDOT's avatar
Clément OUDOT committed
491
            return PE_SAML_DESTINATION_ERROR
492 493
              unless ( $self->checkDestination( $logout->request, $url ) );

Clément OUDOT's avatar
SAML:  
Clément OUDOT committed
494
            # Get IDP entityID
Clément OUDOT's avatar
SAML:  
Clément OUDOT committed
495
            my $idp = $logout->remote_providerID();
Clément OUDOT's avatar
SAML:  
Clément OUDOT committed
496 497 498 499

            $self->lmLog( "Found entityID $idp in SAML message", 'debug' );

            # IDP conf key
Clément OUDOT's avatar
SAML:  
Clément OUDOT committed
500
            my $idpConfKey = $self->{_idpList}->{$idp}->{confKey};
Clément OUDOT's avatar
SAML:  
Clément OUDOT committed
501 502 503 504

            unless ($idpConfKey) {
                $self->lmLog( "$idp do not match any IDP in configuration",
                    'error' );
Clément OUDOT's avatar
Clément OUDOT committed
505
                return PE_SAML_UNKNOWN_ENTITY;
Clément OUDOT's avatar
SAML:  
Clément OUDOT committed
506 507 508 509 510 511 512 513 514 515 516
            }

            $self->lmLog( "$idp match $idpConfKey IDP in configuration",
                'debug' );

            # Do we check signature?
            my $checkSLOMessageSignature =
              $self->{samlIDPMetaDataOptions}->{$idpConfKey}
              ->{samlIDPMetaDataOptionsCheckSLOMessageSignature};

            if ($checkSLOMessageSignature) {
517 518
                unless ( $self->checkSignatureStatus($logout) ) {
                    $self->lmLog( "Signature is not valid", 'error' );
Clément OUDOT's avatar
Clément OUDOT committed
519
                    return PE_SAML_SIGNATURE_ERROR;
520 521 522 523
                }
                else {
                    $self->lmLog( "Signature is valid", 'debug' );
                }
Clément OUDOT's avatar
SAML:  
Clément OUDOT committed
524 525 526 527 528 529
            }
            else {
                $self->lmLog( "Message signature will not be checked",
                    'debug' );
            }

Clément OUDOT's avatar
Clément OUDOT committed
530 531 532 533 534
            # Get NameID and SessionIndex
            my $name_id       = $logout->request()->NameID;
            my $session_index = $logout->request()->SessionIndex;
            my $user          = $name_id->content;

Clément OUDOT's avatar
 
Clément OUDOT committed
535 536
            unless ($name_id) {
                $self->lmLog( "Fail to get NameID from logout request",
Clément OUDOT's avatar
Clément OUDOT committed
537
                    'error' );
Clément OUDOT's avatar
SAML:  
Clément OUDOT committed
538
                $logout_error = 1;
Clément OUDOT's avatar
Clément OUDOT committed
539 540 541 542
            }

            $self->lmLog( "Logout request NameID content: $user", 'debug' );

Clément OUDOT's avatar
 
Clément OUDOT committed
543
            # Get SAML sessions with the same NameID
544 545 546 547
            my $moduleOptions = $self->{samlStorageOptions} || {};
            $moduleOptions->{backend} = $self->{samlStorage};
            my $module = "Lemonldap::NG::Common::Apache::Session";

Clément OUDOT's avatar
Clément OUDOT committed
548
            my $local_sessions =
549
              $module->searchOn( $moduleOptions, "_nameID", $name_id->dump );
Clément OUDOT's avatar
Clément OUDOT committed
550 551 552

            if ( my @local_sessions_keys = keys %$local_sessions ) {

Clément OUDOT's avatar
 
Clément OUDOT committed
553
                # At least one session was found
Clément OUDOT's avatar
Clément OUDOT committed
554 555 556 557 558 559
                foreach (@local_sessions_keys) {

                    my $local_session = $_;

                    # Get session
                    $self->lmLog(
Clément OUDOT's avatar
 
Clément OUDOT committed
560
                        "Retrieve SAML session $local_session for user $user",
Clément OUDOT's avatar
Clément OUDOT committed
561
                        'debug' );
Clément OUDOT's avatar
 
Clément OUDOT committed
562

Clément OUDOT's avatar
SAML:  
Clément OUDOT committed
563
                    my $sessionInfo = $self->getSamlSession($local_session);
Clément OUDOT's avatar
Clément OUDOT committed
564

Clément OUDOT's avatar
 
Clément OUDOT committed
565 566 567
              # If session index is defined and not equal to SAML session index,
              # jump to next session
                    if ( defined $session_index
568 569
                        and $session_index ne
                        $sessionInfo->data->{_sessionIndex} )
Clément OUDOT's avatar
 
Clément OUDOT committed
570 571 572 573 574 575 576 577 578 579 580 581
                    {
                        $self->lmLog(
"Session $local_session has not the good session index, skipping",
                            'debug'
                        );
                        next;
                    }

                    # Delete session
                    else {

                        # Open real session
582
                        my $real_session = $sessionInfo->data->{_saml_id};
Clément OUDOT's avatar
 
Clément OUDOT committed
583

584 585
                        my $ssoSession =
                          $self->getApacheSession( $real_session, 1 );
Clément OUDOT's avatar
 
Clément OUDOT committed
586

Yadd's avatar
Yadd committed
587
                  # Get Lasso::Session dump
Clément OUDOT's avatar
 
Clément OUDOT committed
588
                  # This value is erased if a next session match the SLO request
589
                        if (   $ssoSession
590
                            && $ssoSession->data->{_lassoSessionDump} )
Clément OUDOT's avatar
 
Clément OUDOT committed
591 592 593 594 595 596
                        {
                            $self->lmLog(
"Get Lasso::Session dump from session $real_session",
                                'debug'
                            );
                            $session_dump =
597
                              $ssoSession->data->{_lassoSessionDump};
Clément OUDOT's avatar
 
Clément OUDOT committed
598 599 600 601
                        }

                        # Delete real session
                        my $del_real_result =
602
                          $self->_deleteSession($ssoSession);
Clément OUDOT's avatar
Clément OUDOT committed
603

Yadd's avatar
Yadd committed
604
                        $self->lmLog(
Clément OUDOT's avatar
 
Clément OUDOT committed
605 606 607 608 609 610 611
"Delete real session $real_session result: $del_real_result",
                            'debug'
                        );

                        $logout_error = 1 unless $del_real_result;

                        # Delete SAML session
612
                        my $del_saml_result = $sessionInfo->remove();
Clément OUDOT's avatar
 
Clément OUDOT committed
613 614 615 616 617 618 619 620

                        $self->lmLog(
"Delete SAML session $local_session result: $del_saml_result",
                            'debug'
                        );

                        $logout_error = 1 unless $del_saml_result;
                    }
Clément OUDOT's avatar
Clément OUDOT committed
621 622 623
                }

                # Set session from dump
624 625 626
                unless ( $self->setSessionFromDump( $logout, $session_dump ) ) {
                    $self->lmLog( "Cannot set session from dump in logout",
                        'error' );
Clément OUDOT's avatar
SAML:  
Clément OUDOT committed
627
                    $logout_error = 1;
628
                }
Clément OUDOT's avatar
Clément OUDOT committed
629

Clément OUDOT's avatar
Clément OUDOT committed
630 631 632 633
            }
            else {

                # No corresponding session found
Clément OUDOT's avatar
 
Clément OUDOT committed
634
                $self->lmLog( "No SAML session found for user $user", 'debug' );
Clément OUDOT's avatar
Clément OUDOT committed
635

Clément OUDOT's avatar
SAML:  
Clément OUDOT committed
636
                $logout_error = 1;
Clément OUDOT's avatar
Clément OUDOT committed
637 638 639

            }

Clément OUDOT's avatar
SAML:  
Clément OUDOT committed
640 641 642 643 644 645 646
            # Validate request if no previous error
            unless ($logout_error) {
                unless ( $self->validateLogoutRequest($logout) ) {
                    $self->lmLog( "SLO request is not valid", 'error' );
                }
            }

647 648 649 650 651 652
            # Set RelayState
            if ($relaystate) {
                $logout->msg_relayState($relaystate);
                $self->lmLog( "Set $relaystate in RelayState", 'debug' );
            }

653
            # Do we set signature?
Yadd's avatar
Yadd committed
654
            my $signSLOMessage =
Clément OUDOT's avatar
SAML:  
Clément OUDOT committed
655
              $self->{samlIDPMetaDataOptions}->{$idpConfKey}
Yadd's avatar
Yadd committed
656
              ->{samlIDPMetaDataOptionsSignSLOMessage};
Clément OUDOT's avatar
 
Clément OUDOT committed
657 658

            if ( $signSLOMessage == 0 ) {
Clément OUDOT's avatar
SAML:  
Clément OUDOT committed
659 660
                $self->lmLog(
                    "SLO message to IDP $idpConfKey will not be signed",
Yadd's avatar
Yadd committed
661 662 663
                    'debug' );
                $self->disableSignature($logout);
            }
Clément OUDOT's avatar
 
Clément OUDOT committed
664 665 666 667 668 669 670 671 672 673 674
            elsif ( $signSLOMessage == 1 ) {
                $self->lmLog( "SLO message to IDP $idpConfKey will be signed",
                    'debug' );
                $self->forceSignature($logout);
            }
            else {
                $self->lmLog(
"SLO message to IDP $idpConfKey signature according to metadata",
                    'debug'
                );
            }
675

Clément OUDOT's avatar
Clément OUDOT committed
676
            # Logout response
677 678
            unless ( $self->buildLogoutResponseMsg($logout) ) {
                $self->lmLog( "Unable to build SLO response", 'error' );
Clément OUDOT's avatar
Clément OUDOT committed
679
                return PE_SAML_SLO_ERROR;
680 681 682 683 684 685 686 687 688 689 690 691
            }

            # 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;
                $self->lmLog( "Redirect user to $slo_url", 'debug' );

                $self->{urldc} = $slo_url;

692
                return $self->_subProcess(qw(autoRedirect));
693 694 695
            }

            # HTTP-POST
Yadd's avatar
Yadd committed
696
            elsif ( $method == Lasso::Constants::HTTP_METHOD_POST ) {
697

Clément OUDOT's avatar
SAML:  
Clément OUDOT committed
698 699 700 701 702 703 704
                # Use autosubmit form
                my $slo_url  = $logout->msg_url;
                my $slo_body = $logout->msg_body;

                $self->{postUrl} = $slo_url;
                $self->{postFields} = { 'SAMLResponse' => $slo_body };

Clément OUDOT's avatar
Clément OUDOT committed
705
                # RelayState
Clément OUDOT's avatar
SAML:  
Clément OUDOT committed
706
                $self->{postFields}->{'RelayState'} = $relaystate
Clément OUDOT's avatar
Clément OUDOT committed
707 708
                  if ($relaystate);

709
                return $self->_subProcess(qw(autoPost));
710 711 712
            }

            # HTTP-SOAP
Yadd's avatar
Yadd committed
713
            elsif ( $method == Lasso::Constants::HTTP_METHOD_SOAP ) {
714 715 716 717 718 719 720 721 722 723 724 725

                my $slo_body = $logout->msg_body;

                $self->lmLog( "SOAP response $slo_body", 'debug' );

                $self->{SOAPMessage} = $slo_body;

                $self->_subProcess(qw(returnSOAPMessage));

                # If we are here, there was a problem with SOAP response
                $self->lmLog( "Logout response was not sent trough SOAP",
                    'error' );
Clément OUDOT's avatar
Clément OUDOT committed
726
                return PE_SAML_SLO_ERROR;
727
            }
Clément OUDOT's avatar
Clément OUDOT committed
728 729 730 731 732 733 734 735 736

        }
        else {

            # This should not happen
            $self->lmLog( "SLO request or response was not found", 'error' );

            # Redirect user
            $self->{mustRedirect} = 1;
737
            return $self->_subProcess(qw(autoRedirect));
Clément OUDOT's avatar
Clément OUDOT committed
738 739 740
        }
    }

741
    # 1.3 Artifact
Clément OUDOT's avatar
SAML:  
Clément OUDOT committed
742
    elsif ( $url =~ /^(\Q$saml_ars_url\E)$/io ) {
743

744
        $self->lmLog( "URL $url detected as an artifact resolution service URL",
745 746
            'debug' );

747 748 749 750 751
        # Artifact request are sent with SOAP trough POST
        my $art_request = $self->param('POSTDATA');
        my $art_response;

        # Create Login object
Clément OUDOT's avatar
SAML:  
Clément OUDOT committed
752
        my $login = $self->createLogin($server);
753

754 755 756 757
        # Process request message
        unless ( $self->processArtRequestMsg( $login, $art_request ) ) {
            $self->lmLog( "Unable to process artifact request message",
                'error' );
Clément OUDOT's avatar
Clément OUDOT committed
758
            return PE_SAML_ART_ERROR;
759 760 761
        }

        # Check Destination
Clément OUDOT's avatar
Clément OUDOT committed
762
        return PE_SAML_DESTINATION_ERROR
763 764
          unless ( $self->checkDestination( $login->request, $url ) );

765
        # Create artifact response
766
        unless ( $art_response = $self->createArtifactResponse($login) ) {
767 768
            $self->lmLog( "Unable to create artifact response message",
                'error' );
Clément OUDOT's avatar
Clément OUDOT committed
769
            return PE_SAML_ART_ERROR;
770 771 772 773 774 775 776 777 778 779 780
        }

        $self->{SOAPMessage} = $art_response;

        $self->lmLog( "Send SOAP Message: " . $self->{SOAPMessage}, 'debug' );

        # Return SOAP message
        $self->returnSOAPMessage();

        # If we are here, there was a problem with SOAP request
        $self->lmLog( "Artifact response was not sent trough SOAP", 'error' );
Clément OUDOT's avatar
Clément OUDOT committed
781
        return PE_SAML_ART_ERROR;
782

783 784
    }

Clément OUDOT's avatar
Clément OUDOT committed
785
    # 2. IDP resolution
Yadd's avatar
Yadd committed
786

787 788
    # Search a selected IdP
    my ( $idp, $idp_cookie ) = $self->_sub('getIDP');
789

Clément OUDOT's avatar
Clément OUDOT committed
790 791 792
    # Get confirmation flag
    my $confirm_flag = $self->param("confirm");

793 794
    # If confirmation is -1 from resolved IDP screen,
    # or IDP was not resolve, let the user choose its IDP
Clément OUDOT's avatar
Clément OUDOT committed
795
    if ( $confirm_flag == -1 or !$idp ) {
796
        $self->lmLog( "Redirecting user to IDP list", 'debug' );
Clément OUDOT's avatar
Clément OUDOT committed
797

798 799 800 801
        # Control url parameter
        my $urlcheck = $self->controlUrlOrigin();
        return $urlcheck unless ( $urlcheck == PE_OK );

Clément OUDOT's avatar
Clément OUDOT committed
802
        # IDP list
803
        my @list = ();
Clément OUDOT's avatar
Clément OUDOT committed
804
        foreach ( keys %{ $self->{_idpList} } ) {
Clément OUDOT's avatar
Clément OUDOT committed
805 806
            push @list,
              {
807 808
                val  => $_,
                name => $self->{_idpList}->{$_}->{name}
Clément OUDOT's avatar
Clément OUDOT committed
809
              };
Clément OUDOT's avatar
Clément OUDOT committed
810
        }
811 812
        $self->{list}            = \@list;
        $self->{confirmRemember} = 1;
813

Clément OUDOT's avatar
Clément OUDOT committed
814 815 816 817 818 819 820 821 822 823 824
        # Delete existing IDP resolution cookie
        push @{ $self->{cookie} },
          $self->cookie(
            -name    => $self->{samlIdPResolveCookie},
            -value   => 0,
            -domain  => $self->{domain},
            -path    => "/",
            -secure  => 0,
            -expires => '-1d',
          );

825
        $self->{login} = 1;
Yadd's avatar
Yadd committed
826
        return PE_CONFIRM;
Clément OUDOT's avatar
Clément OUDOT committed
827
    }
Yadd's avatar
Yadd committed
828

Clément OUDOT's avatar
Clément OUDOT committed
829
    # If IDP is found but not confirmed, let the user confirm it
Yadd's avatar
Yadd committed
830
    elsif ( $confirm_flag != 1 ) {
Clément OUDOT's avatar
Clément OUDOT committed
831 832
        $self->lmLog( "IDP $idp selected, need user confirmation", 'debug' );

833 834 835 836
        # Control url parameter
        my $urlcheck = $self->controlUrlOrigin();
        return $urlcheck unless ( $urlcheck == PE_OK );

Clément OUDOT's avatar
Clément OUDOT committed
837
        # Choosen IDP
Yadd's avatar
Yadd committed
838
        my $html = '<h3>'
839
          . $self->msg(PM_SAML_IDPCHOOSEN)
840
          . "</h3>\n" . "<h4>"
841
          . $self->{_idpList}->{$idp}->{name}
842 843
          . "</h4>\n"
          . "<p><i>"
Clément OUDOT's avatar
SAML:  
Clément OUDOT committed
844
          . $idp
845
          . "</i></p>\n"
846 847
          . "<input type=\"hidden\" name=\"url\" value=\""
          . $self->param("url") . "\" />"
Clément OUDOT's avatar
SAML:  
Clément OUDOT committed
848
          . "<input type=\"hidden\" name=\"idp\" value=\"$idp\" />\n";
Clément OUDOT's avatar
Clément OUDOT committed
849 850 851

        $self->info($html);

852
        $self->{login} = 1;
Yadd's avatar
Yadd committed
853
        return PE_CONFIRM;
Clément OUDOT's avatar
Clément OUDOT committed
854 855 856
    }

    # Here confirmation is OK (confirm_flag == 1), store choosen IDP in cookie
857
    unless ( $idp_cookie and $idp eq $idp_cookie ) {
Clément OUDOT's avatar
Clément OUDOT committed
858 859
        $self->lmLog( "Build cookie to remember $idp as IDP choice", 'debug' );

860 861 862 863
        # Control url parameter
        my $urlcheck = $self->controlUrlOrigin();
        return $urlcheck unless ( $urlcheck == PE_OK );

Yadd's avatar
Yadd committed
864 865 866 867 868 869 870 871 872 873 874 875 876
        # User can choose temporary (0) or persistent cookie (1)
        my $cookie_type = $self->param("cookie_type") || "0";

        push @{ $self->{cookie} },
          $self->cookie(
            -name     => $self->{samlIdPResolveCookie},
            -value    => $idp,
            -domain   => $self->{domain},
            -path     => "/",
            -secure   => $self->{securedCookie},
            -httponly => $self->{httpOnly},
            -expires  => $cookie_type ? "+365d" : "",
          );
Clément OUDOT's avatar
Clément OUDOT committed
877
    }
Yadd's avatar
Yadd committed
878

Clément OUDOT's avatar
Clément OUDOT committed
879
    # 3. Build authentication request
Clément OUDOT's avatar
Clément OUDOT committed
880

Clément OUDOT's avatar
SAML:  
Clément OUDOT committed
881
    # IDP conf key
Clément OUDOT's avatar
SAML:  
Clément OUDOT committed
882
    my $idpConfKey = $self->{_idpList}->{$idp}->{confKey};
Clément OUDOT's avatar
SAML:  
Clément OUDOT committed
883 884 885

    unless ($idpConfKey) {
        $self->lmLog( "$idp do not match any IDP in configuration", 'error' );
Clément OUDOT's avatar
Clément OUDOT committed
886
        return PE_SAML_UNKNOWN_ENTITY;
Clément OUDOT's avatar
SAML:  
Clément OUDOT committed
887 888 889
    }

    $self->lmLog( "$idp match $idpConfKey IDP in configuration", 'debug' );
Clément OUDOT's avatar
Clément OUDOT committed
890

891
    # IDP ForceAuthn