package Lemonldap::NG::Portal::Auth::SAML; use strict; use Mouse; use Lemonldap::NG::Portal::Main::Constants qw( PE_CONFIRM PE_LOGOUT_OK PE_OK PE_SAML_ART_ERROR PE_SAML_CONDITIONS_ERROR PE_SAML_DESTINATION_ERROR PM_SAML_IDPCHOOSEN PE_SAML_IDPSSOINITIATED_NOTALLOWED PE_SAML_SESSION_ERROR PE_SAML_SIGNATURE_ERROR PE_SAML_SLO_ERROR PE_SAML_SSO_ERROR PE_SAML_UNKNOWN_ENTITY ); use Lemonldap::NG::Common::Conf::SAML::Metadata; our $VERSION = '2.0.0'; extends 'Lemonldap::NG::Portal::Auth::Base', 'Lemonldap::NG::Portal::Lib::SAML'; has sloAssConsumerRe => ( is => 'rw' ); has sloRe => ( is => 'rw' ); has artRe => ( is => 'rw' ); # INITIALIZATION sub init { my ($self) = @_; my $saml_acs_art_url = $self->getMetaDataURL( "samlSPSSODescriptorAssertionConsumerServiceHTTPArtifact"); my $saml_acs_post_url = $self->getMetaDataURL( "samlSPSSODescriptorAssertionConsumerServiceHTTPPost"); my $saml_acs_get_url = $self->getMetaDataURL( "samlSPSSODescriptorAssertionConsumerServiceHTTPRedirect"); $self->sloAssConsumerRe( qr/^\Q($saml_acs_art_url|$saml_acs_post_url|$saml_acs_get_url)\E$/i); my $saml_slo_soap_url = $self->getMetaDataURL( "samlSPSSODescriptorSingleLogoutServiceSOAP", 1 ); my $saml_slo_soap_url_ret = $self->getMetaDataURL( "samlSPSSODescriptorSingleLogoutServiceSOAP", 1 ); 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 ); $self->sloRe( qr/^\Q($saml_slo_soap_url|$saml_slo_soap_url_ret|$saml_slo_get_url|$saml_slo_get_url_ret|$saml_slo_post_url|$saml_slo_post_url_ret)\E$/i ); my $saml_ars_url = $self->getMetaDataURL( "samlSPSSODescriptorArtifactResolutionServiceArtifact"); $self->artRe(qr/^(\Q$saml_ars_url\E)$/i); # Load SAML service and SAML IdP list return ( $self->SUPER::init and $self->loadIDPs ); } # RUNNING METHODS sub extractFormInfo { my ( $self, $req ) = @_; # 1. Get HTTP request informations to know # if we are receving SAML request or response my $url = $req->uri; my $request_method = $req->method; my $content_type = $req->contentType; # 1.1 SSO assertion consumer if ( $url =~ $self->sloAssConsumerRe ) { $self->lmLog( "URL $url detected as an SSO assertion consumer URL", 'debug' ); # Check SAML Message my ( $request, $response, $method, $relaystate, $artifact ) = $self->checkMessage( $url, $request_method, $content_type, "login" ); # Create Login object my $login = $self->createLogin( $self->lassoServer ); # Ignore signature verification $self->disableSignatureVerification($login); if ($response) { # Process authentication response my $result; if ($artifact) { $result = $self->processArtResponseMsg( $login, $response ); } else { $result = $self->processAuthnResponseMsg( $login, $response ); } unless ($result) { $self->lmLog( "SSO: Fail to process authentication response", 'error' ); return PE_SAML_SSO_ERROR; } $self->lmLog( "SSO: authentication response is valid", 'debug' ); # Get IDP entityID my $idp = $login->remote_providerID(); $self->lmLog( "Found entityID $idp in SAML message", 'debug' ); # IDP conf key my $idpConfKey = $self->idpList->{$idp}->{confKey}; unless ($idpConfKey) { $self->lmLog( "$idp do not match any IDP in configuration", 'error' ); return PE_SAML_UNKNOWN_ENTITY; } $self->lmLog( "$idp match $idpConfKey IDP in configuration", 'debug' ); # Do we check signature? my $checkSSOMessageSignature = $self->conf->{samlIDPMetaDataOptions}->{$idpConfKey} ->{samlIDPMetaDataOptionsCheckSSOMessageSignature}; if ($checkSSOMessageSignature) { $self->forceSignatureVerification($login); if ($artifact) { $result = $self->processArtResponseMsg( $login, $response ); } else { $result = $self->processAuthnResponseMsg( $login, $response ); } unless ($result) { $self->lmLog( "Signature is not valid", 'error' ); return PE_SAML_SIGNATURE_ERROR; } else { $self->lmLog( "Signature is valid", 'debug' ); } } else { $self->lmLog( "Message signature will not be checked", 'debug' ); } # Get SAML response my $saml_response = $login->response(); unless ($saml_response) { $self->lmLog( "No SAML response found", 'error' ); return PE_SAML_SSO_ERROR; } # Check Destination return PE_SAML_DESTINATION_ERROR unless ( $self->checkDestination( $saml_response, $url ) ); # 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) ) { # Assertion was already consumed or is expired # Force authentication replay $self->lmLog( "Message $assertion_responded already used or expired, replay authentication", 'error' ); delete $req->{urldc}; $req->mustRedirect(1); $req->steps( [] ); return PE_OK; } } else { $self->lmLog( "Assertion is not a response to a created authentication request, do not control replay", 'debug' ); } # Get SAML assertion my $assertion = $self->getAssertion($login); unless ($assertion) { $self->lmLog( "No assertion found", 'error' ); return PE_SAML_SSO_ERROR; } # Do we check conditions? my $checkTime = $self->conf->{samlIDPMetaDataOptions}->{$idpConfKey} ->{samlIDPMetaDataOptionsCheckTime}; my $checkAudience = $self->conf->{samlIDPMetaDataOptions}->{$idpConfKey} ->{samlIDPMetaDataOptionsCheckAudience}; # Check conditions - time and audience unless ( $self->validateConditions( $assertion, $self->getMetaDataURL( "samlEntityID", 0, 1 ), $checkTime, $checkAudience ) ) { $self->lmLog( "Conditions not validated", 'error' ); return PE_SAML_CONDITIONS_ERROR; } my $relayStateURL = $self->conf->{samlIDPMetaDataOptions}->{$idpConfKey} ->{samlIDPMetaDataOptionsRelayStateURL}; # Extract RelayState information if ( $self->extractRelayState( $relaystate, $relayStateURL ) ) { $self->lmLog( "RelayState $relaystate extracted", 'debug' ); } # Check if we accept direct login from IDP my $allowLoginFromIDP = $self->conf->{samlIDPMetaDataOptions}->{$idpConfKey} ->{samlIDPMetaDataOptionsAllowLoginFromIDP}; if ( !$assertion_responded and !$allowLoginFromIDP ) { $self->lmLog( "Direct login from IDP $idpConfKey is not allowed", 'error' ); return PE_SAML_IDPSSOINITIATED_NOTALLOWED; } # Check authentication context my $responseAuthnContext; eval { $responseAuthnContext = $assertion->AuthnStatement()->AuthnContext() ->AuthnContextClassRef(); }; if ($@) { $self->lmLog( "Unable to get authentication context from $idpConfKey", 'debug' ); $responseAuthnContext = $self->getAuthnContext("unspecified"); } else { $self->lmLog( "Found authentication context: $responseAuthnContext", 'debug' ); } # Map authentication context to authentication level $req->sessionInfo->{authenticationLevel} = $self->authnContext2authnLevel($responseAuthnContext); # Force redirection to portal if no urldc found # (avoid displaying the whole SAML URL in user browser URL field) $req->mustRedirect(1) unless ( $req->urldc ); # 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' ); } # Get NameID my $nameid = $login->nameIdentifier; # Set user my $user = $nameid->content; unless ($user) { $self->lmLog( "No NameID value found", 'error' ); return PE_SAML_SSO_ERROR; } $self->lmLog( "Found NameID: $user", 'debug' ); $req->user($user); # Store Lasso objects $req->datas->{_lassoLogin} = $login; $req->datas->{_idp} = $idp; $req->datas->{_idpConfKey} = $idpConfKey; $req->datas->{_nameID} = $nameid; $req->datas->{_sessionIndex} = $session_index; # Store Token my $saml_token = $assertion->export_to_xml; $self->lmLog( "SAML Token: $saml_token", 'debug' ); $req->datas->{_samlToken} = $saml_token; # Restore initial SAML request in case of proxying my $moduleOptions = $self->conf->{samlStorageOptions} || {}; $moduleOptions->{backend} = $self->conf->{samlStorage}; my $module = "Lemonldap::NG::Common::Apache::Session"; my $saml_sessions = $module->searchOn( $moduleOptions, "ProxyID", $assertion_responded ); 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); $req->datas->{_proxiedRequest} = $samlSessionInfo->data->{Request}; $req->datas->{_proxiedMethod} = $samlSessionInfo->data->{Method}; $req->datas->{_proxiedRelayState} = $samlSessionInfo->data->{RelayState}; $req->datas->{_proxiedArtifact} = $samlSessionInfo->data->{Artifact}; # Save values in hidden fields in case of other user interactions $self->setHiddenFormValue( 'SAMLRequest', $req->datas->{_proxiedRequest} ); $self->setHiddenFormValue( 'Method', $req->datas->{_proxiedMethod} ); $self->setHiddenFormValue( 'RelayState', $req->datas->{_proxiedRelayState} ); $self->setHiddenFormValue( 'SAMLart', $req->datas->{_proxiedArtifact} ); # Delete session $samlSessionInfo->remove(); } return PE_OK; } 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 $req->mustRedirect(1); $req->steps( [] ); return PE_OK; } } # 1.2 SLO elsif ( $url =~ $self->sloRe ) { $self->lmLog( "URL $url detected as an SLO URL", 'debug' ); # Check SAML Message my ( $request, $response, $method, $relaystate, $artifact ) = $self->checkMessage( $url, $request_method, $content_type, "logout" ); # Create Logout object my $logout = $self->createLogout( $self->lassoServer ); # Ignore signature verification $self->disableSignatureVerification($logout); if ($response) { # Process logout response my $result = $self->processLogoutResponseMsg( $logout, $response ); unless ($result) { $self->lmLog( "Fail to process logout response", 'error' ); return PE_SAML_SLO_ERROR; } $self->lmLog( "Logout response is valid", 'debug' ); # Check Destination return PE_SAML_DESTINATION_ERROR unless ( $self->checkDestination( $logout->response, $url ) ); # Get IDP entityID my $idp = $logout->remote_providerID(); $self->lmLog( "Found entityID $idp in SAML message", 'debug' ); # IDP conf key my $idpConfKey = $self->idpList->{$idp}->{confKey}; unless ($idpConfKey) { $self->lmLog( "$idp do not match any IDP in configuration", 'error' ); return PE_SAML_UNKNOWN_ENTITY; } $self->lmLog( "$idp match $idpConfKey IDP in configuration", 'debug' ); # Do we check signature? my $checkSLOMessageSignature = $self->conf->{samlIDPMetaDataOptions}->{$idpConfKey} ->{samlIDPMetaDataOptionsCheckSLOMessageSignature}; if ($checkSLOMessageSignature) { $self->forceSignatureVerification($logout); $result = $self->processLogoutResponseMsg( $logout, $response ); unless ($result) { $self->lmLog( "Signature is not valid", 'error' ); return PE_SAML_SIGNATURE_ERROR; } else { $self->lmLog( "Signature is valid", 'debug' ); } } else { $self->lmLog( "Message signature will not be checked", 'debug' ); } # Replay protection my $samlID = $logout->response()->InResponseTo; unless ( $self->replayProtection($samlID) ) { # Logout request was already consumed or is expired $self->lmLog( "Message $samlID already used or expired", 'error' ); return PE_SAML_SLO_ERROR; } # If URL in RelayState, different from portal, redirect user if ( $self->extractRelayState($relaystate) ) { $self->lmLog( "RelayState $relaystate extracted", 'debug' ); $self->lmLog( "URL " . $req->urldc . " found in RelayState", 'debug' ); } if ( $req->urldc and $self->conf->{portal} !~ /\Q$req->{urldc}\E\/?/ ) { $req->steps( [] ); return PE_OK; } # Else, inform user that logout is OK 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' ); $logout_error = 1; } $self->lmLog( "Logout request is valid", 'debug' ); # Check Destination return PE_SAML_DESTINATION_ERROR unless ( $self->checkDestination( $logout->request, $url ) ); # Get IDP entityID my $idp = $logout->remote_providerID(); $self->lmLog( "Found entityID $idp in SAML message", 'debug' ); # IDP conf key my $idpConfKey = $self->idpList->{$idp}->{confKey}; unless ($idpConfKey) { $self->lmLog( "$idp do not match any IDP in configuration", 'error' ); return PE_SAML_UNKNOWN_ENTITY; } $self->lmLog( "$idp match $idpConfKey IDP in configuration", 'debug' ); # Do we check signature? my $checkSLOMessageSignature = $self->conf->{samlIDPMetaDataOptions}->{$idpConfKey} ->{samlIDPMetaDataOptionsCheckSLOMessageSignature}; if ($checkSLOMessageSignature) { unless ( $self->checkSignatureStatus($logout) ) { $self->lmLog( "Signature is not valid", 'error' ); return PE_SAML_SIGNATURE_ERROR; } else { $self->lmLog( "Signature is valid", 'debug' ); } } else { $self->lmLog( "Message signature will not be checked", 'debug' ); } # Get NameID and SessionIndex my $name_id = $logout->request()->NameID; my $session_index = $logout->request()->SessionIndex; my $user = $name_id->content; unless ($name_id) { $self->lmLog( "Fail to get NameID from logout request", 'error' ); $logout_error = 1; } $self->lmLog( "Logout request NameID content: $user", 'debug' ); # Get SAML sessions with the same NameID my $moduleOptions = $self->conf->{samlStorageOptions} || {}; $moduleOptions->{backend} = $self->conf->{samlStorage}; my $module = "Lemonldap::NG::Common::Apache::Session"; my $local_sessions = $module->searchOn( $moduleOptions, "_nameID", $name_id->dump ); if ( my @local_sessions_keys = keys %$local_sessions ) { # At least one session was found foreach (@local_sessions_keys) { my $local_session = $_; # Get session $self->lmLog( "Retrieve SAML session $local_session for user $user", 'debug' ); my $sessionInfo = $self->getSamlSession($local_session); # If session index is defined and not equal to SAML session index, # jump to next session if ( defined $session_index and $session_index ne $sessionInfo->data->{_sessionIndex} ) { $self->lmLog( "Session $local_session has not the good session index, skipping", 'debug' ); next; } # Delete session else { # Open real session my $real_session = $sessionInfo->data->{_saml_id}; my $ssoSession = $self->getApacheSession( $real_session, 1 ); # Get Lasso::Session dump # This value is erased if a next session match the SLO request if ( $ssoSession && $ssoSession->data->{_lassoSessionDump} ) { $self->lmLog( "Get Lasso::Session dump from session $real_session", 'debug' ); $session_dump = $ssoSession->data->{_lassoSessionDump}; } # Delete real session my $del_real_result = $self->_deleteSession($ssoSession); $self->lmLog( "Delete real session $real_session result: $del_real_result", 'debug' ); $logout_error = 1 unless $del_real_result; # Delete SAML session my $del_saml_result = $sessionInfo->remove(); $self->lmLog( "Delete SAML session $local_session result: $del_saml_result", 'debug' ); $logout_error = 1 unless $del_saml_result; } } # Set session from dump unless ( $self->setSessionFromDump( $logout, $session_dump ) ) { $self->lmLog( "Cannot set session from dump in logout", 'error' ); $logout_error = 1; } } else { # No corresponding session found $self->lmLog( "No SAML session found for user $user", 'debug' ); $logout_error = 1; } # Validate request if no previous error unless ($logout_error) { unless ( $self->validateLogoutRequest($logout) ) { $self->lmLog( "SLO request is not valid", 'error' ); } } # Set RelayState if ($relaystate) { $logout->msg_relayState($relaystate); $self->lmLog( "Set $relaystate in RelayState", 'debug' ); } # Do we set signature? my $signSLOMessage = $self->conf->{samlIDPMetaDataOptions}->{$idpConfKey} ->{samlIDPMetaDataOptionsSignSLOMessage}; if ( $signSLOMessage == 0 ) { $self->lmLog( "SLO message to IDP $idpConfKey will not be signed", 'debug' ); $self->disableSignature($logout); } 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' ); } # Logout response unless ( $self->buildLogoutResponseMsg($logout) ) { $self->lmLog( "Unable to build SLO response", 'error' ); return PE_SAML_SLO_ERROR; } # Send response depending on request method # HTTP-REDIRECT no strict 'subs'; 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' ); $req->urldc($slo_url); $req->steps( [] ); return PE_OK; } # HTTP-POST elsif ( $method == Lasso::Constants::HTTP_METHOD_POST ) { # Use autosubmit form my $slo_url = $logout->msg_url; my $slo_body = $logout->msg_body; $req->postUrl($slo_url); $req->postFields = { 'SAMLResponse' => $slo_body }; # RelayState $req->postFields->{'RelayState'} = $relaystate if ($relaystate); # TODO: verify this $req->steps( ['autoPost'] ); return PE_OK; } # HTTP-SOAP elsif ( $method == Lasso::Constants::HTTP_METHOD_SOAP ) { my $slo_body = $logout->msg_body; $self->lmLog( "SOAP response $slo_body", 'debug' ); $req->datas->{SOAPMessage} = $slo_body; # TODO: check this $req->steps( ['returnSOAPMessage'] ); return PE_OK; # If we are here, there was a problem with SOAP response $self->lmLog( "Logout response was not sent trough SOAP", 'error' ); return PE_SAML_SLO_ERROR; } } else { # This should not happen $self->lmLog( "SLO request or response was not found", 'error' ); # Redirect user $req->mustRedirect(1); $req->steps( [] ); return PE_OK; } } # 1.3 Artifact elsif ( $url =~ $self->artRe ) { $self->lmLog( "URL $url detected as an artifact resolution service URL", 'debug' ); # Artifact request are sent with SOAP trough POST my $art_request = $req->param('POSTDATA'); my $art_response; # Create Login object my $login = $self->createLogin( $self->lassoServer ); # Process request message unless ( $self->processArtRequestMsg( $login, $art_request ) ) { $self->lmLog( "Unable to process artifact request message", 'error' ); return PE_SAML_ART_ERROR; } # Check Destination return PE_SAML_DESTINATION_ERROR unless ( $self->checkDestination( $login->request, $url ) ); # Create artifact response unless ( $art_response = $self->createArtifactResponse($login) ) { $self->lmLog( "Unable to create artifact response message", 'error' ); return PE_SAML_ART_ERROR; } $req->datas->{SOAPMessage} = $art_response; $self->lmLog( "Send SOAP Message: " . $req->datas->{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' ); return PE_SAML_ART_ERROR; } # 2. IDP resolution # Search a selected IdP my ( $idp, $idp_cookie ) = $self->getIDP($req); # Get confirmation flag my $confirm_flag = $req->param("confirm") // 0; # If confirmation is -1 from resolved IDP screen, # or IDP was not resolve, let the user choose its IDP if ( $confirm_flag == -1 or !$idp ) { $self->lmLog( "Redirecting user to IDP list", 'debug' ); # Control url parameter my $urlcheck = $self->p->controlUrl($req); return $urlcheck unless ( $urlcheck == PE_OK ); # IDP list my @list = (); foreach ( keys %{ $self->idpList } ) { push @list, { val => $_, name => $self->idpList->{$_}->{name} }; } $req->datas->{list} = \@list; $req->datas->{confirmRemember} = 1; # Delete existing IDP resolution cookie push @{ $req->respHeaders }, 'Set-Cookie' => $self->p->cookie( name => $self->conf->{samlIdPResolveCookie}, value => 0, domain => $self->conf->{domain}, path => "/", secure => 0, expires => '-1d', ); #TODO: check this $req->datas->{login} = 1; return PE_CONFIRM; } # If IDP is found but not confirmed, let the user confirm it elsif ( $confirm_flag != 1 ) { $self->lmLog( "IDP $idp selected, need user confirmation", 'debug' ); # Control url parameter my $urlcheck = $self->controlUrlOrigin(); return $urlcheck unless ( $urlcheck == PE_OK ); # Choosen IDP my $html = '
" . $idp . "
\n" . "param("url") . "\" />" . "\n"; $self->info($html); $req->datas->{login} = 1; return PE_CONFIRM; } # Here confirmation is OK (confirm_flag == 1), store choosen IDP in cookie unless ( $idp_cookie and $idp eq $idp_cookie ) { $self->lmLog( "Build cookie to remember $idp as IDP choice", 'debug' ); # Control url parameter my $urlcheck = $self->controlUrlOrigin(); return $urlcheck unless ( $urlcheck == PE_OK ); # User can choose temporary (0) or persistent cookie (1) my $cookie_type = $req->param("cookie_type") || "0"; push @{ $req->{respHeaders} }, 'Set-Cookie' => $self->cookie( name => $self->conf->{samlIdPResolveCookie}, value => $idp, domain => $self->conf->{domain}, path => "/", secure => $self->conf->{securedCookie}, HttpOnly => $self->conf->{httpOnly}, expires => $cookie_type ? "+365d" : "", ); } # 3. Build authentication request # IDP conf key my $idpConfKey = $self->idpList->{$idp}->{confKey}; unless ($idpConfKey) { $self->lmLog( "$idp do not match any IDP in configuration", 'error' ); return PE_SAML_UNKNOWN_ENTITY; } $self->lmLog( "$idp match $idpConfKey IDP in configuration", 'debug' ); # IDP ForceAuthn my $forceAuthn = $self->conf->{samlIDPMetaDataOptions}->{$idpConfKey} ->{samlIDPMetaDataOptionsForceAuthn}; # IDP IsPassive my $isPassive = $self->conf->{samlIDPMetaDataOptions}->{$idpConfKey} ->{samlIDPMetaDataOptionsIsPassive}; # IDP NameIDFormat my $nameIDFormat = $self->conf->{samlIDPMetaDataOptions}->{$idpConfKey} ->{samlIDPMetaDataOptionsNameIDFormat}; $nameIDFormat = $self->getNameIDFormat($nameIDFormat) if $nameIDFormat; # IDP ProxyRestriction my $allowProxiedAuthn = $self->conf->{samlIDPMetaDataOptions}->{$idpConfKey} ->{samlIDPMetaDataOptionsAllowProxiedAuthn}; # IDP HTTP method my $method = $self->conf->{samlIDPMetaDataOptions}->{$idpConfKey} ->{samlIDPMetaDataOptionsSSOBinding}; $method = $self->getHttpMethod($method); # If no method defined, get first HTTP method unless ( defined $method ) { no strict 'subs'; my $protocolType = Lasso::Constants::MD_PROTOCOL_TYPE_SINGLE_SIGN_ON; $method = $self->getFirstHttpMethod( $self->lassoServer, $idp, $protocolType ); } # Failback to HTTP-REDIRECT unless ( defined $method and $method != -1 ) { $self->lmLog( "No method found with IDP $idpConfKey for SSO profile", 'debug' ); $method = $self->getHttpMethod("redirect"); } $self->lmLog( "Use method " . $self->getHttpMethodString($method) . " with IDP $idpConfKey for SSO profile", 'debug' ); # Set signature my $signSSOMessage = $self->conf->{samlIDPMetaDataOptions}->{$idpConfKey} ->{samlIDPMetaDataOptionsSignSSOMessage}; # Authentication Context my $requestedAuthnContext = $self->conf->{samlIDPMetaDataOptions}->{$idpConfKey} ->{samlIDPMetaDataOptionsRequestedAuthnContext}; $requestedAuthnContext = $self->getAuthnContext($requestedAuthnContext) if $requestedAuthnContext; # Create SSO request my $login = $self->createAuthnRequest( $self->lassoServer, $idp, $method, $forceAuthn, $isPassive, $nameIDFormat, $allowProxiedAuthn, $signSSOMessage, $requestedAuthnContext ); unless ($login) { $self->lmLog( "Could not create authentication request on $idpConfKey", 'error' ); return PE_SAML_SSO_ERROR; } $self->lmLog( "Authentication request created", 'debug' ); # Keep assertion ID in memory to prevent replay my $samlID = $login->request()->ID; unless ( $self->storeReplayProtection($samlID) ) { $self->lmLog( "Unable to store assertion ID", 'error' ); return PE_SAML_SSO_ERROR; } # Keep initial SAML request data in memory in case of proxing if ( $req->datas->{_proxiedSamlRequest} ) { my $samlSessionInfo = $self->getSamlSession(); return PE_SAML_SESSION_ERROR unless $samlSessionInfo; my $infos; $infos->{type} = 'proxy'; $infos->{_utime} = time; $infos->{Request} = $req->datas->{_proxiedRequest}; $infos->{Method} = $req->datas->{_proxiedMethod}; $infos->{RelayState} = $req->datas->{_proxiedRelayState}; $infos->{Artifact} = $req->datas->{_proxiedArtifact}; $infos->{ProxyID} = $samlID; $samlSessionInfo->update($infos); $self->lmLog( "Keep initial SAML request data in memory for ID $samlID", 'debug' ); } # Send SSO request depending on request method # HTTP-REDIRECT if ( $method == $self->getHttpMethod('redirect') or $method == $self->getHttpMethod('artifact-get') ) { # Redirect user to response URL my $sso_url = $login->msg_url; $self->lmLog( "Redirect user to $sso_url", 'debug' ); $req->urldc($sso_url); $req->steps( [] ); return PE_OK; } # HTTP-POST elsif ($method == $self->getHttpMethod('post') or $method == $self->getHttpMethod('artifact-post') ) { # Use autosubmit form my $sso_url = $login->msg_url; my $sso_body = $login->msg_body; $self->postUrl($sso_url); if ( $method == $self->getHttpMethod("artifact-post") ) { $req->{postFields} = { 'SAMLart' => $sso_body }; } else { $req->{postFields} = { 'SAMLRequest' => $sso_body }; } # RelayState $req->{postFields}->{'RelayState'} = $login->msg_relayState if ( $login->msg_relayState ); # TODO: verify this $req->steps( ['autoPost'] ); return PE_OK; } # No SOAP transport for SSO request } sub authenticate { my ( $self, $req ) = @_; my $server = $self->lassoServer; my $login = $req->datas->{_lassoLogin}; my $idp = $req->datas->{_idp}; my $idpConfKey = $req->datas->{_idpConfKey}; # Get SAML assertion my $assertion = $self->getAssertion($login); unless ($assertion) { $self->lmLog( "No assertion found", 'error' ); return PE_SAML_SSO_ERROR; } # Force UTF-8 my $force_utf8 = $self->conf->{samlIDPMetaDataOptions}->{$idpConfKey} ->{samlIDPMetaDataOptionsForceUTF8}; # Try to get attributes if attribute statement is present in assertion my $attr_statement = $assertion->AttributeStatement(); if ($attr_statement) { # Get attributes my @attributes = $attr_statement->Attribute(); # Wanted attributes are defined in IDP configuration foreach ( keys %{ $self->conf->{samlIDPMetaDataExportedAttributes}->{$idpConfKey} } ) { # Extract fields from exportedAttr value my ( $mandatory, $name, $format, $friendly_name ) = split( /;/, $self->conf->{samlIDPMetaDataExportedAttributes}->{$idpConfKey} ->{$_} ); # Try to get value my $value = $self->getAttributeValue( $name, $format, $friendly_name, \@attributes, $force_utf8 ); # Store value in sessionInfo $req->{sessionInfo}->{$_} = $value if defined $value; } } # Store other informations in session $req->{sessionInfo}->{_idp} = $idp; $req->{sessionInfo}->{_idpConfKey} = $idpConfKey; # Adapt _utime with SessionNotOnOrAfter my $sessionNotOnOrAfter; eval { $sessionNotOnOrAfter = $assertion->AuthnStatement()->SessionNotOnOrAfter(); }; if ( $@ or !$sessionNotOnOrAfter ) { $self->lmLog( "No SessionNotOnOrAfter value found", 'debug' ); } else { my $samltime = $self->samldate2timestamp($sessionNotOnOrAfter); my $utime = time(); my $timeout = $self->conf->{timeout}; my $adaptSessionUtime = $self->conf->{samlIDPMetaDataOptions}->{$idpConfKey} ->{samlIDPMetaDataOptionsAdaptSessionUtime}; if ( ( $utime + $timeout > $samltime ) and $adaptSessionUtime ) { # Use SAML time to determine the start of the session my $new_utime = $samltime - $timeout; $req->{sessionInfo}->{_utime} = $new_utime; $self->lmLog( "Adapt _utime with SessionNotOnOrAfter value, new _utime: $new_utime", 'debug' ); } } # Establish federation (required for attribute request in UserDBSAML) unless ( $self->acceptSSO($login) ) { $self->lmLog( "Error while accepting SSO from IDP $idpConfKey", 'error' ); return PE_SAML_SSO_ERROR; } # Get created Lasso::Session and Lasso::Identity my $session = $login->get_session; my $identity = $login->get_identity; # Dump Lasso objects in session $req->{sessionInfo}->{_lassoSessionDump} = $session->dump() if $session; $req->{sessionInfo}->{_lassoIdentityDump} = $identity->dump() if $identity; # Keep SAML Token in session my $store_samlToken = $self->conf->{samlIDPMetaDataOptions}->{$idpConfKey} ->{samlIDPMetaDataOptionsStoreSAMLToken}; if ($store_samlToken) { $self->lmLog( "Store SAML Token in session", 'debug' ); $req->{sessionInfo}->{_samlToken} = $req->datas->{_samlToken}; } else { $self->lmLog( "SAML Token will not be stored in session", 'debug' ); } $req->datas->{_lassoLogin} = $login; push @{ $req->steps }, sub { $self->authFinish(@_) }; PE_OK; } # Inserted in $req->steps by authenticate() sub authFinish { my ( $self, $req ) = @_; my %h; # Real session was stored, get id and utime my $id = $req->{id}; my $utime = $req->{sessionInfo}->{_utime}; # Get saved Lasso objects my $nameid = $req->datas->{_nameID}; my $session_index = $req->datas->{_sessionIndex}; $self->lmLog( "Store NameID " . $nameid->dump . " and SessionIndex $session_index for session $id", 'debug' ); # Save SAML session my $samlSessionInfo = $self->getSamlSession(); return PE_SAML_SESSION_ERROR unless $samlSessionInfo; my $infos; $infos->{type} = 'saml'; # Session type $infos->{_utime} = $utime; # Creation time $infos->{_saml_id} = $id; # SSO session id $infos->{_nameID} = $nameid->dump; # SAML NameID $infos->{_sessionIndex} = $session_index; # SAML SessionIndex $samlSessionInfo->update($infos); my $session_id = $samlSessionInfo->id; $self->lmLog( "Link session $id to SAML session $session_id", 'debug' ); return PE_OK; } sub authLogout { my ( $self, $req ) = @_; my $idp = $req->sessionInfo->{_idp}; my $idpConfKey = $req->sessionInfo->{_idpConfKey}; my $session_id = $req->sessionInfo->{_session_id}; my $method; # Real session was previously deleted, # remove corresponding SAML sessions $self->deleteSAMLSecondarySessions($session_id); my $server = $self->lassoServer; # Recover Lasso::Session dump my $session_dump = $req->{sessionInfo}->{_lassoSessionDump}; unless ($session_dump) { $self->lmLog( "Could not get session dump from session", 'error' ); return PE_SAML_SLO_ERROR; } # IDP HTTP method $method = $self->conf->{samlIDPMetaDataOptions}->{$idpConfKey} ->{samlIDPMetaDataOptionsSLOBinding}; $method = $self->getHttpMethod($method); # If no method defined, get first HTTP method no strict 'subs'; unless ( defined $method ) { my $protocolType = Lasso::Constants::MD_PROTOCOL_TYPE_SINGLE_LOGOUT; $method = $self->getFirstHttpMethod( $server, $idp, $protocolType ); } # Skip SLO if no method found unless ( defined $method and $method != -1 ) { $self->lmLog( "No method found with IDP $idpConfKey for SLO profile", 'debug' ); return PE_OK; } $self->lmLog( "Use method " . $self->getHttpMethodString($method) . " with IDP $idpConfKey for SLO profile", 'debug' ); # Set signature my $signSLOMessage = $self->conf->{samlIDPMetaDataOptions}->{$idpConfKey} ->{samlIDPMetaDataOptionsSignSLOMessage}; # Build Logout Request my $logout = $self->createLogoutRequest( $server, $session_dump, $method, $signSLOMessage ); unless ($logout) { $self->lmLog( "Could not create logout request", 'error' ); return PE_SAML_SLO_ERROR; } $self->lmLog( "Logout request created", 'debug' ); # Keep request ID in memory to prevent replay unless ( $self->storeReplayProtection( $logout->request()->ID ) ) { $self->lmLog( "Unable to store Logout request ID", 'error' ); return PE_SAML_SLO_ERROR; } # Send request 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' ); $req->urldc($slo_url); # Redirect done in Portal/Simple.pm return PE_OK; } # HTTP-POST elsif ( $method == Lasso::Constants::HTTP_METHOD_POST ) { # Use autosubmit form my $slo_url = $logout->msg_url; my $slo_body = $logout->msg_body; $req->postUrl($slo_url); $self->postFields( { 'SAMLRequest' => $slo_body } ); # RelayState $self->postFields->{'RelayState'} = $logout->msg_relayState if ( $logout->msg_relayState ); # Post done in Portal/Simple.pm return PE_OK; } # HTTP-SOAP elsif ( $method == Lasso::Constants::HTTP_METHOD_SOAP ) { my $slo_url = $logout->msg_url; my $slo_body = $logout->msg_body; # Send SOAP request and manage response my $response = $self->sendSOAPMessage( $slo_url, $slo_body ); unless ($response) { $self->lmLog( "No logout response to SOAP request", 'error' ); return PE_SAML_SLO_ERROR; } # Create Logout object $logout = $self->createLogout($server); # Process logout response my $result = $self->processLogoutResponseMsg( $logout, $response ); unless ($result) { $self->lmLog( "Fail to process logout response", 'error' ); return PE_SAML_SLO_ERROR; } $self->lmLog( "Logout response is valid", 'debug' ); # Replay protection my $samlID = $logout->response()->InResponseTo; unless ( $self->replayProtection($samlID) ) { # Logout request was already consumed or is expired $self->lmLog( "Message $samlID already used or expired", 'error' ); return PE_SAML_SLO_ERROR; } return PE_OK; } } # TODO: authForce sub getDisplayType { return "logo"; } # Internal methods # Try to find an IdP using : # * HTTP parameter # * "samlIdPResolveCookie" cookie # * Rules # * Common Domain Cookie # # @return Array containing : # * IdP found (or undef) # * Cookie value if exists sub getIDP { my ( $self, $req ) = @_; my $idp; my $idpName; my $idp_cookie; if ( $req->cookies && $req->cookies =~ /$self->{conf}->{samlIdPResolveCookie}=([^,; ]+)/o ) { $idp_cookie = $1; } # Case 1: Recover IDP from idp URL Parameter unless ( $idp = $req->param("idp") ) { # Case 2: Recover IDP from idpName URL Parameter if ( $idpName = $req->param("idpName") ) { foreach ( keys %{ $self->idpList } ) { my $idpConfKey = $self->idpList->{$_}->{confKey}; if ( $idpName eq $idpConfKey ) { $idp = $_; $self->lmLog( "IDP $idp found from idpName URL Parameter ($idpName)", 'debug' ); last; } } } # Case 3: Recover IDP from cookie if ( !$idp and $idp = $idp_cookie ) { $self->lmLog( "IDP $idp found in IDP resolution cookie", 'debug' ); } # Case 4: check all IDP resolution rules # The first match win else { foreach ( keys %{ $self->idpList } ) { my $idpConfKey = $self->idpList->{$_}->{confKey}; my $cond = $self->conf->{samlIDPMetaDataOptions}->{$idpConfKey} ->{samlIDPMetaDataOptionsResolutionRule}; next unless defined $cond; if ( $self->safe->reval($cond) ) { $self->lmLog( "IDP $idpConfKey resolution rule match", 'debug' ); $idp = $_; last; } } } # Case 5: use Common Domain Cookie if ( !$idp and $self->conf->{samlCommonDomainCookieActivation} and $self->conf->{samlCommonDomainCookieReader} ) { $self->lmLog( "Will try to use Common Domain Cookie for IDP resolution", 'debug' ); # Add current URL to CDC Reader URL my $return_url = encode_base64( $self->self_url(), '' ); my $cdc_reader_url = $self->conf->{samlCommonDomainCookieReader}; $cdc_reader_url .= ( $self->conf->{samlCommonDomainCookieReader} =~ /\?/ ? '&u->confrl=' . $return_url : '?url=' . $return_url ); $self->lmLog( "Redirect user to $cdc_reader_url", 'debug' ); $req->urldc($cdc_reader_url); $req->steps( [] ); return PE_OK; } $self->lmLog( 'No IDP found', 'debug' ) unless ($idp); } # Alert when selected IDP is unknown if ( $idp and !exists $self->idpList->{$idp} ) { $self->userError("Required IDP $idp does not exists"); $idp = undef; } return ( $idp, $idp_cookie ); } 1;