package Lemonldap::NG::Portal::Issuer::SAML; use strict; use Mouse; use URI; use URI::QueryParam; use HTML::Entities qw(encode_entities); use Lemonldap::NG::Portal::Lib::SAML; use Lemonldap::NG::Portal::Main::Constants qw( PE_OK PE_REDIRECT PE_SESSIONEXPIRED 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_ISSUERMISSINGREQATTR PE_SAML_UNKNOWN_ENTITY PE_SAML_SERVICE_NOT_ALLOWED PE_UNAUTHORIZEDPARTNER ); our $VERSION = '2.0.14'; extends 'Lemonldap::NG::Portal::Main::Issuer', 'Lemonldap::NG::Portal::Lib::SAML'; has rule => ( is => 'rw' ); has ssoUrlRe => ( is => 'rw' ); has ssoUrlArtifact => ( is => 'rw' ); has ssoGetUrl => ( is => 'rw' ); use constant sessionKind => 'ISAML'; use constant lsDump => '_lassoSessionDumpI'; use constant liDump => '_lassoIdentityDumpI'; # INTERFACE # Simply store SP in $req->env use constant beforeAuth => 'storeEnv'; # INITIALIZATION sub init { my ($self) = @_; # Parse activation rule my $hd = $self->p->HANDLER; $self->logger->debug( "SAML rule -> " . $self->conf->{issuerDBSAMLRule} ); my $rule = $hd->buildSub( $hd->substitute( $self->conf->{issuerDBSAMLRule} ) ); unless ($rule) { my $error = $hd->tsv->{jail}->error || '???'; $self->error("Bad SAML activation rule -> $error"); return 0; } $self->{rule} = $rule; # Prepare SSO URL catching my $saml_sso_get_url = $self->ssoGetUrl( $self->getMetaDataURL( "samlIDPSSODescriptorSingleSignOnServiceHTTPRedirect", 1 ) ); 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 ); $self->ssoUrlRe( 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 ); $self->ssoUrlArtifact( qr/^($saml_sso_art_url|$saml_sso_art_url_ret)(?:\?.*)?$/i); # Launch parents initialization subroutines, then launch IdP en SP lists my $res = ( $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() ); return 0 unless ($res); if ( $self->conf->{samlOverrideIDPEntityID} ) { $self->lassoServer->ProviderID( $self->conf->{samlOverrideIDPEntityID} ); } # 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'] ); # SOAP routes (access without authentication) $self->addRouteFromMetaDataURL( 'samlIDPSSODescriptorArtifactResolutionServiceArtifact', 3, 'artifactServer', ['POST'] ); $self->addRouteFromMetaDataURL( 'samlAttributeAuthorityDescriptorAttributeServiceSOAP', 1, 'attributeServer', ['POST'] ); $self->addUnauthRoute( $self->path => { relaySingleLogoutSOAP => 'sloRelaySoap' }, [ 'GET', 'POST' ] ); $self->addAuthRoute( $self->path => { relaySingleLogoutPOST => 'sloRelayPost' }, [ 'GET', 'POST' ] ); $self->addUnauthRoute( $self->path => { relaySingleLogoutPOST => 'sloRelayPost' }, [ 'GET', 'POST' ] ); $self->addUnauthRoute( $self->path => { singleLogoutResume => 'sloResume' }, [ 'GET', 'POST' ] ); $self->addUnauthRoute( $self->path => { relaySingleLogoutTermination => 'sloRelayTerm' }, [ 'GET', 'POST' ] ); return $res; } # RUNNING METHODS # "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 ); # This doesn't always work, especially when coming from a HTTP-Redirect flow and # POST-ing the login form 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 ); $self->disableSignatureVerification($login); $self->processAuthnRequestMsg( $login, $request, 'debug' ); 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; # Store target authentication level in pdata my $targetAuthnLevel = $self->conf->{samlSPMetaDataOptions}->{$spConfKey} ->{samlSPMetaDataOptionsAuthnLevel}; $req->pdata->{targetAuthnLevel} = $targetAuthnLevel if $targetAuthnLevel; } if ( $login->request ) { my $acs = $login->request->AssertionConsumerServiceURL; $req->env->{llng_saml_acs} = $acs if $acs; } } return PE_OK; } sub ssoMatch { my ( $self, $req ) = @_; my $url = $self->normalize_url( $req->uri, $self->conf->{issuerDBSAMLPath}, $self->ssoGetUrl ); return ( $url =~ $self->ssoUrlRe or $req->data->{_proxiedRequest} ? 1 : 0 ); } # Main method (launched only for authenticated users, see Main/Issuer) sub run { my ( $self, $req ) = @_; my $login; my $protocolProfile; my $artifact_method; my $authn_context; # Check activation rule unless ( $self->rule->( $req, $req->sessionInfo ) ) { $self->userLogger->error('SAML service not authorized'); return PE_SAML_SERVICE_NOT_ALLOWED; } # Session ID my $session_id = $req->{sessionInfo}->{_session_id} || $req->id; # Session creation timestamp my $time = $req->{sessionInfo}->{_utime} || time(); # Get HTTP request information to know # if we are receving SAML request or response my $url = $req->uri; my $request_method = $req->param('issuerMethod') || $req->method; my $content_type = $req->content_type(); my $idp_initiated = $req->param('IDPInitiated'); my $idp_initiated_sp = $req->param('sp'); my $idp_initiated_spConfKey = $req->param('spConfKey'); my $idp_initiated_spDest = $req->param('spDest'); # Normalize URL to be tolerant to SAML Path $url = $self->normalize_url( $url, $self->conf->{issuerDBSAMLPath}, $self->ssoGetUrl ); # Get domain GET attribute my $domain = $req->param('domain'); if ($domain) { $self->logger->debug("Found domain $domain in SAML GET parameter"); } # 1.1. SSO (SSO URL or Proxy Mode) if ( $url =~ $self->ssoUrlRe or $req->data->{_proxiedRequest} ) { $self->logger->debug("URL $url detected as an SSO request URL"); # Check message my ( $request, $response, $method, $relaystate, $artifact ) = $self->checkMessage( $req, $url, $request_method, $content_type ); # Create Login object my $login = $self->createLogin( $self->lassoServer ); # Ignore signature verification $self->disableSignatureVerification($login); if ($request) { $req->data->{_proxiedSamlRequest} = $login->request(); $req->data->{_proxiedRequest} = $request; $req->data->{_proxiedMethod} = $method; $req->data->{_proxiedRelayState} = $relaystate, $req->data->{_proxiedArtifact} = $artifact; } # Process the request or use IDP initiated mode if ( $request or $idp_initiated ) { # Load Session and Identity if they exist my $session = $req->{sessionInfo}->{ $self->lsDump }; my $identity = $req->{sessionInfo}->{ $self->liDump }; if ($session) { unless ( $self->setSessionFromDump( $login, $session ) ) { $self->logger->error("Unable to load Lasso Session"); return PE_SAML_SSO_ERROR; } $self->logger->debug("Lasso Session loaded"); } if ($identity) { unless ( $self->setIdentityFromDump( $login, $identity ) ) { $self->logger->error("Unable to load Lasso Identity"); return PE_SAML_SSO_ERROR; } $self->logger->debug("Lasso Identity loaded"); } 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 ) { $self->userLogger->warn( "sp or spConfKey parameter needed to make IDP initiated SSO" ); 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} ) { $self->userLogger->error( "SP $idp_initiated_sp not known"); 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} ) { $self->userLogger->error( "IDP Initiated SSO not allowed for SP $idp_initiated_spConfKey" ); return PE_SAML_SSO_ERROR; } $result = $self->initIdpInitiatedAuthnRequest( $login, $idp_initiated_sp ); unless ($result) { $self->logger->error( "SSO: Fail to init IDP Initiated authentication request" ); return PE_SAML_SSO_ERROR; } # Force NameID Format my $nameIDFormatKey = $self->conf->{samlSPMetaDataOptions} ->{$idp_initiated_spConfKey} ->{samlSPMetaDataOptionsNameIDFormat} || "email"; eval { $login->request()->NameIDPolicy() ->Format( $self->getNameIDFormat($nameIDFormatKey) ); }; # Force AllowCreate to TRUE eval { $login->request()->NameIDPolicy()->AllowCreate(1); }; # Allow selection the AssertionConsumerServiceURL by the user if ($idp_initiated_spDest) { $login->request->AssertionConsumerServiceURL( $idp_initiated_spDest); } } # Process authentication request if ($artifact) { $result = $self->processArtResponseMsg( $login, $request ); } else { $result = $self->processAuthnRequestMsg( $login, $request ); } unless ($result) { $self->logger->error( "SSO: Fail to process authentication request"); return PE_SAML_SSO_ERROR; } my $h = $self->p->processHook( $req, 'samlGotAuthnRequest', $login ); return $h if ( $h != PE_OK ); # Get SP entityID my $sp = $request ? $login->remote_providerID() : $idp_initiated_sp; $self->logger->debug("Found entityID $sp in SAML message"); $req->env->{llng_saml_sp} = $sp; # SP conf key my $spConfKey = $self->spList->{$sp}->{confKey}; unless ($spConfKey) { $self->userLogger->error( "$sp do not match any SP in configuration"); return PE_SAML_UNKNOWN_ENTITY; } $self->logger->debug("$sp match $spConfKey SP in configuration"); $req->env->{llng_saml_spconfkey} = $spConfKey; if ( $login->request ) { my $acs = $login->request->AssertionConsumerServiceURL; if ($acs) { $req->env->{llng_saml_acs} = $acs; $self->logger->debug( "Using AssertionConsumerServiceURL $acs"); } } # Check access rule if ( my $rule = $self->spRules->{$spConfKey} ) { unless ( $rule->( $req, $req->sessionInfo ) ) { $self->userLogger->warn( 'User ' . $req->sessionInfo->{ $self->conf->{whatToTrace} } . " is not authorized to access to $spConfKey" ); return PE_UNAUTHORIZEDPARTNER; } } $self->userLogger->notice( 'User ' . $req->sessionInfo->{ $self->conf->{whatToTrace} } . " is authorized to access to $spConfKey" ); # 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) { $self->logger->error( "Could not verify signature of incoming SSO request from $spConfKey" ); return PE_SAML_SIGNATURE_ERROR; } else { $self->logger->debug("Signature is valid"); } } else { $self->logger->debug("Message signature will not be checked"); } my $nameIDFormat; # Check NameID Policy in request if ( $login->request()->NameIDPolicy ) { $nameIDFormat = $login->request()->NameIDPolicy->Format(); $self->logger->debug( "Get NameID format $nameIDFormat from request"); } # Get default NameID Format from configuration # Set to "email" if no value in configuration my $nameIDFormatKey = $self->conf->{samlSPMetaDataOptions}->{$spConfKey} ->{samlSPMetaDataOptionsNameIDFormat} || "email"; # NameID unspecified is forced to default NameID format if ( !$nameIDFormat or $nameIDFormat eq $self->getNameIDFormat("unspecified") ) { $nameIDFormat = $self->getNameIDFormat($nameIDFormatKey); eval { $login->request()->NameIDPolicy()->Format($nameIDFormat); }; } # Force AllowCreate to TRUE for transient/persistent NameIDPolicy if ( $login->request()->NameIDPolicy ) { if ( $nameIDFormat eq $self->getNameIDFormat("transient") or $nameIDFormat eq $self->getNameIDFormat("persistent") ) { $self->logger->debug( "Force AllowCreate flag in NameIDPolicy"); eval { $login->request()->NameIDPolicy()->AllowCreate(1); }; } } # Validate request unless ( $self->validateRequestMsg( $login, 1, 1 ) ) { $self->logger->error("Unable to validate SSO request message"); return PE_SAML_SSO_ERROR; } $self->logger->debug("SSO: authentication request is valid"); my $spAuthnLevel = $self->conf->{samlSPMetaDataOptions}->{$spConfKey} ->{samlSPMetaDataOptionsAuthnLevel} || 0; # Get ForceAuthn flag my $force_authn; eval { $force_authn = $login->request()->ForceAuthn(); }; if ($@) { $self->logger->warn( "Unable to get ForceAuthn flag, set it to false"); $force_authn = 0; } $self->logger->debug( "Found ForceAuthn flag with value $force_authn"); # Force authentication if flag is on, or previous flag still active if ( $force_authn and ( time - $req->sessionInfo->{_utime} > $self->conf->{portalForceAuthnInterval} ) ) { $self->userLogger->info( "SAML SP $spConfKey ask to refresh session of " . $req->sessionInfo->{ $self->conf->{whatToTrace} } ); # Replay authentication process $req->pdata->{targetAuthnLevel} = $spAuthnLevel; return $self->reAuth($req); } # Check Destination (only in non proxy mode) unless ( $req->data->{_proxiedRequest} ) { return PE_SAML_DESTINATION_ERROR unless ( $self->checkDestination( $login->request, $url ) ); } # Check if we have sufficient auth level my $authenticationLevel = $req->{sessionInfo}->{authenticationLevel} || 0; if ( $authenticationLevel < $spAuthnLevel ) { $self->logger->debug( "Insufficient authentication level for service $spConfKey" . " (has: $authenticationLevel, want: $spAuthnLevel)" ); # Reauth with sp auth level as target $req->pdata->{targetAuthnLevel} = $spAuthnLevel; return $self->upgradeAuth($req); } $authn_context = $self->authnLevel2authnContext($authenticationLevel); $self->logger->debug("Authentication context is $authn_context"); # Get SP options notOnOrAfterTimeout my $notOnOrAfterTimeout = $self->conf->{samlSPMetaDataOptions}->{$spConfKey} ->{samlSPMetaDataOptionsNotOnOrAfterTimeout}; # Build Assertion unless ( $self->buildAssertion( $req, $login, $authn_context, $notOnOrAfterTimeout ) ) { $self->logger->error("Unable to build assertion"); return PE_SAML_SSO_ERROR; } $self->logger->debug("SSO: assertion is built"); # 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 = $nameIDFormatConfiguration->{$nameIDFormat} ? $self->conf->{ $nameIDFormatConfiguration->{$nameIDFormat} } : ''; # Override default NameID Mapping if ( $self->conf->{samlSPMetaDataOptions}->{$spConfKey} ->{samlSPMetaDataOptionsNameIDSessionKey} ) { $nameIDSessionKey = $self->conf->{samlSPMetaDataOptions}->{$spConfKey} ->{samlSPMetaDataOptionsNameIDSessionKey}; } my $nameIDContent; if ( $nameIDSessionKey and $self->spMacros->{$sp}->{$nameIDSessionKey} ) { $nameIDContent = $self->spMacros->{$sp}->{$nameIDSessionKey} ->( $req, $req->{sessionInfo} ); } elsif ( $nameIDSessionKey and ( defined $req->{sessionInfo}->{$nameIDSessionKey} ) ) { $nameIDContent = $self->p->getFirstValue( $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); } $self->logger->debug( "NameID Format is " . $login->nameIdentifier->Format ); $self->logger->debug( "NameID Content is " . ( $login->nameIdentifier->content || "" ) ); # Push 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; # Lookup attribute value in SP macros or session my $value; if ( $self->spMacros->{$sp}->{$_} ) { $value = $self->spMacros->{$sp}->{$_} ->( $req, $req->{sessionInfo} ); } else { $value = $req->{sessionInfo}->{$_}; } # Check whether the value is required or not unless ( defined $value ) { if ($mandatory) { $self->logger->error( "Session key $_ is required to set SAML $name attribute ($sp)" ); return PE_ISSUERMISSINGREQATTR; } else { $self->logger->debug( "SAML2 attribute $name has no value but is not mandatory ($sp), skip it" ); next; } } $self->logger->debug( "SAML2 attribute $name will be set with $_ session key ($sp)" ); # SAML2 attribute my $attribute = $self->createAttribute( $name, $format, $friendly_name ); unless ($attribute) { $self->logger->error( "Unable to create a new SAML attribute"); 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) { $self->logger->error( "Unable to create a new SAML attribute value"); $self->checkLassoError($@); return PE_SAML_SSO_ERROR; } push @saml2values, $saml2value; $self->logger->debug("Push $_ in SAML attribute $name"); } $attribute->AttributeValue(@saml2values); # Push attribute in attribute list push @attributes, $attribute; } # Get response assertion my @response_assertions = $login->response->Assertion; unless ( $response_assertions[0] ) { $self->logger->error("Unable to get response assertion"); return PE_SAML_SSO_ERROR; } # 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); } # Set subject NameID $response_assertions[0] ->set_subject_name_id( $login->nameIdentifier ); # Set basic conditions my $oneTimeUse = $self->conf->{samlSPMetaDataOptions}->{$spConfKey} ->{samlSPMetaDataOptionsOneTimeUse} // 0; my $conditionNotOnOrAfter = $notOnOrAfterTimeout || "86400"; eval { $response_assertions[0] ->set_basic_conditions( 60, $conditionNotOnOrAfter, $oneTimeUse ); }; if ($@) { $self->logger->debug("Basic conditions not set: $@"); } # 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 my $sessionIndexSession = $self->getSamlSession(); return PE_SAML_SESSION_ERROR unless $sessionIndexSession; $sessionIndexSession->update( { '_utime' => time, '_saml_id' => $session_id } ); my $sessionIndex = $sessionIndexSession->id; $authn_statements[0]->SessionIndex($sessionIndex); $self->logger->debug( "Set sessionIndex $sessionIndex (linked to session $session_id)" ); # 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); $self->logger->debug( "Set sessionNotOnOrAfter $sessionNotOnOrAfter"); # 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} ->{samlSPMetaDataOptionsSignSSOMessage} // -1; if ( $signSSOMessage == 0 ) { $self->logger->debug("SSO response will not be signed"); $self->disableSignature($login); } elsif ( $signSSOMessage == 1 ) { $self->logger->debug("SSO response will be signed"); $self->forceSignature($login); } else { $self->logger->debug( "SSO response signature according to metadata"); } # log that a SAML authn response is build my $user = $req->{sessionInfo}->{ $self->conf->{whatToTrace} }; my $nameIDLog = ''; foreach my $format (qw(persistent transient)) { if ( $login->nameIdentifier->Format eq $self->getNameIDFormat($format) ) { $nameIDLog = " with $format NameID " . $login->nameIdentifier->content; last; } } $self->userLogger->notice( "SAML authentication response sent to SAML SP $spConfKey for $user$nameIDLog" ); $h = $self->p->processHook( $req, 'samlBuildAuthnResponse', $login ); return $h if ( $h != PE_OK ); # Build SAML response $protocolProfile = $login->protocolProfile(); # Artifact # Choose method if ( $artifact or $protocolProfile == Lasso::Constants::LOGIN_PROTOCOL_PROFILE_BRWS_ART ) { $artifact = 1; if ( $method == $self->getHttpMethod("post") || $method == $self->getHttpMethod("artifact-post") ) { $artifact_method = $self->getHttpMethod("artifact-post"); } else { $artifact_method = $self->getHttpMethod("artifact-get"); } } if ( $protocolProfile == Lasso::Constants::LOGIN_PROTOCOL_PROFILE_BRWS_ART ) { # Build artifact message unless ( $self->buildArtifactMsg( $login, $artifact_method ) ) { $self->logger->error( "Unable to build SSO artifact response message"); return PE_SAML_ART_ERROR; } $self->logger->debug("SSO: artifact response is built"); # 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) ) { $self->logger->error( "Unable to build SSO response message"); return PE_SAML_SSO_ERROR; } $self->logger->debug("SSO: authentication response is built"); } # Save Identity and Session if ( $login->is_identity_dirty ) { # Update session $self->logger->debug("Save Lasso identity in session"); $self->p->updatePersistentSession( $req, { $self->liDump => $login->get_identity->dump }, undef, $session_id ); } if ( $login->is_session_dirty ) { $self->logger->debug("Save Lasso session in session"); $self->p->updateSession( $req, { $self->lsDump => $login->get_session->dump }, $session_id ); } # Keep SAML elements for later queries my $nameid = $login->nameIdentifier; $self->logger->debug( "Store NameID " . $nameid->dump . " and SessionIndex $sessionIndex for session $session_id" ); 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 my $samlSessionInfo = $self->getSamlSession( undef, $infos ); return PE_SAML_SESSION_ERROR unless $samlSessionInfo; my $saml_session_id = $samlSessionInfo->id; $self->logger->debug( "Link session $session_id to SAML session $saml_session_id"); # 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 ); $self->logger->debug( "Will register IDP $cdc_idp in Common Domain Cookie"); # 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 ); my $cdc_iframe = qq''; $req->info( $self->loadTemplate( $req, 'simpleInfo', params => { trspan => 'updateCdc' } ) . $cdc_iframe ); } # HTTP-POST if ( ( !$artifact and $protocolProfile eq Lasso::Constants::LOGIN_PROTOCOL_PROFILE_BRWS_POST ) or ( $artifact and $artifact_method == $self->getHttpMethod("artifact-post") ) ) { # Use autosubmit form my $sso_url = $login->msg_url; my $sso_body = $login->msg_body; $req->postUrl($sso_url); if ( $artifact_method and $artifact_method == $self->getHttpMethod("artifact-post") ) { $req->{postFields} = { 'SAMLart' => $sso_body }; } else { $req->{postFields} = { 'SAMLResponse' => $sso_body }; } # RelayState if ($relaystate) { $req->{postFields}->{'RelayState'} = encode_entities($relaystate); $req->data->{safeHiddenFormValues}->{RelayState} = 1; } $req->steps( ['autoPost'] ); return PE_OK; } # HTTP-REDIRECT if ( $protocolProfile eq Lasso::Constants::LOGIN_PROTOCOL_PROFILE_REDIRECT or $artifact ) { # Redirect user to response URL my $sso_url = $login->msg_url; $self->logger->debug("Redirect user to $sso_url"); $req->{urldc} = $sso_url; $req->mustRedirect(1); $req->steps( [] ); return PE_OK; } } elsif ($response) { $self->logger->debug( "Authentication responses are not managed by this module"); return PE_OK; } else { # No request or response # This should not happen $self->logger->debug("No request or response found"); return PE_OK; } } $self->logger->debug("Not an issuer request $url"); return PE_OK; } sub artifactServer { my ( $self, $req ) = @_; $self->logger->debug( "URL " . $req->uri . " detected as an artifact resolution service URL" ); # Artifact request are sent with SOAP trough POST my $art_request = $req->content; 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 unless ( $art_response = $self->createArtifactResponse( $req, $login ) ) { return $self->p->sendError( $req, "Unable to create artifact response message", 400 ); } $self->{SOAPMessage} = $art_response; # Return SOAP message $self->logger->debug("Send SOAP Message: $art_response"); return [ 200, [ 'Content-Type' => 'text/xml', 'Content-Length' => length($art_response) ], [$art_response] ]; } sub soapSloServer { my ( $self, $req ) = @_; my $url = $req->uri; my $request_method = $req->param('issuerMethod') || $req->method; my $content_type = $req->content_type(); $self->logger->debug("URL $url detected as an SLO URL"); # 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 ); } $self->logger->debug("SLO: Logout request is valid"); my $h = $self->p->processHook( $req, 'samlGotLogoutRequest', $logout ); if ( $h != PE_OK ) { return $self->p->sendError( $req, "SLO: samlGotLogoutRequest hook returned error", 400 ); } # 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(); $self->logger->debug("Found entityID $sp in SAML message"); # SP conf key my $spConfKey = $self->spList->{$sp}->{confKey}; unless ($spConfKey) { return $self->p->sendError( $req, "$sp do not match any SP in configuration", 400 ); } $self->logger->debug("$sp match $spConfKey SP in configuration"); # 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 { $self->logger->debug("Signature is valid"); } } else { $self->logger->debug("Message signature will not be checked"); } # 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; }; # SLO requests without session index are not accepted in SOAP mode unless ( defined $session_index ) { $self->p->sendError( $req, "No session index in SLO request from $spConfKey SP", 400 ); } # Get session index my $sessionIndexSession = $self->getSamlSession($session_index); return $self->p->sendError( $req, 'SAML session not found', 400 ) unless $sessionIndexSession; my $local_session_id = $sessionIndexSession->data->{_saml_id}; $sessionIndexSession->remove; $self->logger->debug( "Get session id $local_session_id (from session index $session_index)" ); # Open local session my $local_session = $self->p->getApacheSession($local_session_id); unless ($local_session) { return $self->p->sendError( $req, "No local session found", 400 ); } # Load Session and Identity if they exist my $session = $local_session->data->{ $self->lsDump }; my $identity = $local_session->data->{ $self->liDump }; if ($session) { unless ( $self->setSessionFromDump( $logout, $session ) ) { return $self->p->sendError( $req, "Unable to load Lasso Session", 400 ); } $self->logger->debug("Lasso Session loaded"); } if ($identity) { unless ( $self->setIdentityFromDump( $logout, $identity ) ) { return $self->p->sendError( $req, "Unable to load Lasso Identity", 400 ); } $self->logger->debug("Lasso Identity loaded"); } # Close SAML sessions unless ( $self->deleteSAMLSecondarySessions($local_session_id) ) { return $self->p->sendError( $req, "Fail to delete SAML sessions", 400 ); } # Close local session unless ( $self->p->_deleteSession( $req, $local_session ) ) { return $self->p->sendError( $req, "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) { $self->logger->debug("SOAP SLO successful on $providerID"); } else { $self->logger->debug("SOAP SLO error on $providerID"); } } # Set RelayState if ($relaystate) { $logout->msg_relayState($relaystate); $self->logger->debug("Set $relaystate in RelayState"); } # Signature my $signSLOMessage = $self->{samlSPMetaDataOptions}->{$spConfKey} ->{samlSPMetaDataOptionsSignSLOMessage} // 0; if ( $signSLOMessage == 0 ) { $self->logger->debug("SLO response will not be signed"); $self->disableSignature($logout); } elsif ( $signSLOMessage == 1 ) { $self->logger->debug("SLO response will be signed"); $self->forceSignature($logout); } else { $self->logger->debug( "SLO response signature according to metadata"); } $h = $self->p->processHook( $req, 'samlBuildLogoutResponse', $logout ); if ( $h != PE_OK ) { return $self->p->sendError( $req, "SLO: samlBuildLogoutResponse hook returned error", 400 ); } # Send logout response unless ( $self->buildLogoutResponseMsg($logout) ) { $self->logger->error("Unable to build SLO response for $spConfKey"); return $self->p->sendError( $req, 'Unable to build SLO response', 400 ); } my $slo_body = $logout->msg_body; return [ 200, [ 'Content-Type' => 'text/xml', 'Content-Length' => length($slo_body) ], [$slo_body] ]; } } sub logout { my ( $self, $req ) = @_; return PE_OK if ( $req->data->{samlSLOCalled} ); # Session ID my $session_id = $req->{sessionInfo}->{_session_id} || $req->id; # Close SAML sessions unless ( $self->deleteSAMLSecondarySessions($session_id) ) { $self->logger->error("Fail to delete SAML sessions"); } # Create Logout object my $logout = $self->createLogout( $self->lassoServer ); # Load Session and Identity if they exist my $session = $req->{sessionInfo}->{ $self->lsDump }; my $identity = $req->{sessionInfo}->{ $self->liDump }; if ($session) { unless ( $self->setSessionFromDump( $logout, $session ) ) { $self->logger->error("Unable to load Lasso Session"); return PE_SAML_SLO_ERROR; } $self->logger->debug("Lasso Session loaded"); } # No need to initiate logout requests on SP, if no SAML session is # available into the session. else { $self->logger->debug('No SAML session available into this session'); return PE_OK; } if ($identity) { unless ( $self->setIdentityFromDump( $logout, $identity ) ) { $self->logger->error("Unable to load Lasso Identity"); return PE_SAML_SLO_ERROR; } $self->logger->debug("Lasso Identity loaded"); } # 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. if ( $self->sendLogoutRequestToProviders( $req, $logout ) ) { $self->{urldc} = $req->script_name . "?logout=1"; return PE_OK; } return PE_OK; } sub sloRelaySoap { my ( $self, $req ) = @_; $self->logger->debug( "URL " . $req->uri . " detected as a SOAP relay service URL" ); # Check if relay parameter is present (mandatory) my $relayID; unless ( $relayID = $req->param('relay') ) { $self->logger->error("No relayID detected"); return $self->imgnok($req); } # Retrieve the corresponding data from samlStorage my $relayInfos = $self->getSamlSession($relayID); unless ($relayInfos) { $self->logger->error("Could not get relay session $relayID"); return $self->imgnok($req); } $self->logger->debug("Found relay session $relayID"); # Rebuild the logout object my $logout; unless ( $logout = $self->createLogout( $self->lassoServer ) ) { $self->logger->error("Could not rebuild logout object"); return $self->imgnok($req); } # Load Session and Identity if they exist my $session = $relayInfos->data->{ $self->lsDump }; my $identity = $relayInfos->data->{ $self->liDump }; my $providerID = $relayInfos->data->{_providerID}; my $relayState = $relayInfos->data->{_relayState} // ''; my $spConfKey = $self->spList->{$providerID}->{confKey}; if ($session) { unless ( $self->setSessionFromDump( $logout, $session ) ) { $self->logger->error("Unable to load Lasso Session"); return $self->imgnok($req); } $self->logger->debug("Lasso Session loaded"); } if ($identity) { unless ( $self->setIdentityFromDump( $logout, $identity ) ) { $self->logger->error("Unable to load Lasso Identity"); return $self->imgnok($req); } $self->logger->debug("Lasso Identity loaded"); } # Send the logout request my ( $rstatus, $rmethod, $rinfo ) = $self->sendLogoutRequestToProvider( $req, $logout, $providerID, Lasso::Constants::HTTP_METHOD_SOAP, undef, $relayState ); unless ($rstatus) { $self->logger->error( "Fail to process SOAP logout request to $providerID"); return $self->imgnok($req); } # Store success status for this SLO request my $sloStatusSessionInfos = $self->getSamlSession( $relayState, { $spConfKey => 1, _utime => time() } ); if ($sloStatusSessionInfos) { $self->logger->debug( "Store SLO status for $spConfKey in session $relayState"); } else { $self->logger->warn( "Unable to store SLO status for $spConfKey in session $relayState"); } # Delete relay session $relayInfos->remove(); # SLO response is OK $self->logger->debug("Display OK status for SLO on $spConfKey"); return $self->imgok($req); } sub sloRelayPost { my ( $self, $req ) = @_; $self->logger->debug( "URL " . $req->uri . " detected as a POST relay service URL" ); # 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 $relayID" ); } $self->logger->debug("Found relay session $relayID"); # Get data to build POST form $req->{postUrl} = $relayInfos->data->{url}; $req->{postFields}->{'SAMLRequest'} = $relayInfos->data->{body}; # RelayState if ( $relayInfos->data->{relayState} ) { $req->{postFields}->{'RelayState'} = encode_entities( $relayInfos->data->{relayState} ); $req->data->{safeHiddenFormValues}->{RelayState} = 1; } # Delete relay session $relayInfos->remove(); $req->frame(1); return $self->p->do( $req, ['autoPost'] ); } sub sloRelayTerm { my ( $self, $req ) = @_; $self->logger->debug( "URL " . $req->uri . " detected as a SLO Termination relay service URL" ); # Check if relay parameter is present (mandatory) my $relayID = $self->p->getHiddenFormValue( $req, 'relay', '', 0 ) || $req->param('relay'); unless ($relayID) { 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 $relayID" ); } $self->logger->debug("Found relay session $relayID"); # Get data from relay session my $logout_dump = $relayInfos->data->{_logout}; my $session_dump = $relayInfos->data->{_session}; my $method = $relayInfos->data->{_method}; unless ($logout_dump) { $self->logger->error("Could not get logout dump"); return PE_SAML_SLO_ERROR; } # Rebuild Lasso::Logout object my $logout = $self->createLogout( $self->lassoServer, $logout_dump ); unless ($logout) { $self->logger->error("Could not build Lasso::Logout"); return PE_SAML_SLO_ERROR; } # Inject session unless ($session_dump) { $self->logger->error("Could not get session dump"); return PE_SAML_SLO_ERROR; } unless ( $self->setSessionFromDump( $logout, $session_dump ) ) { $self->logger->error("Could not set session from dump"); return PE_SAML_SLO_ERROR; } # Get Lasso::Session my $session = $logout->get_session(); unless ($session) { $self->logger->error("Could not get session from logout"); return PE_SAML_SLO_ERROR; } # Loop on assertions and remove them if SLO status is OK $self->resetProviderIdIndex($logout); while ( my $sp = $self->getNextProviderId($logout) ) { # Try to get SLO status from SLO session my $spConfKey = $self->spList->{$sp}->{confKey}; my $status = $relayInfos->data->{$spConfKey}; # Remove assertion if status is OK if ($status) { eval { $session->remove_assertion($sp); }; if ($@) { $self->logger->warn("Unable to remove assertion for $sp"); } else { $self->logger->debug("Assertion removed for $sp"); } } else { $self->logger->debug( "SLO status was not ok for $sp, assertion not removed"); } } # Reinject session unless ( $session->is_empty() ) { $self->setSessionFromDump( $logout, $session->dump ); } # Delete relay session $relayInfos->remove(); # Send SLO response if ( my $tmp = $self->sendLogoutResponseToServiceProvider( $req, $logout, $method ) ) { return $tmp; } else { $self->logger->error("Fail to send SLO response"); return PE_SAML_SLO_ERROR; } } sub authSloServer { my ( $self, $req ) = @_; $self->p->importHandlerData($req); return $self->sloServer($req); } sub sloResume { my ( $self, $req ) = @_; my $ResumeParams = $req->params('ResumeParams'); unless ($ResumeParams) { $self->logger->error("Could not find resumption info"); return PE_SAML_SLO_ERROR; } my $logoutContextSession = $self->getSamlSession($ResumeParams); unless ($logoutContextSession) { $self->logger->error("Could not find logout context session"); return PE_SAML_SLO_ERROR; } my $spConfKey = $logoutContextSession->data->{spConfKey}; my $method = $logoutContextSession->data->{method}; my $provider_nb = $logoutContextSession->data->{provider_nb}; my $relayID = $logoutContextSession->data->{relayID}; # Restore Lasso logout object from XML dump my $logout = $self->createLogout( $self->lassoServer, $logoutContextSession->data->{logout} ); # Restore session info (for logout of other SPs) $req->setInfo( $logoutContextSession->data->{info} ) if $logoutContextSession->data->{info}; return $self->_finishSlo( $req, $logout, $method, $spConfKey, $provider_nb, $relayID ); } sub _finishSlo { my ( $self, $req, $logout, $method, $spConfKey, $provider_nb, $relayID ) = @_; # Signature my $signSLOMessage = $self->conf->{samlSPMetaDataOptions}->{$spConfKey} ->{samlSPMetaDataOptionsSignSLOMessage}; unless ($signSLOMessage) { $self->logger->debug("Do not sign this SLO response"); return $self->sendSLOErrorResponse( $req, $logout, $method ) unless ( $self->disableSignature($logout) ); } # If no waiting SP, return directly SLO response unless ($provider_nb) { return $self->sendLogoutResponseToServiceProvider( $req, $logout, $method ); } # Else build SLO status relay URL and display info else { $req->{urldc} = $self->conf->{portal} . '/saml/relaySingleLogoutTermination'; $self->p->setHiddenFormValue( $req, 'relay', $relayID, '', 0 ); return $self->p->do( $req, [] ); } } sub sloServer { my ( $self, $req ) = @_; my $url = $req->uri; my $request_method = $req->param('issuerMethod') || $req->method; my $content_type = $req->content_type(); $self->logger->debug("URL $url detected as an SLO URL"); # 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); # Disable Content-Security-Policy header since logout can be embedded in # a frame $req->frame(1); if ($request) { # Process logout request unless ( $self->processLogoutRequestMsg( $logout, $request ) ) { return $self->p->sendError( $req, "SLO: Fail to process logout request", 400 ); } $self->logger->debug("SLO: Logout request is valid"); my $h = $self->p->processHook( $req, 'samlGotLogoutRequest', $logout ); if ( $h != PE_OK ) { return $self->p->sendError( $req, "SLO: samlGotLogoutRequest hook returned error", 400 ); } # Get SP entityID my $sp = $logout->remote_providerID(); $req->env->{llng_saml_sp} = $sp; $self->logger->debug("Found entityID $sp in SAML message"); # SP conf key my $spConfKey = $self->spList->{$sp}->{confKey}; unless ($spConfKey) { return $self->p->sendError( $req, "$sp do not match any SP in configuration", 400 ); } $self->logger->debug("$sp match $spConfKey SP in configuration"); $req->env->{llng_saml_spconfkey} = $spConfKey; # Load Session and Identity if they exist my ( $session, $session_index, $identity, $local_session_id ); eval { $session_index = $logout->request()->SessionIndex; }; # SLO requests without session index can be accepted unless ( defined $session_index ) { $self->logger->warn( "No session index in SLO request from $spConfKey SP"); } if ($session_index) { my $sessionIndexSession = $self->getSamlSession($session_index); return $self->p->do( $req, [ sub { PE_SESSIONEXPIRED } ] ) unless $sessionIndexSession; $local_session_id = $sessionIndexSession->data->{_saml_id}; $sessionIndexSession->remove; $self->logger->debug( "Get session id $local_session_id (from session index $session_index)" ); } else { $local_session_id = $req->id; $self->logger->debug( "Get session id $local_session_id (from cookie)"); } if ( $req->{sessionInfo} ) { $session = $req->{sessionInfo}->{ $self->lsDump }; $identity = $req->{sessionInfo}->{ $self->liDump }; } unless ($session) { # Open local session my $local_session = $self->p->getApacheSession($local_session_id); unless ($local_session) { $self->logger->error("No local session found"); return $self->sendSLOErrorResponse( $req, $logout, $method ); } # Load Session and Identity if they exist $session = $local_session->data->{ $self->lsDump }; $identity = $local_session->data->{ $self->liDump }; # Import user data in $req (for other "logout" subs) $req->id( $local_session->data->{_session_id} ); $req->sessionInfo( $local_session->data ); $req->user( $local_session->data->{ $self->conf->{whatToTrace} } ); } if ($session) { unless ( $self->setSessionFromDump( $logout, $session ) ) { return $self->p->sendError( $req, "Unable to load Lasso Session", 400 ); } $self->logger->debug("Lasso Session loaded"); } if ($identity) { unless ( $self->setIdentityFromDump( $logout, $identity ) ) { return $self->p->sendError( $req, "Unable to load Lasso Identity", 400 ); } $self->logger->debug("Lasso Identity loaded"); } # 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 { $self->logger->debug("Signature is valid"); } } else { $self->logger->debug("Message signature will not be checked"); } # Check Destination return $self->sendSLOErrorResponse( $req, $logout, $method ) unless ( $self->checkDestination( $logout->request, $url ) ); # Validate request if no previous error unless ( $self->validateLogoutRequest($logout) ) { return $self->p->sendError( $req, "SLO request is not valid", 400 ); } # Set RelayState if ($relaystate) { $logout->msg_relayState($relaystate); $self->logger->debug("Set $relaystate in RelayState"); } my $sloInfos; $sloInfos->{type} = 'sloStatus'; $sloInfos->{_utime} = time; $sloInfos->{_logout} = $logout->dump; $sloInfos->{_session} = $logout->get_session() ? $logout->get_session()->dump : ""; $sloInfos->{_method} = $method; # Create SLO status session and get ID my $sloStatusSessionInfo = $self->getSamlSession( undef, $sloInfos ); my $relayID = $sloStatusSessionInfo->id; $self->logger->debug("Create relay session $relayID"); # Prepare logout on all others SP my $provider_nb = $self->sendLogoutRequestToProviders( $req, $logout, $relayID ); # Close SAML sessions unless ( $self->deleteSAMLSecondarySessions($local_session_id) ) { $self->logger->error("Fail to delete SAML sessions"); } # Close local session # This flag is for logout() to say that SAML logout is already done $req->data->{samlSLOCalled} = 1; # This variable decides if we call the authLogout step # (which can redirect away) my $doAuthLogout = 0; my $logoutContextSession; # TODO: for now, we only try authLogout to disconnect an # external IDP if the current session has been opened on a # SAML IDP. We only propagate logout to the IDP our SP is SAML # AND our IDP is SAML too if ( $req->sessionInfo->{_lassoSessionDump} ) { $doAuthLogout = 1; # In case we have to redirect to the IDP, save current state # to allow resumption. This needs to be done here because # issuerUrldc will be put in the IdP logout's RelayState my $logoutInfos = { logout => $logout->dump, spConfKey => $spConfKey, method => $method, provider_nb => $provider_nb, relayID => $relayID, _utime => time(), }; $logoutContextSession = $self->getSamlSession( undef, $logoutInfos ); my $uri = URI->new( $self->conf->{portal} . '/saml/singleLogoutResume' ); $uri->query_param( ResumeParams => $logoutContextSession->id ); $req->{issuerUrldc} = $uri->as_string; } # We don't want info to interfere with the auth logout process my $savedInfo = $req->info; $req->setInfo(''); # Launch normal logout and ignore errors $req->steps( [ @{ $self->p->beforeLogout }, ( $doAuthLogout ? 'authLogout' : () ), 'deleteSession' ] ); my $res = $self->p->process($req); if ( $res eq PE_REDIRECT ) { # Save session info (for logout of other SP) if ($savedInfo) { $logoutContextSession->update( { info => $savedInfo } ); } return $self->p->do( $req, [ sub { PE_REDIRECT } ] ); } $req->info($savedInfo); return $self->_finishSlo( $req, $logout, $method, $spConfKey, $provider_nb, $relayID ); } elsif ($response) { # Process logout response my $result = $self->processLogoutResponseMsg( $logout, $response ); unless ($result) { $self->logger->error("Fail to process logout response"); $self->imgnok($req); } $self->logger->debug("Logout response is valid"); my $h = $self->p->processHook( $req, 'samlGotLogoutResponse', $logout ); $self->imgnok($req) if ( $h != PE_OK ); # Check Destination $self->imgnok($req) unless ( $self->checkDestination( $logout->response, $url ) ); # Get SP entityID my $sp = $logout->remote_providerID(); $self->logger->debug("Found entityID $sp in SAML message"); # SP conf key my $spConfKey = $self->spList->{$sp}->{confKey}; unless ($spConfKey) { $self->logger->error("$sp do not match any SP in configuration"); $self->imgnok($req); } $self->logger->debug("$sp match $spConfKey SP in configuration"); # Do we check signature? my $checkSLOMessageSignature = $self->conf->{samlSPMetaDataOptions}->{$spConfKey} ->{samlSPMetaDataOptionsCheckSLOMessageSignature}; if ($checkSLOMessageSignature) { unless ( $self->checkSignatureStatus($logout) ) { $self->logger->error( "Could not verify signature of incoming SLO request from $spConfKey" ); $self->imgnok($req); } else { $self->logger->debug("Signature is valid"); } } else { $self->logger->debug("Message signature will not be checked"); } # Store success status for this SLO request if ($relaystate) { my $sloStatusSessionInfos = $self->getSamlSession($relaystate); if ($sloStatusSessionInfos) { $sloStatusSessionInfos->update( { $spConfKey => 1 } ); $self->logger->debug( "Store SLO status for $spConfKey in session $relaystate"); } else { $self->logger->warn( "Unable to store SLO status for $spConfKey in session $relaystate" ); } } else { $self->logger->warn( "Unable to store SLO status for $spConfKey because there is no RelayState" ); } # SLO response is OK $self->logger->debug("Display OK status for SLO on $spConfKey"); $self->imgok($req); } else { # No request or response # This should not happen return $self->p->sendError( $req, "No request or response found", 400 ); } } sub attributeServer { my ( $self, $req, ) = @_; my $url = $req->uri; $self->logger->debug("URL $url detected as an attribute service URL"); # Attribute request are sent with SOAP trough POST my $att_request = $req->content; my $att_response; # Process request my $query = $self->processAttributeRequest( $self->lassoServer, $att_request ); unless ($query) { return $self->p->sendError( $req, "Unable to process attribute request", 400 ); } # Get SP entityID my $sp = $query->remote_providerID(); $self->logger->debug("Found entityID $sp in SAML message"); # SP conf key my $spConfKey = $self->spList->{$sp}->{confKey}; unless ($spConfKey) { return $self->p->sendError( $req, "$sp do not match any SP in configuration", 400 ); } $self->logger->debug("$sp match $spConfKey SP in configuration"); # Check Destination unless ( $self->checkDestination( $query->request, $url ) ) { return $self->p->sendError( $req, "Bad destination $url", 400 ); } # Validate request unless ( $self->validateAttributeRequest($query) ) { return $self->p->sendError( $req, "Attribute request not valid", 400 ); } # Get NameID my $name_id = $query->nameIdentifier(); unless ($name_id) { $self->p->sendError( $req, "Fail to get NameID from attribute request", 400 ); } my $user = $name_id->content(); # Get sessionInfo for the given NameID my $sessionInfo; my $saml_sessions = Lemonldap::NG::Common::Apache::Session->searchOn( $self->amOpts, "_nameID", $name_id->dump ); if ( my @saml_sessions_keys = grep { $saml_sessions->{$_}->{_session_kind} eq $self->sessionKind } keys %$saml_sessions ) { # Warning if more than one session found if ( $#saml_sessions_keys > 0 ) { $self->logger->warn( "More than one SAML session found for user $user"); } # Take the first session my $saml_session = shift @saml_sessions_keys; # Get session $self->logger->debug( "Retrieve SAML session $saml_session for user $user"); my $samlSessionInfo = $self->getSamlSession($saml_session); # Get real session my $real_session = $samlSessionInfo->data->{_saml_id}; $self->logger->debug( "Retrieve real session $real_session for user $user"); $sessionInfo = $self->p->getApacheSession($real_session); unless ($sessionInfo) { return $self->p->sendError( $req, "Cannot get session $real_session", 500 ); } } else { return $self->p->sendError( $req, "No SAML session found for user $user", 400 ); } # Get requested attributes my @requested_attributes; eval { @requested_attributes = $query->request()->Attribute(); }; if ($@) { $self->checkLassoError($@); return $self->p->sendError( $req, "Unable to get requested attributes", 400 ); } # Returned attributes my @returned_attributes; # Browse SP authorized attributes foreach ( keys %{ $self->conf->{samlSPMetaDataExportedAttributes}->{$spConfKey} } ) { my $sp_attr = $_; # Extract fields from exportedAttr value my ( $mandatory, $name, $format, $friendly_name ) = split( /;/, $self->conf->{samlSPMetaDataExportedAttributes}->{$spConfKey} ->{$sp_attr} ); foreach (@requested_attributes) { my $req_attr = $_; my $rname = $req_attr->Name(); my $rformat = $req_attr->NameFormat(); my $rfriendly_name = $req_attr->FriendlyName(); # Skip if name does not match next unless ( $rname =~ /^$name$/ ); # Check format and friendly name next if ( $rformat and $rformat !~ /^$format$/ ); next if ( $rfriendly_name and $rfriendly_name !~ /^$friendly_name$/ ); $self->logger->debug( "SP $spConfKey is authorized to access attribute $rname"); $self->logger->debug( "Attribute $rname is linked to $sp_attr session key"); # Check if values are given my $rvalue = $self->getAttributeValue( $rname, $rformat, $rfriendly_name, [$req_attr] ); $self->logger->debug( "Some values are explicitely requested: $rvalue") if defined $rvalue; # Get session value if ( $sessionInfo->data->{$sp_attr} ) { my @values = split $self->conf->{multiValuesSeparator}, $sessionInfo->data->{$sp_attr}; my @saml2values; # SAML2 attribute my $ret_attr = $self->createAttribute( $rname, $rformat, $rfriendly_name ); unless ($ret_attr) { return $self->p->sendError( $req, "Unable to create a new SAML attribute", 500 ); } foreach (@values) { my $local_value = $_; # Check if values were set in requested attribute # In this case, only requested values can be returned if ( $rvalue and !map( /^$local_value$/, split( $self->conf->{multiValuesSeparator}, $rvalue ) ) ) { $self->logger->warn( "$local_value value is not in requested values, it will not be sent" ); next; } # SAML2 attribute value my $saml2value = $self->createAttributeValue( $local_value, $self->conf->{samlSPMetaDataOptions}->{$spConfKey} ->{samlSPMetaDataOptionsForceUTF8} ); unless ($saml2value) { return $self->p->sendError( $req, "Unable to create a new SAML attribute value", 400 ); } push @saml2values, $saml2value; $self->logger->debug( "Push $local_value in SAML attribute $name"); } $ret_attr->AttributeValue(@saml2values); # Push attribute in attribute list push @returned_attributes, $ret_attr; } else { $self->logger->debug("No session value for $sp_attr"); } } } # Create attribute statement if ( scalar @returned_attributes ) { my $attribute_statement; eval { $attribute_statement = Lasso::Saml2AttributeStatement->new(); }; if ($@) { $self->checkLassoError($@); return $self->p->sendError( $req, 'An error occurs, see IdP logs', 500 ); } # Register attributes in attribute statement $attribute_statement->Attribute(@returned_attributes); # Create assetion my $assertion; eval { $assertion = Lasso::Saml2Assertion->new(); }; if ($@) { $self->checkLassoError($@); return $self->p->sendError( $req, 'An error occurs, see IdP logs', 500 ); } # Add attribute statement in response assertion my @attributes_statement = ($attribute_statement); $assertion->AttributeStatement(@attributes_statement); # Set response assertion $query->response->Assertion( ($assertion) ); } # Build response $att_response = $self->buildAttributeResponse($query); unless ($att_response) { $self->p->sendError( $req, "Unable to build attribute response", 500 ); } return [ 200, [ 'Content-Type' => 'text/xml', 'Content-Length' => length($att_response) ], [$att_response] ]; } # INTERNAL METHODS sub imgok { my ( $self, $req, ) = @_; return $self->sendImage( $req, 'icons/ok.png' ); } sub imgnok { my ( $self, $req, ) = @_; return $self->sendImage( $req, 'icons/warning.png' ); } sub sendImage { my ( $self, $req,, $img ) = @_; return [ 302, [ 'Location' => $self->conf->{portal} . $self->p->staticPrefix . '/common/' . $img, ], [], ]; } # Normalize url to be tolerant to SAML Path # Usefull if SAML Path is a regex # @return normalized url sub normalize_url { my ( $self, $url, $samlPath, $metadataUrl ) = @_; my $initialPath = ""; my $finalPath = ""; # Get current (bad) path if ( $url =~ m/($samlPath)/ ) { $initialPath = $1; } # Get destination (good) path if ( $metadataUrl =~ m/($samlPath)/ ) { $finalPath = $1; } if ( $initialPath ne "" and $finalPath ne "" and $initialPath ne $finalPath ) { $self->logger->debug( "Normalizing url path form $initialPath to $finalPath"); $url =~ s/$initialPath/$finalPath/; } return $url; } 1;