PE_OK
PE_CONFIRM
PE_IDPCHOICE
PE_LOGOUT_OK
PE_REDIRECT
PE_SAML_ART_ERROR
PE_SAML_CONDITIONS_ERROR
PE_SAML_DESTINATION_ERROR
PE_SAML_ERROR
PE_SAML_IDPSSOINITIATED_NOTALLOWED
PE_SAML_SESSION_ERROR
PE_SAML_SIGNATURE_ERROR
PE_SLO_ERROR
PE_SAML_SSO_ERROR
PE_SAML_UNKNOWN_ENTITY
PE_SENDRESPONSE
)
;
our
$VERSION
=
'2.19.0'
;
extends
qw(
Lemonldap::NG::Portal::Lib::SAML
)
;
has
ssoAssConsumerRe
=> (
is
=>
'rw'
);
has
sloRe
=> (
is
=>
'rw'
);
has
artRe
=> (
is
=>
'rw'
);
has
catch
=> (
is
=>
'rw'
);
use
constant
lsDump
=>
'_lassoSessionDump'
;
use
constant
liDump
=>
'_lassoIdentityDump'
;
use
constant
niDump
=>
'_lassoNameIdDump'
;
use
constant
sIndex
=>
'_lassoSessionIndex'
;
sub
forAuthUser {
'handleAuthRequests'
}
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
->ssoAssConsumerRe(
qr/^($saml_acs_art_url|$saml_acs_post_url|$saml_acs_get_url)(?:\?.*)?$/
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/^($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)(?:\?.*)?$/
i
);
my
$saml_ars_url
=
$self
->getMetaDataURL(
"samlSPSSODescriptorArtifactResolutionServiceArtifact"
);
$self
->artRe(
qr/^($saml_ars_url)(?:\?.*)?$/
i);
$self
->
catch
(
qr/^($saml_acs_art_url|$saml_acs_post_url|$saml_acs_get_url$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)(?:\?.*)?$/
i
);
return
(
$self
->SUPER::init and
$self
->loadIDPs );
}
sub
extractFormInfo {
my
(
$self
,
$req
) =
@_
;
my
$url
=
$req
->uri;
my
$request_method
=
$req
->method;
my
$content_type
=
$req
->content_type;
if
(
$url
=~
$self
->ssoAssConsumerRe ) {
$self
->logger->debug(
"URL $url detected as an SSO assertion consumer URL"
);
my
(
$request
,
$response
,
$method
,
$relaystate
,
$artifact
) =
$self
->checkMessage(
$req
,
$url
,
$request_method
,
$content_type
,
"login"
);
my
$login
=
$self
->createLogin(
$self
->lassoServer );
$self
->disableSignatureVerification(
$login
);
if
(
$response
) {
my
$result
;
if
(
$artifact
) {
$result
=
$self
->processArtResponseMsg(
$login
,
$response
);
}
else
{
$result
=
$self
->processAuthnResponseMsg(
$login
,
$response
);
}
unless
(
$result
) {
$self
->logger->error(
"SAML SSO: Fail to process authentication response"
);
return
PE_SAML_SSO_ERROR;
}
$self
->logger->debug(
"SSO: authentication response is valid"
);
my
$idp
=
$login
->remote_providerID();
$self
->logger->debug(
"Found entityID $idp in SAML message"
);
my
$idpConfKey
=
$self
->getIDPConfKey(
$idp
);
unless
(
$idpConfKey
) {
$self
->userLogger->error(
"$idp do not match any IDP in configuration"
);
return
PE_SAML_UNKNOWN_ENTITY;
}
$self
->logger->debug(
"$idp match $idpConfKey IDP in configuration"
);
my
$checkSSOMessageSignature
=
$self
->idpOptions->{
$idp
}
->{samlIDPMetaDataOptionsCheckSSOMessageSignature};
if
(
$checkSSOMessageSignature
) {
$self
->forceSignatureVerification(
$login
);
if
(
$artifact
) {
$result
=
$self
->processArtResponseMsg(
$login
,
$response
);
}
else
{
$result
=
$self
->processAuthnResponseMsg(
$login
,
$response
);
}
unless
(
$result
) {
$self
->userLogger->error(
"Signature is not valid"
);
return
PE_SAML_SIGNATURE_ERROR;
}
else
{
$self
->logger->debug(
"Signature is valid"
);
}
}
else
{
$self
->logger->debug(
"Message signature will not be checked"
);
}
my
$saml_response
=
$login
->response();
unless
(
$saml_response
) {
$self
->userLogger->error(
"No SAML response found"
);
return
PE_SAML_SSO_ERROR;
}
my
$h
=
$self
->p->processHook(
$req
,
'samlGotAuthnResponse'
,
$idp
,
$login
);
return
PE_SAML_SSO_ERROR
if
(
$h
!= PE_OK );
return
PE_SAML_DESTINATION_ERROR
unless
(
$self
->checkDestination(
$saml_response
,
$url
) );
my
$assertion_responded
=
$saml_response
->InResponseTo;
if
(
$assertion_responded
) {
unless
(
$self
->replayProtection(
$assertion_responded
) ) {
$self
->userLogger->error(
"Message $assertion_responded already used or expired, replay authentication"
);
delete
$req
->{urldc};
$req
->mustRedirect(1);
$req
->steps( [] );
$req
->
continue
(1);
return
PE_OK;
}
}
else
{
$self
->logger->debug(
"Assertion is not a response to a created authentication request, do not control replay"
);
}
my
$assertion
=
$self
->getAssertion(
$login
);
unless
(
$assertion
) {
$self
->userLogger->error(
"No assertion found"
);
return
PE_SAML_SSO_ERROR;
}
my
$checkTime
=
$self
->idpOptions->{
$idp
}->{samlIDPMetaDataOptionsCheckTime};
my
$checkAudience
=
$self
->idpOptions->{
$idp
}->{samlIDPMetaDataOptionsCheckAudience};
unless
(
$self
->validateConditions(
$assertion
,
$self
->getMetaDataURL(
"samlEntityID"
, 0, 1 ),
$checkTime
,
$checkAudience
)
)
{
$self
->userLogger->error(
"Conditions not validated"
);
return
PE_SAML_CONDITIONS_ERROR;
}
my
$relayStateURL
=
$self
->idpOptions->{
$idp
}->{samlIDPMetaDataOptionsRelayStateURL};
if
(
$self
->extractRelayState(
$req
,
$relaystate
,
$relayStateURL
) )
{
$self
->logger->debug(
"RelayState $relaystate extracted"
);
}
my
$allowLoginFromIDP
=
$self
->idpOptions->{
$idp
}
->{samlIDPMetaDataOptionsAllowLoginFromIDP};
if
( !
$assertion_responded
and !
$allowLoginFromIDP
) {
$self
->userLogger->error(
"Direct login from IDP $idpConfKey is not allowed"
);
return
PE_SAML_IDPSSOINITIATED_NOTALLOWED;
}
my
$responseAuthnContext
;
eval
{
$responseAuthnContext
=
$assertion
->AuthnStatement()->AuthnContext()
->AuthnContextClassRef();
};
if
($@) {
$self
->logger->debug(
"Unable to get authentication context from $idpConfKey"
);
$responseAuthnContext
=
$self
->getAuthnContext(
"unspecified"
);
}
else
{
$self
->logger->debug(
"Found authentication context: $responseAuthnContext"
);
}
$req
->sessionInfo->{authenticationLevel} =
$self
->authnContext2authnLevel(
$responseAuthnContext
);
$req
->mustRedirect(1)
unless
(
$req
->urldc );
my
$session_index
;
eval
{
$session_index
=
$assertion
->AuthnStatement()->SessionIndex();
};
if
( $@ or !
defined
(
$session_index
) ) {
$self
->logger->debug(
"No SessionIndex found"
);
}
else
{
$self
->logger->debug(
"Found SessionIndex $session_index"
);
}
my
$nameid
=
$login
->nameIdentifier;
unless
(
$nameid
) {
$self
->userLogger->error(
"No NameID element found"
);
return
PE_SAML_SSO_ERROR;
}
my
$nameid_content
=
$nameid
->content;
unless
(
$nameid_content
) {
$self
->userLogger->error(
"No NameID value found"
);
return
PE_SAML_SSO_ERROR;
}
$self
->logger->debug(
"Found NameID content $nameid_content"
);
my
$user
=
$nameid_content
;
my
$userAttribute
=
$self
->idpOptions->{
$idp
}->{samlIDPMetaDataOptionsUserAttribute};
if
(
$userAttribute
) {
$self
->logger->debug(
"Try to set user value from SAML attribute $userAttribute"
);
my
$force_utf8
=
$self
->idpOptions->{
$idp
}->{samlIDPMetaDataOptionsForceUTF8};
my
$attr_statement
=
$assertion
->AttributeStatement();
if
(
$attr_statement
) {
my
@attributes
=
$attr_statement
->Attribute();
my
$value
=
$self
->getAttributeValue(
$userAttribute
,
undef
,
undef
,
\
@attributes
,
$force_utf8
);
if
(
defined
$value
) {
$user
=
$value
;
$self
->logger->debug(
"Found value $value for attribute $userAttribute"
);
}
else
{
$self
->logger->
warn
(
"No value for $userAttribute found in SAML assertion"
);
}
}
else
{
$self
->logger->
warn
(
"No attributes found in SAML assertion"
);
}
}
$req
->user(
$user
);
$self
->logger->debug(
"Set user value to $user"
);
$req
->data->{_lassoLogin} =
$login
;
$req
->data->{_idp} =
$idp
;
$req
->data->{_idpConfKey} =
$idpConfKey
;
$req
->data->{_nameID} =
$nameid
;
$req
->data->{_sessionIndex} =
$session_index
;
my
$saml_token
=
$assertion
->export_to_xml;
$self
->logger->debug(
"SAML Token: $saml_token"
);
$req
->data->{_samlToken} =
$saml_token
;
if
(
$assertion_responded
) {
my
$saml_sessions
=
Lemonldap::NG::Common::Apache::Session->searchOn(
$self
->amOpts,
"ProxyID"
,
$assertion_responded
);
if
(
my
@saml_sessions_keys
=
grep
{
$saml_sessions
->{
$_
}->{_session_kind} eq
$self
->sessionKind
}
keys
%$saml_sessions
)
{
if
(
$#saml_sessions_keys
> 0 ) {
$self
->logger->
warn
(
"More than one SAML proxy session found for ID $assertion_responded"
);
}
my
$saml_session
=
shift
@saml_sessions_keys
;
$self
->logger->debug(
"Retrieve SAML proxy session $saml_session for ID $assertion_responded"
);
my
$samlSessionInfo
=
$self
->getSamlSession(
$saml_session
);
$req
->data->{_proxiedRequest} =
$samlSessionInfo
->data->{Request};
$req
->data->{_proxiedMethod} =
$samlSessionInfo
->data->{Method};
$req
->data->{_proxiedRelayState} =
$samlSessionInfo
->data->{RelayState};
$req
->data->{_proxiedArtifact} =
$samlSessionInfo
->data->{Artifact};
$self
->p->setHiddenFormValue(
$req
,
'SAMLRequest'
,
$self
->{_proxiedRequest} );
$self
->p->setHiddenFormValue(
$req
,
'Method'
,
$self
->{_proxiedMethod} );
$self
->p->setHiddenFormValue(
$req
,
'RelayState'
,
$self
->{_proxiedRelayState} );
$self
->p->setHiddenFormValue(
$req
,
'SAMLart'
,
$self
->{_proxiedArtifact} );
$samlSessionInfo
->remove();
}
}
return
PE_OK;
}
elsif
(
$request
) {
$self
->logger->debug(
"This module do not manage SSO request, see IssuerDBSAML"
);
return
PE_OK;
}
else
{
$self
->userLogger->error(
"SSO request or response was not found"
);
return
PE_SAML_ERROR;
}
}
elsif
(
$url
=~
$self
->sloRe ) {
$self
->logger->debug(
"URL $url detected as an SLO URL"
);
$req
->steps( [ @{
$self
->p->beforeLogout },
'deleteSession'
] );
$req
->data->{nofail} = 1;
my
(
$request
,
$response
,
$method
,
$relaystate
,
$artifact
) =
$self
->checkMessage(
$req
,
$url
,
$request_method
,
$content_type
,
"logout"
);
my
$logout
=
$self
->createLogout(
$self
->lassoServer );
$self
->disableSignatureVerification(
$logout
);
if
(
$response
) {
my
$result
=
$self
->processLogoutResponseMsg(
$logout
,
$response
);
unless
(
$result
) {
$self
->userLogger->error(
"Fail to process logout response"
);
return
PE_SLO_ERROR;
}
$self
->logger->debug(
"Logout response is valid"
);
return
PE_SAML_DESTINATION_ERROR
unless
(
$self
->checkDestination(
$logout
->response,
$url
) );
my
$idp
=
$logout
->remote_providerID();
$self
->logger->debug(
"Found entityID $idp in SAML message"
);
my
$idpConfKey
=
$self
->getIDPConfKey(
$idp
);
unless
(
$idpConfKey
) {
$self
->userLogger->error(
"$idp do not match any IDP in configuration"
);
return
PE_SAML_UNKNOWN_ENTITY;
}
$self
->logger->debug(
"$idp match $idpConfKey IDP in configuration"
);
my
$checkSLOMessageSignature
=
$self
->idpOptions->{
$idp
}
->{samlIDPMetaDataOptionsCheckSLOMessageSignature};
if
(
$checkSLOMessageSignature
) {
$self
->forceSignatureVerification(
$logout
);
$result
=
$self
->processLogoutResponseMsg(
$logout
,
$response
);
unless
(
$result
) {
$self
->userLogger->error(
"Signature is not valid"
);
return
PE_SAML_SIGNATURE_ERROR;
}
else
{
$self
->logger->debug(
"Signature is valid"
);
}
}
else
{
$self
->logger->debug(
"Message signature will not be checked"
);
}
my
$samlID
=
$logout
->response()->InResponseTo;
unless
(
$self
->replayProtection(
$samlID
) ) {
$self
->userLogger->error(
"Message $samlID already used or expired"
);
return
PE_SLO_ERROR;
}
if
(
$self
->extractRelayState(
$req
,
$relaystate
) ) {
$self
->logger->debug(
"RelayState $relaystate extracted"
);
$self
->logger->debug(
"URL "
.
$req
->urldc .
" found in RelayState"
);
}
if
(
$req
->urldc
and
$req
->portal !~ /
$req
->{urldc}\/?/ )
{
$req
->steps( [] );
$req
->user(
'TODO'
);
return
PE_OK;
}
$req
->user(
'TODO'
);
return
PE_OK;
}
elsif
(
$request
) {
my
$logout_error
= 0;
my
$session_dump
;
unless
(
$self
->processLogoutRequestMsg(
$logout
,
$request
) ) {
$self
->userLogger->error(
"Fail to process logout request"
);
return
PE_SLO_ERROR;
}
$self
->logger->debug(
"Logout request is valid"
);
return
PE_SAML_DESTINATION_ERROR
unless
(
$self
->checkDestination(
$logout
->request,
$url
) );
my
$idp
=
$logout
->remote_providerID();
$self
->logger->debug(
"Found entityID $idp in SAML message"
);
my
$idpConfKey
=
$self
->getIDPConfKey(
$idp
);
unless
(
$idpConfKey
) {
$self
->userLogger->error(
"$idp do not match any IDP in configuration"
);
return
PE_SAML_UNKNOWN_ENTITY;
}
$self
->logger->debug(
"$idp match $idpConfKey IDP in configuration"
);
my
$checkSLOMessageSignature
=
$self
->idpOptions->{
$idp
}
->{samlIDPMetaDataOptionsCheckSLOMessageSignature};
if
(
$checkSLOMessageSignature
) {
unless
(
$self
->checkSignatureStatus(
$logout
) ) {
$self
->userLogger->error(
"Signature is not valid"
);
return
PE_SAML_SIGNATURE_ERROR;
}
else
{
$self
->logger->debug(
"Signature is valid"
);
}
}
else
{
$self
->logger->debug(
"Message signature will not be checked"
);
}
my
$name_id
=
$logout
->request()->NameID;
my
$session_index
=
$logout
->request()->SessionIndex;
my
$user
=
$name_id
->content;
unless
(
$name_id
) {
$self
->userLogger->error(
"Fail to get NameID from logout request"
);
$logout_error
= 1;
}
$self
->logger->debug(
"Logout request NameID content: $user"
);
my
$local_sessions
=
Lemonldap::NG::Common::Apache::Session->searchOn(
$self
->amOpts,
"_nameID"
,
$name_id
->
dump
);
if
(
my
@local_sessions_keys
=
grep
{
$local_sessions
->{
$_
}->{_session_kind} eq
$self
->sessionKind
}
keys
%$local_sessions
)
{
foreach
(
@local_sessions_keys
) {
my
$local_session
=
$_
;
$self
->logger->debug(
"Retrieve SAML session $local_session for user $user"
);
my
$sessionInfo
=
$self
->getSamlSession(
$local_session
);
if
(
defined
$session_index
and
$session_index
ne
$sessionInfo
->data->{_sessionIndex} )
{
$self
->logger->debug(
"Session $local_session has not the good session index, skipping"
);
next
;
}
else
{
my
$real_session
=
$sessionInfo
->data->{_saml_id};
my
$ssoSession
=
$self
->p->getApacheSession(
$real_session
);
$self
->importRealSession(
$req
,
$ssoSession
);
if
(
$ssoSession
&&
$ssoSession
->data->{
$self
->lsDump } )
{
$self
->logger->debug(
"Get Lasso::Session dump from session $real_session"
);
$session_dump
=
$ssoSession
->data->{
$self
->lsDump };
}
my
$del_saml_result
=
$sessionInfo
->remove();
$self
->logger->debug(
"Delete SAML session $local_session result: $del_saml_result"
);
$logout_error
= 1
unless
$del_saml_result
;
}
}
unless
(
$self
->setSessionFromDump(
$logout
,
$session_dump
) ) {
$self
->userLogger->error(
"Cannot set session from dump in logout"
);
$logout_error
= 1;
}
}
else
{
$self
->logger->debug(
"No SAML session found for user $user"
);
$logout_error
= 1;
}
unless
(
$logout_error
) {
unless
(
$self
->validateLogoutRequest(
$logout
) ) {
$self
->userLogger->error(
"SLO request is not valid"
);
}
}
if
(
$relaystate
) {
$logout
->msg_relayState(
$relaystate
);
$self
->logger->debug(
"Set $relaystate in RelayState"
);
}
my
$signSLOMessage
=
$self
->idpOptions->{
$idp
}->{samlIDPMetaDataOptionsSignSLOMessage};
if
(
$signSLOMessage
== 0 ) {
$self
->logger->debug(
"SLO message to IDP $idpConfKey will not be signed"
);
$self
->disableSignature(
$logout
);
}
elsif
(
$signSLOMessage
== 1 ) {
$self
->logger->debug(
"SLO message to IDP $idpConfKey will be signed"
);
$self
->forceSignature(
$logout
);
}
else
{
$self
->logger->debug(
"SLO message to IDP $idpConfKey signature according to metadata"
);
}
unless
(
$self
->buildLogoutResponseMsg(
$logout
) ) {
$self
->logger->error(
"Unable to build SLO response"
);
return
PE_SLO_ERROR;
}
if
(
$method
== Lasso::Constants::HTTP_METHOD_REDIRECT ) {
my
$slo_url
=
$logout
->msg_url;
$self
->logger->debug(
"Redirect user to $slo_url"
);
$req
->urldc(
$slo_url
);
return
PE_OK;
}
elsif
(
$method
== Lasso::Constants::HTTP_METHOD_POST ) {
my
$slo_url
=
$logout
->msg_url;
my
$slo_body
=
$logout
->msg_body;
$req
->postUrl(
$slo_url
);
$req
->postFields( {
'SAMLResponse'
=>
$slo_body
} );
if
(
$relaystate
) {
$req
->{postFields}->{
'RelayState'
} =
encode_entities(
$relaystate
);
$req
->data->{safeHiddenFormValues}->{RelayState} = 1;
}
push
@{
$req
->steps },
'autoPost'
;
return
PE_OK;
}
elsif
(
$method
== Lasso::Constants::HTTP_METHOD_SOAP ) {
my
$slo_body
=
$logout
->msg_body;
$self
->logger->debug(
"SOAP response $slo_body"
);
$req
->response( [
200,
[
'Content-Type'
=>
'text/xml'
,
'Content-Length'
=>
length
(
$slo_body
)
],
[
$slo_body
]
]
);
$req
->steps( [
@{
$self
->p->beforeLogout },
sub
{
my
(
$req
) =
@_
;
my
$res
=
$self
->p->deleteSession(
$req
);
return
(
$res
eq PE_LOGOUT_OK ? PE_SENDRESPONSE :
$res
);
}
]
);
$req
->user(
'SOAP client'
);
return
PE_OK;
}
}
else
{
$self
->userLogger->error(
"SLO request or response was not found"
);
$req
->mustRedirect(1);
$req
->steps( [] );
return
PE_REDIRECT;
}
}
elsif
(
$url
=~
$self
->artRe ) {
$self
->logger->debug(
"URL $url detected as an artifact resolution service URL"
);
my
$art_request
=
$req
->content;
my
$art_response
;
my
$login
=
$self
->createLogin(
$self
->lassoServer );
unless
(
$self
->processArtRequestMsg(
$login
,
$art_request
) ) {
$self
->userLogger->error(
"Unable to process artifact request message"
);
return
PE_SAML_ART_ERROR;
}
return
PE_SAML_DESTINATION_ERROR
unless
(
$self
->checkDestination(
$login
->request,
$url
) );
unless
(
$art_response
=
$self
->createArtifactResponse(
$req
,
$login
) )
{
$self
->logger(
"Unable to create artifact response message"
);
return
PE_SAML_ART_ERROR;
}
$req
->response( [
200,
[
'Content-Type'
=>
'text/xml'
,
'Content-Length'
=>
length
(
$art_response
)
],
[
$art_response
]
]
);
$req
->user(
'SOAP client'
);
return
PE_SENDRESPONSE;
}
my
$idp
=
$self
->getIDP(
$req
);
if
( !
$idp
and
$self
->conf->{samlCommonDomainCookieActivation}
and
$self
->conf->{samlCommonDomainCookieReader} )
{
$self
->logger->debug(
"Will try to use Common Domain Cookie for IDP resolution"
);
my
$return_url
= encode_base64(
$self
->p->fullUrl(
$req
),
''
);
my
$cdc_reader_url
=
$self
->conf->{samlCommonDomainCookieReader};
$cdc_reader_url
.= (
$cdc_reader_url
=~ /\?/ ?
'&'
:
'?'
)
. build_urlencoded(
url
=>
$return_url
);
$self
->logger->debug(
"Redirect user to $cdc_reader_url"
);
$req
->urldc(
$cdc_reader_url
);
$req
->steps( [] );
return
PE_REDIRECT;
}
if
( !
$idp
and
$self
->conf->{samlDiscoveryProtocolActivation}
and
defined
$self
->conf->{samlDiscoveryProtocolURL} )
{
$self
->logger->debug(
"Will try to use SAML Discovery Protocol for IDP resolution"
);
if
(
$req
->urldc ) {
$req
->pdata->{_url} = encode_base64(
$req
->urldc,
''
);
}
my
$disco_url
=
$self
->conf->{samlDiscoveryProtocolURL};
my
$portal
=
$req
->portal;
$disco_url
.= (
$disco_url
=~ /\?/ ?
'&'
:
'?'
)
. build_urlencoded(
entityID
=>
$self
->getMetaDataURL(
'samlEntityID'
, 0, 1 ),
return
=>
$portal
,
returnIDParam
=>
'idp'
);
if
(
defined
$self
->conf->{samlDiscoveryProtocolPolicy} ) {
my
$dppolicy
=
$self
->conf->{samlDiscoveryProtocolPolicy};
$disco_url
.=
"&"
. build_urlencoded(
policy
=>
$dppolicy
);
}
if
(
defined
$self
->conf->{samlDiscoveryProtocolIsPassive} ) {
my
$dpispassive
=
$self
->conf->{samlDiscoveryProtocolIsPassive};
$disco_url
.=
"&"
. build_urlencoded(
isPassive
=>
$dpispassive
?
"true"
:
"false"
);
}
$self
->logger->debug(
"Redirect user to $disco_url"
);
$req
->urldc(
$disco_url
);
$req
->steps( [] );
return
PE_REDIRECT;
}
unless
(
$idp
) {
$self
->logger->debug(
"Redirecting user to IDP list"
);
my
$urlcheck
=
$self
->p->controlUrl(
$req
);
return
$urlcheck
unless
(
$urlcheck
== PE_OK );
my
@list
= ();
foreach
(
keys
%{
$self
->idpList } ) {
my
$idpName
=
$self
->{idpList}->{
$_
}->{name};
$idpName
=
$self
->{idpList}->{
$_
}->{displayName}
if
$self
->{idpList}->{
$_
}->{displayName};
my
$icon
=
$self
->{idpList}->{
$_
}->{icon};
my
$order
=
$self
->{idpList}->{
$_
}->{order} // 0;
my
$tooltip
=
$self
->{idpList}->{
$_
}->{tooltip} ||
$idpName
;
my
$img_src
=
''
;
if
(
$icon
) {
$img_src
=
(
$icon
=~ m
?
$icon
:
$self
->p->staticPrefix .
"/common/"
.
$icon
;
}
$self
->logger->debug(
"IDP "
.
$self
->{idpList}->{
$_
}->{name}
.
" -> DisplayName : $idpName with Icon : $img_src at order : $order"
);
push
@list
,
{
val
=>
$_
,
name
=>
$idpName
,
title
=>
$tooltip
,
icon
=>
$img_src
,
order
=>
$order
};
}
@list
=
sort
{
$a
->{order} <=>
$b
->{order}
or
$a
->{name} cmp
$b
->{name}
or
$a
->{val} cmp
$b
->{val}
}
@list
;
$req
->data->{list} = \
@list
;
$req
->data->{login} = 1;
return
PE_IDPCHOICE;
}
my
$idpConfKey
=
$self
->getIDPConfKey(
$idp
);
unless
(
$idpConfKey
) {
$self
->userLogger->error(
"$idp do not match any IDP in configuration"
);
return
PE_SAML_UNKNOWN_ENTITY;
}
$self
->logger->debug(
"$idp match $idpConfKey IDP in configuration"
);
my
$forceAuthn
=
$self
->idpOptions->{
$idp
}->{samlIDPMetaDataOptionsForceAuthn};
my
$isPassive
=
$self
->idpOptions->{
$idp
}->{samlIDPMetaDataOptionsIsPassive};
my
$nameIDFormat
=
$self
->idpOptions->{
$idp
}->{samlIDPMetaDataOptionsNameIDFormat};
$nameIDFormat
=
$self
->getNameIDFormat(
$nameIDFormat
)
if
$nameIDFormat
;
my
$method
=
$self
->idpOptions->{
$idp
}->{samlIDPMetaDataOptionsSSOBinding};
$method
=
$self
->getHttpMethod(
$method
);
unless
(
defined
$method
) {
my
$protocolType
= Lasso::Constants::MD_PROTOCOL_TYPE_SINGLE_SIGN_ON;
$method
=
$self
->getFirstHttpMethod(
$self
->lassoServer,
$idp
,
$protocolType
);
}
unless
(
defined
$method
and
$method
!= -1 ) {
$self
->logger->debug(
"No method found with IDP $idpConfKey for SSO profile"
);
$method
=
$self
->getHttpMethod(
"redirect"
);
}
$self
->logger->debug(
"Use method "
.
$self
->getHttpMethodString(
$method
)
.
" with IDP $idpConfKey for SSO profile"
);
my
$signSSOMessage
=
$self
->idpOptions->{
$idp
}->{samlIDPMetaDataOptionsSignSSOMessage} // -1;
my
$requestedAuthnContext
=
$self
->idpOptions->{
$idp
}->{samlIDPMetaDataOptionsRequestedAuthnContext}
//
''
;
$requestedAuthnContext
=
$self
->getAuthnContext(
$requestedAuthnContext
)
if
$requestedAuthnContext
;
my
$login
=
$self
->createAuthnRequest(
$req
,
$self
->lassoServer,
$idp
,
$method
,
$forceAuthn
,
$isPassive
,
$nameIDFormat
, 0,
$signSSOMessage
,
$requestedAuthnContext
);
unless
(
$login
) {
$self
->userLogger->error(
"Could not create authentication request on $idpConfKey"
);
return
PE_SAML_SSO_ERROR;
}
$self
->logger->debug(
"Authentication request created"
);
my
$samlID
=
$login
->request()->ID;
unless
(
$self
->storeReplayProtection(
$samlID
) ) {
$self
->logger->error(
"Unable to store assertion ID"
);
return
PE_SAML_SSO_ERROR;
}
if
(
$req
->data->{_proxiedSamlRequest} ) {
my
$infos
;
$infos
->{type} =
'proxy'
;
$infos
->{_utime} =
time
;
$infos
->{Request} =
$req
->data->{_proxiedRequest};
$infos
->{Method} =
$req
->data->{_proxiedMethod};
$infos
->{RelayState} =
$req
->data->{_proxiedRelayState};
$infos
->{Artifact} =
$req
->data->{_proxiedArtifact};
$infos
->{ProxyID} =
$samlID
;
my
$samlSessionInfo
=
$self
->getSamlSession(
undef
,
$infos
);
return
PE_SAML_SESSION_ERROR
unless
$samlSessionInfo
;
$self
->logger->debug(
"Keep initial SAML request data in memory for ID $samlID"
);
}
if
(
$method
==
$self
->getHttpMethod(
'redirect'
)
or
$method
==
$self
->getHttpMethod(
'artifact-get'
) )
{
my
$sso_url
=
$login
->msg_url;
$self
->logger->debug(
"Redirect user to $sso_url"
);
$req
->urldc(
$sso_url
);
return
PE_REDIRECT;
}
elsif
(
$method
==
$self
->getHttpMethod(
'post'
)
or
$method
==
$self
->getHttpMethod(
'artifact-post'
) )
{
my
$sso_url
=
$login
->msg_url;
my
$sso_body
=
$login
->msg_body;
$req
->postUrl(
$sso_url
);
if
(
$method
==
$self
->getHttpMethod(
"artifact-post"
) ) {
$req
->{postFields} = {
'SAMLart'
=>
$sso_body
};
}
else
{
$req
->{postFields} = {
'SAMLRequest'
=>
$sso_body
};
}
if
(
$login
->msg_relayState ) {
$req
->{postFields}->{
'RelayState'
} =
encode_entities(
$login
->msg_relayState );
$req
->data->{safeHiddenFormValues}->{RelayState} = 1;
}
$req
->steps( [
'autoPost'
] );
$req
->
continue
(1);
return
PE_OK;
}
return
PE_SAML_SSO_ERROR;
}
sub
authenticate {
return
PE_OK;
}
sub
setAuthSessionInfo {
my
(
$self
,
$req
) =
@_
;
my
$login
=
$req
->data->{_lassoLogin};
my
$idp
=
$req
->data->{_idp};
my
$idpConfKey
=
$req
->data->{_idpConfKey};
my
$nameIdentifier
=
$req
->data->{_nameID};
my
$session_index
=
$req
->data->{_sessionIndex};
my
$assertion
=
$self
->getAssertion(
$login
);
unless
(
$assertion
) {
$self
->userLogger->error(
"No assertion found"
);
return
PE_SAML_SSO_ERROR;
}
my
$force_utf8
=
$self
->idpOptions->{
$idp
}->{samlIDPMetaDataOptionsForceUTF8};
my
$attr_statement
=
$assertion
->AttributeStatement();
if
(
$attr_statement
) {
my
@attributes
=
$attr_statement
->Attribute();
foreach
(
keys
%{
$self
->idpAttributes->{
$idp
} } ) {
my
(
$mandatory
,
$name
,
$format
,
$friendly_name
) =
split
( /;/,
$self
->idpAttributes->{
$idp
}->{
$_
} );
my
$value
=
$self
->getAttributeValue(
$name
,
$format
,
$friendly_name
,
\
@attributes
,
$force_utf8
);
$req
->{sessionInfo}->{
$_
} =
$value
if
defined
$value
;
}
}
$req
->{sessionInfo}->{_idp} =
$idp
;
$req
->{sessionInfo}->{_idpConfKey} =
$idpConfKey
;
my
$sessionNotOnOrAfter
;
eval
{
$sessionNotOnOrAfter
=
$assertion
->AuthnStatement()->SessionNotOnOrAfter();
};
if
( $@ or !
$sessionNotOnOrAfter
) {
$self
->logger->debug(
"No SessionNotOnOrAfter value found"
);
}
else
{
my
$samltime
=
$self
->samldate2timestamp(
$sessionNotOnOrAfter
);
my
$utime
=
time
();
my
$timeout
=
$self
->conf->{timeout};
my
$adaptSessionUtime
=
$self
->idpOptions->{
$idp
}->{samlIDPMetaDataOptionsAdaptSessionUtime};
if
( (
$utime
+
$timeout
>
$samltime
) and
$adaptSessionUtime
) {
my
$new_utime
=
$samltime
-
$timeout
;
$req
->{sessionInfo}->{_utime} =
$new_utime
;
$self
->logger->debug(
"Adapt _utime with SessionNotOnOrAfter value, new _utime: $new_utime"
);
}
}
unless
(
$self
->acceptSSO(
$login
) ) {
$self
->userLogger->error(
"Error while accepting SSO from IDP $idpConfKey"
);
return
PE_SAML_SSO_ERROR;
}
my
$session
=
$login
->get_session;
my
$identity
=
$login
->get_identity;
$req
->{sessionInfo}->{
$self
->lsDump } =
$session
->
dump
()
if
$session
;
$req
->{sessionInfo}->{
$self
->liDump } =
$identity
->
dump
()
if
$identity
;
$req
->{sessionInfo}->{
$self
->niDump } =
$nameIdentifier
->
dump
()
if
ref
(
$nameIdentifier
);
$req
->{sessionInfo}->{
$self
->sIndex } =
$session_index
if
$session_index
;
my
$store_samlToken
=
$self
->idpOptions->{
$idp
}->{samlIDPMetaDataOptionsStoreSAMLToken};
if
(
$store_samlToken
) {
$self
->logger->debug(
"Store SAML Token in session"
);
$req
->{sessionInfo}->{_samlToken} =
$req
->data->{_samlToken};
}
else
{
$self
->logger->debug(
"SAML Token will not be stored in session"
);
}
$req
->data->{_lassoLogin} =
$login
;
return
PE_OK;
}
sub
authFinish {
my
(
$self
,
$req
) =
@_
;
my
$nameid
=
$req
->sessionInfo->{
$self
->niDump };
my
$session_index
=
$req
->sessionInfo->{
$self
->sIndex };
return
unless
(
$nameid
);
my
$id
=
$req
->{id};
my
$utime
=
$req
->{sessionInfo}->{_utime};
$self
->logger->debug(
"Store NameID "
.
$nameid
. (
$session_index
?
" and SessionIndex $session_index"
:
""
)
.
" for session $id"
);
my
$infos
;
$infos
->{type} =
'saml'
;
$infos
->{_utime} =
$utime
;
$infos
->{_saml_id} =
$id
;
$infos
->{_nameID} =
$nameid
;
$infos
->{_sessionIndex} =
$session_index
;
my
$samlSessionInfo
=
$self
->getSamlSession(
undef
,
$infos
);
return
PE_SAML_SESSION_ERROR
unless
$samlSessionInfo
;
my
$session_id
=
$samlSessionInfo
->id;
$self
->logger->debug(
"Link session $id to SAML session $session_id"
);
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
;
$self
->deleteSAMLSecondarySessions(
$session_id
);
my
$session_dump
=
$req
->{sessionInfo}->{
$self
->lsDump };
unless
(
$session_dump
) {
$self
->userLogger->error(
"Could not get session dump from session"
);
return
PE_SLO_ERROR;
}
$method
=
$self
->idpOptions->{
$idp
}->{samlIDPMetaDataOptionsSLOBinding};
$method
=
$self
->getHttpMethod(
$method
);
unless
(
defined
$method
) {
my
$protocolType
= Lasso::Constants::MD_PROTOCOL_TYPE_SINGLE_LOGOUT;
$method
=
$self
->getFirstHttpMethod(
$self
->lassoServer,
$idp
,
$protocolType
);
}
unless
(
defined
$method
and
$method
!= -1 ) {
$self
->logger->debug(
"No method found with IDP $idpConfKey for SLO profile"
);
return
PE_OK;
}
$self
->logger->debug(
"Use method "
.
$self
->getHttpMethodString(
$method
)
.
" with IDP $idpConfKey for SLO profile"
);
my
$signSLOMessage
=
$self
->idpOptions->{
$idp
}->{samlIDPMetaDataOptionsSignSLOMessage} // 0;
my
$logout
=
$self
->createLogoutRequest(
$req
,
$self
->lassoServer,
$session_dump
,
$method
,
$signSLOMessage
);
unless
(
$logout
) {
$self
->logger->error(
"Could not create logout request"
);
return
PE_SLO_ERROR;
}
$self
->logger->debug(
"Logout request created"
);
unless
(
$self
->storeReplayProtection(
$logout
->request()->ID ) ) {
$self
->logger->error(
"Unable to store Logout request ID"
);
return
PE_SLO_ERROR;
}
if
(
$method
== Lasso::Constants::HTTP_METHOD_REDIRECT
or
$method
== Lasso::Constants::HTTP_METHOD_ARTIFACT_GET )
{
my
$slo_url
=
$logout
->msg_url;
$self
->logger->debug(
"Redirect user to $slo_url"
);
$req
->urldc(
$slo_url
);
return
PE_OK;
}
elsif
(
$method
== Lasso::Constants::HTTP_METHOD_POST
or
$method
== Lasso::Constants::HTTP_METHOD_ARTIFACT_POST )
{
my
$slo_url
=
$logout
->msg_url;
my
$slo_body
=
$logout
->msg_body;
$self
->logger->debug(
"Redirect user to $slo_url using autoPost"
);
$req
->postUrl(
$slo_url
);
$req
->postFields( {
'SAMLRequest'
=>
$slo_body
} );
if
(
$logout
->msg_relayState ) {
$req
->{postFields}->{
'RelayState'
} =
encode_entities(
$logout
->msg_relayState );
$req
->data->{safeHiddenFormValues}->{RelayState} = 1;
}
$req
->steps( [
'deleteSession'
,
'autoPost'
] );
return
PE_OK;
}
elsif
(
$method
== Lasso::Constants::HTTP_METHOD_SOAP ) {
my
$slo_url
=
$logout
->msg_url;
my
$slo_body
=
$logout
->msg_body;
my
$response
=
$self
->sendSOAPMessage(
$slo_url
,
$slo_body
);
unless
(
$response
) {
$self
->logger->error(
"No logout response to SOAP request"
);
return
PE_SLO_ERROR;
}
$logout
=
$self
->createLogout(
$self
->lassoServer );
my
$checkSLOMessageSignature
=
$self
->idpOptions->{
$idp
}
->{samlIDPMetaDataOptionsCheckSLOMessageSignature};
unless
(
$checkSLOMessageSignature
) {
$self
->disableSignatureVerification(
$logout
);
}
my
$result
=
$self
->processLogoutResponseMsg(
$logout
,
$response
);
unless
(
$result
) {
$self
->logger->error(
"Fail to process logout response"
);
return
PE_SLO_ERROR;
}
$self
->logger->debug(
"Logout response is valid"
);
my
$samlID
=
$logout
->response()->InResponseTo;
unless
(
$self
->replayProtection(
$samlID
) ) {
$self
->userLogger->error(
"Message $samlID already used or expired"
);
return
PE_SLO_ERROR;
}
return
PE_OK;
}
else
{
$self
->userLogger->error(
"Lasso method $method not implemented here"
);
return
PE_SLO_ERROR;
}
}
sub
handleAuthRequests {
my
(
$self
,
$req
) =
@_
;
if
(
$req
->uri =~
$self
->sloRe ) {
return
$self
->extractFormInfo(
$req
);
}
return
PE_OK;
}
sub
getDisplayType {
return
"logo"
;
}
sub
getIDPConfKey {
my
(
$self
,
$entityID
) =
@_
;
if
(
$self
->idpList->{
$entityID
} ) {
return
$self
->idpList->{
$entityID
}->{confKey};
}
return
undef
;
}
sub
getEntityID {
my
(
$self
,
$confKey
) =
@_
;
foreach
(
keys
%{
$self
->idpList } ) {
my
$idpConfKey
=
$self
->idpList->{
$_
}->{confKey} //
""
;
if
(
$confKey
eq
$idpConfKey
) {
return
$_
;
}
}
return
undef
;
}
sub
getIDP {
my
(
$self
,
$req
) =
@_
;
my
$idp
;
my
$idpName
;
unless
(
$idp
=
$req
->param(
"idp"
) ) {
if
(
$idpName
=
$req
->param(
"idpName"
) ) {
$idp
=
$self
->getEntityID(
$idpName
);
$self
->logger->debug(
"IDP $idp selected from idpName URL Parameter ($idpName)"
)
if
$idp
;
}
else
{
foreach
(
keys
%{
$self
->idpList } ) {
my
$idpConfKey
=
$self
->idpList->{
$_
}->{confKey};
my
$cond
=
$self
->idpRules->{
$_
} or
next
;
if
(
$cond
->(
$req
,
$req
->sessionInfo ) ) {
$self
->logger->debug(
"IDP $idpConfKey selected from resolution rule"
);
$idp
=
$_
;
last
;
}
}
}
$self
->logger->debug(
'No IDP selected'
)
unless
(
$idp
);
}
else
{
$self
->logger->debug(
"IDP $idp selected from idp URL Parameter"
);
}
$self
->lazy_load_entityid(
$idp
)
if
$idp
;
if
(
scalar
keys
%{
$self
->idpList } == 1
and not(
$self
->conf->{samlDiscoveryProtocolActivation}
and
defined
$self
->conf->{samlDiscoveryProtocolURL} )
)
{
(
$idp
) =
keys
%{
$self
->idpList };
$self
->logger->debug(
"Selecting the only defined SAML IDP: $idp"
);
}
if
(
$idp
and !
exists
$self
->idpList->{
$idp
} ) {
$self
->logger->error(
"Required IDP $idp does not exists"
);
$idp
=
undef
;
}
return
$idp
;
}
1;