2016-09-25 18:44:48 +02:00
|
|
|
package Lemonldap::NG::Portal::Auth::SAML;
|
|
|
|
|
|
|
|
use strict;
|
2018-07-18 10:31:54 +02:00
|
|
|
use MIME::Base64 qw/encode_base64/;
|
2016-09-25 18:44:48 +02:00
|
|
|
use Mouse;
|
2017-10-26 11:16:44 +02:00
|
|
|
use Lemonldap::NG::Portal::Lib::SAML;
|
2018-06-26 19:13:06 +02:00
|
|
|
use Lemonldap::NG::Common::FormEncode;
|
2016-09-25 18:44:48 +02:00
|
|
|
use Lemonldap::NG::Portal::Main::Constants qw(
|
|
|
|
PE_CONFIRM
|
2018-06-29 17:51:39 +02:00
|
|
|
PE_IDPCHOICE
|
2016-09-25 18:44:48 +02:00
|
|
|
PE_LOGOUT_OK
|
2016-12-04 22:57:25 +01:00
|
|
|
PE_REDIRECT
|
2016-09-25 18:44:48 +02:00
|
|
|
PE_OK
|
|
|
|
PE_SAML_ART_ERROR
|
|
|
|
PE_SAML_CONDITIONS_ERROR
|
|
|
|
PE_SAML_DESTINATION_ERROR
|
2016-11-29 22:10:00 +01:00
|
|
|
PE_SAML_ERROR
|
2016-09-25 18:44:48 +02:00
|
|
|
PE_SAML_IDPSSOINITIATED_NOTALLOWED
|
|
|
|
PE_SAML_SESSION_ERROR
|
|
|
|
PE_SAML_SIGNATURE_ERROR
|
|
|
|
PE_SAML_SLO_ERROR
|
|
|
|
PE_SAML_SSO_ERROR
|
|
|
|
PE_SAML_UNKNOWN_ENTITY
|
2016-12-18 09:07:48 +01:00
|
|
|
PE_SENDRESPONSE
|
2016-09-25 18:44:48 +02:00
|
|
|
);
|
|
|
|
|
2019-02-12 18:21:38 +01:00
|
|
|
our $VERSION = '2.1.0';
|
2016-09-25 18:44:48 +02:00
|
|
|
|
2018-02-19 22:11:43 +01:00
|
|
|
extends 'Lemonldap::NG::Portal::Main::Auth', 'Lemonldap::NG::Portal::Lib::SAML';
|
2016-09-25 18:44:48 +02:00
|
|
|
|
2016-12-25 16:41:23 +01:00
|
|
|
# INTERFACE
|
|
|
|
|
2018-06-22 13:08:18 +02:00
|
|
|
has ssoAssConsumerRe => ( is => 'rw' );
|
2016-11-16 16:27:01 +01:00
|
|
|
has sloRe => ( is => 'rw' );
|
|
|
|
has artRe => ( is => 'rw' );
|
2018-06-18 23:19:33 +02:00
|
|
|
has catch => ( is => 'rw' );
|
2018-06-29 06:50:31 +02:00
|
|
|
use constant sessionKind => 'SAML';
|
|
|
|
use constant lsDump => '_lassoSessionDump';
|
|
|
|
use constant liDump => '_lassoIdentityDump';
|
2016-11-16 16:27:01 +01:00
|
|
|
|
2016-12-25 16:41:23 +01:00
|
|
|
sub forAuthUser { 'handleAuthRequests' }
|
|
|
|
|
2016-09-26 21:12:40 +02:00
|
|
|
# INITIALIZATION
|
|
|
|
|
2016-09-25 18:44:48 +02:00
|
|
|
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");
|
2018-06-22 13:08:18 +02:00
|
|
|
$self->ssoAssConsumerRe(
|
2016-12-07 23:30:00 +01:00
|
|
|
qr/^($saml_acs_art_url|$saml_acs_post_url|$saml_acs_get_url)(?:\?.*)?$/i
|
|
|
|
);
|
2016-09-25 18:44:48 +02:00
|
|
|
my $saml_slo_soap_url =
|
|
|
|
$self->getMetaDataURL( "samlSPSSODescriptorSingleLogoutServiceSOAP", 1 );
|
|
|
|
my $saml_slo_soap_url_ret =
|
2016-11-16 16:27:01 +01:00
|
|
|
$self->getMetaDataURL( "samlSPSSODescriptorSingleLogoutServiceSOAP", 1 );
|
2016-09-25 18:44:48 +02:00
|
|
|
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 );
|
2016-11-16 16:27:01 +01:00
|
|
|
$self->sloRe(
|
2016-12-07 23:30:00 +01:00
|
|
|
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
|
2016-11-16 16:27:01 +01:00
|
|
|
);
|
|
|
|
|
2016-09-25 18:44:48 +02:00
|
|
|
my $saml_ars_url = $self->getMetaDataURL(
|
|
|
|
"samlSPSSODescriptorArtifactResolutionServiceArtifact");
|
2016-12-07 23:30:00 +01:00
|
|
|
$self->artRe(qr/^($saml_ars_url)(?:\?.*)?$/i);
|
2018-06-22 13:08:18 +02:00
|
|
|
$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
|
|
|
|
);
|
2016-11-16 16:27:01 +01:00
|
|
|
|
|
|
|
# Load SAML service and SAML IdP list
|
|
|
|
return ( $self->SUPER::init and $self->loadIDPs );
|
|
|
|
}
|
|
|
|
|
|
|
|
# RUNNING METHODS
|
|
|
|
|
|
|
|
sub extractFormInfo {
|
|
|
|
my ( $self, $req ) = @_;
|
|
|
|
|
2016-12-19 21:47:44 +01:00
|
|
|
# 1. Get HTTP request information to know
|
2016-11-16 16:27:01 +01:00
|
|
|
# if we are receving SAML request or response
|
|
|
|
my $url = $req->uri;
|
|
|
|
my $request_method = $req->method;
|
2017-01-04 17:36:54 +01:00
|
|
|
my $content_type = $req->content_type;
|
2016-09-25 18:44:48 +02:00
|
|
|
|
|
|
|
# 1.1 SSO assertion consumer
|
2018-06-22 13:08:18 +02:00
|
|
|
if ( $url =~ $self->ssoAssConsumerRe ) {
|
2017-02-15 07:41:50 +01:00
|
|
|
$self->logger->debug(
|
|
|
|
"URL $url detected as an SSO assertion consumer URL");
|
2016-09-25 18:44:48 +02:00
|
|
|
|
|
|
|
# Check SAML Message
|
|
|
|
my ( $request, $response, $method, $relaystate, $artifact ) =
|
2016-11-28 22:15:57 +01:00
|
|
|
$self->checkMessage( $req, $url, $request_method, $content_type,
|
|
|
|
"login" );
|
2016-09-25 18:44:48 +02:00
|
|
|
|
|
|
|
# Create Login object
|
|
|
|
my $login = $self->createLogin( $self->lassoServer );
|
|
|
|
|
|
|
|
# Ignore signature verification
|
|
|
|
$self->disableSignatureVerification($login);
|
|
|
|
|
|
|
|
if ($response) {
|
|
|
|
|
|
|
|
# Process authentication response
|
|
|
|
my $result;
|
|
|
|
if ($artifact) {
|
|
|
|
$result = $self->processArtResponseMsg( $login, $response );
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
$result = $self->processAuthnResponseMsg( $login, $response );
|
|
|
|
}
|
|
|
|
|
|
|
|
unless ($result) {
|
2017-02-15 07:41:50 +01:00
|
|
|
$self->logger->error(
|
|
|
|
"SAML SSO: Fail to process authentication response");
|
2016-09-25 18:44:48 +02:00
|
|
|
return PE_SAML_SSO_ERROR;
|
|
|
|
}
|
|
|
|
|
2017-02-15 07:41:50 +01:00
|
|
|
$self->logger->debug("SSO: authentication response is valid");
|
2016-09-25 18:44:48 +02:00
|
|
|
|
|
|
|
# Get IDP entityID
|
|
|
|
my $idp = $login->remote_providerID();
|
|
|
|
|
2017-02-15 07:41:50 +01:00
|
|
|
$self->logger->debug("Found entityID $idp in SAML message");
|
2016-09-25 18:44:48 +02:00
|
|
|
|
|
|
|
# IDP conf key
|
|
|
|
my $idpConfKey = $self->idpList->{$idp}->{confKey};
|
|
|
|
|
|
|
|
unless ($idpConfKey) {
|
2017-02-15 07:41:50 +01:00
|
|
|
$self->userLogger->error(
|
|
|
|
"$idp do not match any IDP in configuration");
|
2016-09-25 18:44:48 +02:00
|
|
|
return PE_SAML_UNKNOWN_ENTITY;
|
|
|
|
}
|
|
|
|
|
2017-02-15 07:41:50 +01:00
|
|
|
$self->logger->debug("$idp match $idpConfKey IDP in configuration");
|
2016-09-25 18:44:48 +02:00
|
|
|
|
|
|
|
# Do we check signature?
|
|
|
|
my $checkSSOMessageSignature =
|
|
|
|
$self->conf->{samlIDPMetaDataOptions}->{$idpConfKey}
|
|
|
|
->{samlIDPMetaDataOptionsCheckSSOMessageSignature};
|
|
|
|
|
|
|
|
if ($checkSSOMessageSignature) {
|
|
|
|
|
|
|
|
$self->forceSignatureVerification($login);
|
|
|
|
|
|
|
|
if ($artifact) {
|
|
|
|
$result = $self->processArtResponseMsg( $login, $response );
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
$result =
|
|
|
|
$self->processAuthnResponseMsg( $login, $response );
|
|
|
|
}
|
|
|
|
|
|
|
|
unless ($result) {
|
2017-02-18 15:25:48 +01:00
|
|
|
$self->userLogger->error("Signature is not valid");
|
2016-09-25 18:44:48 +02:00
|
|
|
return PE_SAML_SIGNATURE_ERROR;
|
|
|
|
}
|
|
|
|
else {
|
2017-02-15 07:41:50 +01:00
|
|
|
$self->logger->debug("Signature is valid");
|
2016-09-25 18:44:48 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
else {
|
2017-02-15 07:41:50 +01:00
|
|
|
$self->logger->debug("Message signature will not be checked");
|
2016-09-25 18:44:48 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
# Get SAML response
|
|
|
|
my $saml_response = $login->response();
|
|
|
|
unless ($saml_response) {
|
2017-02-18 15:25:48 +01:00
|
|
|
$self->userLogger->error("No SAML response found");
|
2016-09-25 18:44:48 +02:00
|
|
|
return PE_SAML_SSO_ERROR;
|
|
|
|
}
|
|
|
|
|
|
|
|
# Check Destination
|
|
|
|
return PE_SAML_DESTINATION_ERROR
|
|
|
|
unless ( $self->checkDestination( $saml_response, $url ) );
|
|
|
|
|
|
|
|
# Replay protection if this is a response to a created authn request
|
|
|
|
my $assertion_responded = $saml_response->InResponseTo;
|
|
|
|
if ($assertion_responded) {
|
|
|
|
unless ( $self->replayProtection($assertion_responded) ) {
|
|
|
|
|
|
|
|
# Assertion was already consumed or is expired
|
|
|
|
# Force authentication replay
|
2017-02-15 07:41:50 +01:00
|
|
|
$self->userLogger->error(
|
|
|
|
"Message $assertion_responded already used or expired, replay authentication"
|
2016-09-25 18:44:48 +02:00
|
|
|
);
|
|
|
|
delete $req->{urldc};
|
|
|
|
$req->mustRedirect(1);
|
2016-09-26 21:12:40 +02:00
|
|
|
$req->steps( [] );
|
2018-10-28 13:26:50 +01:00
|
|
|
$req->continue(1);
|
2016-09-26 21:12:40 +02:00
|
|
|
return PE_OK;
|
2016-09-25 18:44:48 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
else {
|
2017-02-15 07:41:50 +01:00
|
|
|
$self->logger->debug(
|
|
|
|
"Assertion is not a response to a created authentication request, do not control replay"
|
2016-09-25 18:44:48 +02:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
# Get SAML assertion
|
|
|
|
my $assertion = $self->getAssertion($login);
|
|
|
|
|
|
|
|
unless ($assertion) {
|
2017-02-18 15:25:48 +01:00
|
|
|
$self->userLogger->error("No assertion found");
|
2016-09-25 18:44:48 +02:00
|
|
|
return PE_SAML_SSO_ERROR;
|
|
|
|
}
|
|
|
|
|
|
|
|
# Do we check conditions?
|
2017-02-15 07:41:50 +01:00
|
|
|
my $checkTime = $self->conf->{samlIDPMetaDataOptions}->{$idpConfKey}
|
2016-09-29 21:25:02 +02:00
|
|
|
->{samlIDPMetaDataOptionsCheckTime};
|
|
|
|
my $checkAudience =
|
|
|
|
$self->conf->{samlIDPMetaDataOptions}->{$idpConfKey}
|
|
|
|
->{samlIDPMetaDataOptionsCheckAudience};
|
2016-09-25 18:44:48 +02:00
|
|
|
|
|
|
|
# Check conditions - time and audience
|
2016-09-29 21:25:02 +02:00
|
|
|
unless (
|
|
|
|
$self->validateConditions(
|
|
|
|
$assertion, $self->getMetaDataURL( "samlEntityID", 0, 1 ),
|
|
|
|
$checkTime, $checkAudience
|
2016-09-25 18:44:48 +02:00
|
|
|
)
|
|
|
|
)
|
|
|
|
{
|
2017-02-18 15:25:48 +01:00
|
|
|
$self->userLogger->error("Conditions not validated");
|
2016-09-25 18:44:48 +02:00
|
|
|
return PE_SAML_CONDITIONS_ERROR;
|
|
|
|
}
|
|
|
|
|
2016-10-04 11:18:27 +02:00
|
|
|
my $relayStateURL =
|
|
|
|
$self->conf->{samlIDPMetaDataOptions}->{$idpConfKey}
|
|
|
|
->{samlIDPMetaDataOptionsRelayStateURL};
|
|
|
|
|
2016-09-25 18:44:48 +02:00
|
|
|
# Extract RelayState information
|
2017-03-20 13:43:57 +01:00
|
|
|
if ( $self->extractRelayState( $req, $relaystate, $relayStateURL ) )
|
|
|
|
{
|
2017-02-15 07:41:50 +01:00
|
|
|
$self->logger->debug("RelayState $relaystate extracted");
|
2016-09-25 18:44:48 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
# Check if we accept direct login from IDP
|
|
|
|
my $allowLoginFromIDP =
|
|
|
|
$self->conf->{samlIDPMetaDataOptions}->{$idpConfKey}
|
|
|
|
->{samlIDPMetaDataOptionsAllowLoginFromIDP};
|
|
|
|
if ( !$assertion_responded and !$allowLoginFromIDP ) {
|
2017-02-15 07:41:50 +01:00
|
|
|
$self->userLogger->error(
|
|
|
|
"Direct login from IDP $idpConfKey is not allowed");
|
2016-09-25 18:44:48 +02:00
|
|
|
return PE_SAML_IDPSSOINITIATED_NOTALLOWED;
|
|
|
|
}
|
|
|
|
|
|
|
|
# Check authentication context
|
|
|
|
my $responseAuthnContext;
|
|
|
|
eval {
|
|
|
|
$responseAuthnContext =
|
|
|
|
$assertion->AuthnStatement()->AuthnContext()
|
|
|
|
->AuthnContextClassRef();
|
|
|
|
};
|
|
|
|
if ($@) {
|
2017-02-15 07:41:50 +01:00
|
|
|
$self->logger->debug(
|
|
|
|
"Unable to get authentication context from $idpConfKey");
|
2016-09-25 18:44:48 +02:00
|
|
|
$responseAuthnContext = $self->getAuthnContext("unspecified");
|
|
|
|
}
|
|
|
|
else {
|
2017-02-15 07:41:50 +01:00
|
|
|
$self->logger->debug(
|
|
|
|
"Found authentication context: $responseAuthnContext");
|
2016-09-25 18:44:48 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
# Map authentication context to authentication level
|
|
|
|
$req->sessionInfo->{authenticationLevel} =
|
|
|
|
$self->authnContext2authnLevel($responseAuthnContext);
|
|
|
|
|
|
|
|
# Force redirection to portal if no urldc found
|
|
|
|
# (avoid displaying the whole SAML URL in user browser URL field)
|
|
|
|
$req->mustRedirect(1) unless ( $req->urldc );
|
|
|
|
|
|
|
|
# Get SessionIndex
|
|
|
|
my $session_index;
|
|
|
|
|
|
|
|
eval {
|
|
|
|
$session_index = $assertion->AuthnStatement()->SessionIndex();
|
|
|
|
};
|
|
|
|
if ( $@ or !defined($session_index) ) {
|
2017-02-15 07:41:50 +01:00
|
|
|
$self->logger->debug("No SessionIndex found");
|
2016-09-25 18:44:48 +02:00
|
|
|
}
|
|
|
|
else {
|
2017-02-15 07:41:50 +01:00
|
|
|
$self->logger->debug("Found SessionIndex $session_index");
|
2016-09-25 18:44:48 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
# Get NameID
|
2018-10-02 16:04:24 +02:00
|
|
|
my $nameid = $login->nameIdentifier;
|
|
|
|
my $nameid_content = $nameid->content;
|
2016-09-25 18:44:48 +02:00
|
|
|
|
2018-10-02 16:04:24 +02:00
|
|
|
unless ($nameid_content) {
|
2017-02-18 15:25:48 +01:00
|
|
|
$self->userLogger->error("No NameID value found");
|
2016-09-25 18:44:48 +02:00
|
|
|
return PE_SAML_SSO_ERROR;
|
|
|
|
}
|
|
|
|
|
2018-10-02 16:04:24 +02:00
|
|
|
$self->logger->debug("Found NameID content $nameid_content");
|
|
|
|
|
|
|
|
# Set user
|
|
|
|
my $user = $nameid_content;
|
|
|
|
my $userAttribute =
|
|
|
|
$self->conf->{samlIDPMetaDataOptions}->{$idpConfKey}
|
|
|
|
->{samlIDPMetaDataOptionsUserAttribute};
|
|
|
|
|
|
|
|
if ($userAttribute) {
|
|
|
|
$self->logger->debug(
|
|
|
|
"Try to set user value from SAML attribute $userAttribute");
|
|
|
|
|
|
|
|
my $force_utf8 =
|
|
|
|
$self->conf->{samlIDPMetaDataOptions}->{$idpConfKey}
|
|
|
|
->{samlIDPMetaDataOptionsForceUTF8};
|
|
|
|
|
|
|
|
my $attr_statement = $assertion->AttributeStatement();
|
|
|
|
if ($attr_statement) {
|
|
|
|
|
|
|
|
# Get attributes
|
|
|
|
my @attributes = $attr_statement->Attribute();
|
|
|
|
#
|
|
|
|
# Try to get value
|
|
|
|
my $value =
|
|
|
|
$self->getAttributeValue( $userAttribute, undef, undef,
|
|
|
|
\@attributes, $force_utf8 );
|
|
|
|
|
|
|
|
# Store value as user
|
|
|
|
if ( defined $value ) {
|
|
|
|
$user = $value;
|
|
|
|
$self->logger->debug(
|
|
|
|
"Found value $value for attribute $userAttribute");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
$self->logger->debug(
|
|
|
|
"No attributes found in SAML assertion");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-09-25 18:44:48 +02:00
|
|
|
$req->user($user);
|
2018-10-02 16:04:24 +02:00
|
|
|
$self->logger->debug("Set user value to $user");
|
2016-09-25 18:44:48 +02:00
|
|
|
|
|
|
|
# Store Lasso objects
|
2018-07-05 22:56:16 +02:00
|
|
|
$req->data->{_lassoLogin} = $login;
|
|
|
|
$req->data->{_idp} = $idp;
|
|
|
|
$req->data->{_idpConfKey} = $idpConfKey;
|
|
|
|
$req->data->{_nameID} = $nameid;
|
|
|
|
$req->data->{_sessionIndex} = $session_index;
|
2016-09-25 18:44:48 +02:00
|
|
|
|
|
|
|
# Store Token
|
|
|
|
my $saml_token = $assertion->export_to_xml;
|
|
|
|
|
2017-02-15 07:41:50 +01:00
|
|
|
$self->logger->debug("SAML Token: $saml_token");
|
2016-09-25 18:44:48 +02:00
|
|
|
|
2018-07-05 22:56:16 +02:00
|
|
|
$req->data->{_samlToken} = $saml_token;
|
2016-09-25 18:44:48 +02:00
|
|
|
|
|
|
|
# Restore initial SAML request in case of proxying
|
2017-02-15 09:36:24 +01:00
|
|
|
if ($assertion_responded) {
|
2016-09-25 18:44:48 +02:00
|
|
|
|
2017-02-15 09:36:24 +01:00
|
|
|
my $saml_sessions =
|
2017-09-27 07:00:00 +02:00
|
|
|
Lemonldap::NG::Common::Apache::Session->searchOn(
|
|
|
|
$self->amOpts, "ProxyID", $assertion_responded );
|
2016-09-25 18:44:48 +02:00
|
|
|
|
2018-06-29 06:50:31 +02:00
|
|
|
if (
|
|
|
|
my @saml_sessions_keys = grep {
|
|
|
|
$saml_sessions->{$_}->{_session_kind} eq
|
|
|
|
$self->sessionKind
|
|
|
|
} keys %$saml_sessions
|
|
|
|
)
|
|
|
|
{
|
2016-09-25 18:44:48 +02:00
|
|
|
|
2017-02-15 09:36:24 +01:00
|
|
|
# Warning if more than one session found
|
|
|
|
if ( $#saml_sessions_keys > 0 ) {
|
|
|
|
$self->logger->warn(
|
2017-02-15 07:41:50 +01:00
|
|
|
"More than one SAML proxy session found for ID $assertion_responded"
|
2017-02-15 09:36:24 +01:00
|
|
|
);
|
|
|
|
}
|
2016-09-25 18:44:48 +02:00
|
|
|
|
2017-02-15 09:36:24 +01:00
|
|
|
# Take the first session
|
|
|
|
my $saml_session = shift @saml_sessions_keys;
|
2016-09-25 18:44:48 +02:00
|
|
|
|
2017-02-15 09:36:24 +01:00
|
|
|
# Get session
|
|
|
|
$self->logger->debug(
|
2017-02-15 07:41:50 +01:00
|
|
|
"Retrieve SAML proxy session $saml_session for ID $assertion_responded"
|
2017-02-15 09:36:24 +01:00
|
|
|
);
|
2016-09-25 18:44:48 +02:00
|
|
|
|
2017-02-15 09:36:24 +01:00
|
|
|
my $samlSessionInfo = $self->getSamlSession($saml_session);
|
2016-09-25 18:44:48 +02:00
|
|
|
|
2018-07-05 22:56:16 +02:00
|
|
|
$req->data->{_proxiedRequest} =
|
2017-02-15 09:36:24 +01:00
|
|
|
$samlSessionInfo->data->{Request};
|
2018-07-05 22:56:16 +02:00
|
|
|
$req->data->{_proxiedMethod} =
|
2017-02-15 09:36:24 +01:00
|
|
|
$samlSessionInfo->data->{Method};
|
2018-07-05 22:56:16 +02:00
|
|
|
$req->data->{_proxiedRelayState} =
|
2017-02-15 09:36:24 +01:00
|
|
|
$samlSessionInfo->data->{RelayState};
|
2018-07-05 22:56:16 +02:00
|
|
|
$req->data->{_proxiedArtifact} =
|
2017-02-15 09:36:24 +01:00
|
|
|
$samlSessionInfo->data->{Artifact};
|
2016-09-25 18:44:48 +02:00
|
|
|
|
2017-03-20 13:43:57 +01:00
|
|
|
# Save values in hidden fields in case of other user interactions
|
|
|
|
$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} );
|
|
|
|
|
2017-02-15 09:36:24 +01:00
|
|
|
# Delete session
|
|
|
|
$samlSessionInfo->remove();
|
|
|
|
}
|
2016-09-25 18:44:48 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
return PE_OK;
|
|
|
|
}
|
|
|
|
elsif ($request) {
|
|
|
|
|
|
|
|
# Do nothing
|
2017-02-15 07:41:50 +01:00
|
|
|
$self->logger->debug(
|
|
|
|
"This module do not manage SSO request, see IssuerDBSAML");
|
2016-09-25 18:44:48 +02:00
|
|
|
|
|
|
|
return PE_OK;
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
|
|
|
|
# This should not happen
|
2017-02-18 15:25:48 +01:00
|
|
|
$self->userLogger->error("SSO request or response was not found");
|
2016-09-25 18:44:48 +02:00
|
|
|
|
2016-11-29 22:10:00 +01:00
|
|
|
return PE_SAML_ERROR;
|
2016-09-25 18:44:48 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
# 1.2 SLO
|
2016-11-16 16:27:01 +01:00
|
|
|
elsif ( $url =~ $self->sloRe ) {
|
2017-02-15 07:41:50 +01:00
|
|
|
$self->logger->debug("URL $url detected as an SLO URL");
|
2016-09-25 18:44:48 +02:00
|
|
|
|
2016-12-23 17:03:36 +01:00
|
|
|
# TODO: call authLogout instead of duplicating SLO
|
|
|
|
$req->steps( [ @{ $self->p->beforeLogout }, 'deleteSession' ] );
|
|
|
|
|
2016-09-25 18:44:48 +02:00
|
|
|
# Check SAML Message
|
|
|
|
my ( $request, $response, $method, $relaystate, $artifact ) =
|
2016-11-28 22:15:57 +01:00
|
|
|
$self->checkMessage( $req, $url, $request_method, $content_type,
|
|
|
|
"logout" );
|
2016-09-25 18:44:48 +02:00
|
|
|
|
|
|
|
# Create Logout object
|
|
|
|
my $logout = $self->createLogout( $self->lassoServer );
|
|
|
|
|
|
|
|
# Ignore signature verification
|
|
|
|
$self->disableSignatureVerification($logout);
|
|
|
|
|
|
|
|
if ($response) {
|
|
|
|
|
|
|
|
# Process logout response
|
|
|
|
my $result = $self->processLogoutResponseMsg( $logout, $response );
|
|
|
|
|
|
|
|
unless ($result) {
|
2017-02-18 15:25:48 +01:00
|
|
|
$self->userLogger->error("Fail to process logout response");
|
2016-09-25 18:44:48 +02:00
|
|
|
return PE_SAML_SLO_ERROR;
|
|
|
|
}
|
|
|
|
|
2017-02-15 07:41:50 +01:00
|
|
|
$self->logger->debug("Logout response is valid");
|
2016-09-25 18:44:48 +02:00
|
|
|
|
|
|
|
# Check Destination
|
|
|
|
return PE_SAML_DESTINATION_ERROR
|
|
|
|
unless ( $self->checkDestination( $logout->response, $url ) );
|
|
|
|
|
|
|
|
# Get IDP entityID
|
|
|
|
my $idp = $logout->remote_providerID();
|
|
|
|
|
2017-02-15 07:41:50 +01:00
|
|
|
$self->logger->debug("Found entityID $idp in SAML message");
|
2016-09-25 18:44:48 +02:00
|
|
|
|
|
|
|
# IDP conf key
|
|
|
|
my $idpConfKey = $self->idpList->{$idp}->{confKey};
|
|
|
|
|
|
|
|
unless ($idpConfKey) {
|
2017-02-15 07:41:50 +01:00
|
|
|
$self->userLogger->error(
|
|
|
|
"$idp do not match any IDP in configuration");
|
2016-09-25 18:44:48 +02:00
|
|
|
return PE_SAML_UNKNOWN_ENTITY;
|
|
|
|
}
|
|
|
|
|
2017-02-15 07:41:50 +01:00
|
|
|
$self->logger->debug("$idp match $idpConfKey IDP in configuration");
|
2016-09-25 18:44:48 +02:00
|
|
|
|
|
|
|
# Do we check signature?
|
|
|
|
my $checkSLOMessageSignature =
|
|
|
|
$self->conf->{samlIDPMetaDataOptions}->{$idpConfKey}
|
|
|
|
->{samlIDPMetaDataOptionsCheckSLOMessageSignature};
|
|
|
|
|
|
|
|
if ($checkSLOMessageSignature) {
|
|
|
|
|
|
|
|
$self->forceSignatureVerification($logout);
|
|
|
|
|
|
|
|
$result = $self->processLogoutResponseMsg( $logout, $response );
|
|
|
|
|
|
|
|
unless ($result) {
|
2017-02-18 15:25:48 +01:00
|
|
|
$self->userLogger->error("Signature is not valid");
|
2016-09-25 18:44:48 +02:00
|
|
|
return PE_SAML_SIGNATURE_ERROR;
|
|
|
|
}
|
|
|
|
else {
|
2017-02-15 07:41:50 +01:00
|
|
|
$self->logger->debug("Signature is valid");
|
2016-09-25 18:44:48 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
else {
|
2017-02-15 07:41:50 +01:00
|
|
|
$self->logger->debug("Message signature will not be checked");
|
2016-09-25 18:44:48 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
# Replay protection
|
|
|
|
my $samlID = $logout->response()->InResponseTo;
|
|
|
|
|
|
|
|
unless ( $self->replayProtection($samlID) ) {
|
|
|
|
|
|
|
|
# Logout request was already consumed or is expired
|
2017-02-15 07:41:50 +01:00
|
|
|
$self->userLogger->error(
|
|
|
|
"Message $samlID already used or expired");
|
2016-09-25 18:44:48 +02:00
|
|
|
return PE_SAML_SLO_ERROR;
|
|
|
|
}
|
|
|
|
|
|
|
|
# If URL in RelayState, different from portal, redirect user
|
2017-03-20 13:43:57 +01:00
|
|
|
if ( $self->extractRelayState( $req, $relaystate ) ) {
|
2017-02-15 07:41:50 +01:00
|
|
|
$self->logger->debug("RelayState $relaystate extracted");
|
|
|
|
$self->logger->debug(
|
|
|
|
"URL " . $req->urldc . " found in RelayState" );
|
2016-09-25 18:44:48 +02:00
|
|
|
}
|
|
|
|
|
2016-09-26 21:12:40 +02:00
|
|
|
if ( $req->urldc
|
2016-11-29 06:43:46 +01:00
|
|
|
and $self->conf->{portal} !~ /$req->{urldc}\/?/ )
|
2016-09-26 21:12:40 +02:00
|
|
|
{
|
|
|
|
$req->steps( [] );
|
2016-12-23 17:03:36 +01:00
|
|
|
$req->user('TODO');
|
2016-09-26 21:12:40 +02:00
|
|
|
return PE_OK;
|
|
|
|
}
|
2016-09-25 18:44:48 +02:00
|
|
|
|
|
|
|
# Else, inform user that logout is OK
|
2016-12-23 17:03:36 +01:00
|
|
|
$req->user('TODO');
|
|
|
|
return PE_OK;
|
2016-09-25 18:44:48 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
elsif ($request) {
|
|
|
|
|
|
|
|
# Logout error
|
|
|
|
my $logout_error = 0;
|
|
|
|
|
|
|
|
# Lasso::Session dump
|
|
|
|
my $session_dump;
|
|
|
|
|
|
|
|
# Process logout request
|
|
|
|
unless ( $self->processLogoutRequestMsg( $logout, $request ) ) {
|
2017-02-18 15:25:48 +01:00
|
|
|
$self->userLogger->error("Fail to process logout request");
|
2016-09-25 18:44:48 +02:00
|
|
|
$logout_error = 1;
|
|
|
|
}
|
|
|
|
|
2017-02-15 07:41:50 +01:00
|
|
|
$self->logger->debug("Logout request is valid");
|
2016-09-25 18:44:48 +02:00
|
|
|
|
|
|
|
# Check Destination
|
|
|
|
return PE_SAML_DESTINATION_ERROR
|
|
|
|
unless ( $self->checkDestination( $logout->request, $url ) );
|
|
|
|
|
|
|
|
# Get IDP entityID
|
|
|
|
my $idp = $logout->remote_providerID();
|
|
|
|
|
2017-02-15 07:41:50 +01:00
|
|
|
$self->logger->debug("Found entityID $idp in SAML message");
|
2016-09-25 18:44:48 +02:00
|
|
|
|
|
|
|
# IDP conf key
|
|
|
|
my $idpConfKey = $self->idpList->{$idp}->{confKey};
|
|
|
|
|
|
|
|
unless ($idpConfKey) {
|
2017-02-15 07:41:50 +01:00
|
|
|
$self->userLogger->error(
|
|
|
|
"$idp do not match any IDP in configuration");
|
2016-09-25 18:44:48 +02:00
|
|
|
return PE_SAML_UNKNOWN_ENTITY;
|
|
|
|
}
|
|
|
|
|
2017-02-15 07:41:50 +01:00
|
|
|
$self->logger->debug("$idp match $idpConfKey IDP in configuration");
|
2016-09-25 18:44:48 +02:00
|
|
|
|
|
|
|
# Do we check signature?
|
|
|
|
my $checkSLOMessageSignature =
|
|
|
|
$self->conf->{samlIDPMetaDataOptions}->{$idpConfKey}
|
|
|
|
->{samlIDPMetaDataOptionsCheckSLOMessageSignature};
|
|
|
|
|
|
|
|
if ($checkSLOMessageSignature) {
|
|
|
|
unless ( $self->checkSignatureStatus($logout) ) {
|
2017-02-18 15:25:48 +01:00
|
|
|
$self->userLogger->error("Signature is not valid");
|
2016-09-25 18:44:48 +02:00
|
|
|
return PE_SAML_SIGNATURE_ERROR;
|
|
|
|
}
|
|
|
|
else {
|
2017-02-15 07:41:50 +01:00
|
|
|
$self->logger->debug("Signature is valid");
|
2016-09-25 18:44:48 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
else {
|
2017-02-15 07:41:50 +01:00
|
|
|
$self->logger->debug("Message signature will not be checked");
|
2016-09-25 18:44:48 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
# Get NameID and SessionIndex
|
|
|
|
my $name_id = $logout->request()->NameID;
|
|
|
|
my $session_index = $logout->request()->SessionIndex;
|
|
|
|
my $user = $name_id->content;
|
|
|
|
|
|
|
|
unless ($name_id) {
|
2017-02-15 07:41:50 +01:00
|
|
|
$self->userLogger->error(
|
|
|
|
"Fail to get NameID from logout request");
|
2016-09-25 18:44:48 +02:00
|
|
|
$logout_error = 1;
|
|
|
|
}
|
|
|
|
|
2017-02-15 07:41:50 +01:00
|
|
|
$self->logger->debug("Logout request NameID content: $user");
|
2016-09-25 18:44:48 +02:00
|
|
|
|
|
|
|
# Get SAML sessions with the same NameID
|
|
|
|
|
|
|
|
my $local_sessions =
|
2017-09-27 07:00:00 +02:00
|
|
|
Lemonldap::NG::Common::Apache::Session->searchOn( $self->amOpts,
|
|
|
|
"_nameID", $name_id->dump );
|
2016-09-25 18:44:48 +02:00
|
|
|
|
2018-06-29 06:50:31 +02:00
|
|
|
if (
|
|
|
|
my @local_sessions_keys = grep {
|
|
|
|
$local_sessions->{$_}->{_session_kind} eq $self->sessionKind
|
|
|
|
} keys %$local_sessions
|
|
|
|
)
|
|
|
|
{
|
2016-09-25 18:44:48 +02:00
|
|
|
|
|
|
|
# At least one session was found
|
|
|
|
foreach (@local_sessions_keys) {
|
|
|
|
|
|
|
|
my $local_session = $_;
|
|
|
|
|
|
|
|
# Get session
|
2017-02-15 07:41:50 +01:00
|
|
|
$self->logger->debug(
|
|
|
|
"Retrieve SAML session $local_session for user $user");
|
2016-09-25 18:44:48 +02:00
|
|
|
|
|
|
|
my $sessionInfo = $self->getSamlSession($local_session);
|
|
|
|
|
|
|
|
# If session index is defined and not equal to SAML session index,
|
|
|
|
# jump to next session
|
|
|
|
if ( defined $session_index
|
|
|
|
and $session_index ne
|
|
|
|
$sessionInfo->data->{_sessionIndex} )
|
|
|
|
{
|
2017-02-15 07:41:50 +01:00
|
|
|
$self->logger->debug(
|
|
|
|
"Session $local_session has not the good session index, skipping"
|
2016-09-25 18:44:48 +02:00
|
|
|
);
|
|
|
|
next;
|
|
|
|
}
|
|
|
|
|
|
|
|
# Delete session
|
|
|
|
else {
|
|
|
|
|
|
|
|
# Open real session
|
|
|
|
my $real_session = $sessionInfo->data->{_saml_id};
|
|
|
|
|
|
|
|
my $ssoSession =
|
2017-01-24 06:10:57 +01:00
|
|
|
$self->p->getApacheSession($real_session);
|
2016-09-25 18:44:48 +02:00
|
|
|
|
2016-12-23 17:03:36 +01:00
|
|
|
# Import SSO session in $req
|
|
|
|
$self->importRealSession( $req, $ssoSession );
|
|
|
|
|
2016-09-25 18:44:48 +02:00
|
|
|
# Get Lasso::Session dump
|
|
|
|
# This value is erased if a next session match the SLO request
|
|
|
|
if ( $ssoSession
|
2018-06-29 06:50:31 +02:00
|
|
|
&& $ssoSession->data->{ $self->lsDump } )
|
2016-09-25 18:44:48 +02:00
|
|
|
{
|
2017-02-15 07:41:50 +01:00
|
|
|
$self->logger->debug(
|
|
|
|
"Get Lasso::Session dump from session $real_session"
|
2016-09-25 18:44:48 +02:00
|
|
|
);
|
|
|
|
$session_dump =
|
2018-06-29 06:50:31 +02:00
|
|
|
$ssoSession->data->{ $self->lsDump };
|
2016-09-25 18:44:48 +02:00
|
|
|
}
|
|
|
|
|
2016-12-23 17:03:36 +01:00
|
|
|
# Real session will be deleted after (see $req->steps... before)
|
2016-09-25 18:44:48 +02:00
|
|
|
|
|
|
|
# Delete SAML session
|
|
|
|
my $del_saml_result = $sessionInfo->remove();
|
|
|
|
|
2017-02-15 07:41:50 +01:00
|
|
|
$self->logger->debug(
|
|
|
|
"Delete SAML session $local_session result: $del_saml_result"
|
2016-09-25 18:44:48 +02:00
|
|
|
);
|
|
|
|
|
|
|
|
$logout_error = 1 unless $del_saml_result;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
# Set session from dump
|
|
|
|
unless ( $self->setSessionFromDump( $logout, $session_dump ) ) {
|
2017-02-15 07:41:50 +01:00
|
|
|
$self->userLogger->error(
|
|
|
|
"Cannot set session from dump in logout");
|
2016-09-25 18:44:48 +02:00
|
|
|
$logout_error = 1;
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
|
|
|
|
# No corresponding session found
|
2017-02-15 07:41:50 +01:00
|
|
|
$self->logger->debug("No SAML session found for user $user");
|
2016-09-25 18:44:48 +02:00
|
|
|
|
|
|
|
$logout_error = 1;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
# Validate request if no previous error
|
|
|
|
unless ($logout_error) {
|
|
|
|
unless ( $self->validateLogoutRequest($logout) ) {
|
2017-02-18 15:25:48 +01:00
|
|
|
$self->userLogger->error("SLO request is not valid");
|
2016-09-25 18:44:48 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
# Set RelayState
|
|
|
|
if ($relaystate) {
|
|
|
|
$logout->msg_relayState($relaystate);
|
2017-02-15 07:41:50 +01:00
|
|
|
$self->logger->debug("Set $relaystate in RelayState");
|
2016-09-25 18:44:48 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
# Do we set signature?
|
|
|
|
my $signSLOMessage =
|
|
|
|
$self->conf->{samlIDPMetaDataOptions}->{$idpConfKey}
|
|
|
|
->{samlIDPMetaDataOptionsSignSLOMessage};
|
|
|
|
|
|
|
|
if ( $signSLOMessage == 0 ) {
|
2017-02-15 07:41:50 +01:00
|
|
|
$self->logger->debug(
|
|
|
|
"SLO message to IDP $idpConfKey will not be signed");
|
2016-09-25 18:44:48 +02:00
|
|
|
$self->disableSignature($logout);
|
|
|
|
}
|
|
|
|
elsif ( $signSLOMessage == 1 ) {
|
2017-02-15 07:41:50 +01:00
|
|
|
$self->logger->debug(
|
|
|
|
"SLO message to IDP $idpConfKey will be signed");
|
2016-09-25 18:44:48 +02:00
|
|
|
$self->forceSignature($logout);
|
|
|
|
}
|
|
|
|
else {
|
2017-02-15 07:41:50 +01:00
|
|
|
$self->logger->debug(
|
|
|
|
"SLO message to IDP $idpConfKey signature according to metadata"
|
2016-09-25 18:44:48 +02:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
# Logout response
|
|
|
|
unless ( $self->buildLogoutResponseMsg($logout) ) {
|
2017-02-15 07:41:50 +01:00
|
|
|
$self->logger->error("Unable to build SLO response");
|
2016-09-25 18:44:48 +02:00
|
|
|
return PE_SAML_SLO_ERROR;
|
|
|
|
}
|
|
|
|
|
|
|
|
# Send response depending on request method
|
|
|
|
# HTTP-REDIRECT
|
|
|
|
if ( $method == Lasso::Constants::HTTP_METHOD_REDIRECT ) {
|
|
|
|
|
|
|
|
# Redirect user to response URL
|
|
|
|
my $slo_url = $logout->msg_url;
|
2017-02-15 07:41:50 +01:00
|
|
|
$self->logger->debug("Redirect user to $slo_url");
|
2016-09-25 18:44:48 +02:00
|
|
|
|
|
|
|
$req->urldc($slo_url);
|
|
|
|
|
2016-12-25 16:41:23 +01:00
|
|
|
return PE_OK;
|
2016-09-25 18:44:48 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
# HTTP-POST
|
|
|
|
elsif ( $method == Lasso::Constants::HTTP_METHOD_POST ) {
|
|
|
|
|
|
|
|
# Use autosubmit form
|
|
|
|
my $slo_url = $logout->msg_url;
|
|
|
|
my $slo_body = $logout->msg_body;
|
|
|
|
|
|
|
|
$req->postUrl($slo_url);
|
2016-12-25 16:41:23 +01:00
|
|
|
$req->postFields( { 'SAMLResponse' => $slo_body } );
|
2016-09-25 18:44:48 +02:00
|
|
|
|
|
|
|
# RelayState
|
|
|
|
$req->postFields->{'RelayState'} = $relaystate
|
|
|
|
if ($relaystate);
|
|
|
|
|
2016-09-26 21:12:40 +02:00
|
|
|
# TODO: verify this
|
2016-12-25 16:41:23 +01:00
|
|
|
push @{ $req->steps }, 'autoPost';
|
2016-09-26 21:12:40 +02:00
|
|
|
return PE_OK;
|
2016-09-25 18:44:48 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
# HTTP-SOAP
|
|
|
|
elsif ( $method == Lasso::Constants::HTTP_METHOD_SOAP ) {
|
|
|
|
|
|
|
|
my $slo_body = $logout->msg_body;
|
|
|
|
|
2017-02-15 07:41:50 +01:00
|
|
|
$self->logger->debug("SOAP response $slo_body");
|
2016-09-25 18:44:48 +02:00
|
|
|
|
2019-02-07 09:27:56 +01:00
|
|
|
$req->response( [
|
2016-12-18 09:07:48 +01:00
|
|
|
200,
|
|
|
|
[
|
|
|
|
'Content-Type' => 'application/xml',
|
|
|
|
'Content-Length' => length($slo_body)
|
|
|
|
],
|
|
|
|
[$slo_body]
|
|
|
|
]
|
|
|
|
);
|
2019-02-07 09:27:56 +01:00
|
|
|
$req->steps( [
|
2016-12-23 17:03:36 +01:00
|
|
|
@{ $self->p->beforeLogout },
|
|
|
|
sub {
|
|
|
|
my ($req) = @_;
|
|
|
|
my $res = $self->p->deleteSession($req);
|
|
|
|
return (
|
|
|
|
$res eq PE_LOGOUT_OK ? PE_SENDRESPONSE : $res );
|
2018-11-26 14:40:21 +01:00
|
|
|
}
|
2016-12-23 17:03:36 +01:00
|
|
|
]
|
|
|
|
);
|
2016-09-25 18:44:48 +02:00
|
|
|
|
2016-12-23 17:03:36 +01:00
|
|
|
$req->user('SOAP client');
|
|
|
|
return PE_OK;
|
2016-09-25 18:44:48 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
|
|
|
|
# This should not happen
|
2017-02-18 15:25:48 +01:00
|
|
|
$self->userLogger->error("SLO request or response was not found");
|
2016-09-25 18:44:48 +02:00
|
|
|
|
|
|
|
# Redirect user
|
|
|
|
$req->mustRedirect(1);
|
2016-09-26 21:12:40 +02:00
|
|
|
$req->steps( [] );
|
2016-12-04 22:57:25 +01:00
|
|
|
return PE_REDIRECT;
|
2016-09-25 18:44:48 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
# 1.3 Artifact
|
2016-11-16 16:27:01 +01:00
|
|
|
elsif ( $url =~ $self->artRe ) {
|
2016-09-25 18:44:48 +02:00
|
|
|
|
2017-02-15 07:41:50 +01:00
|
|
|
$self->logger->debug(
|
|
|
|
"URL $url detected as an artifact resolution service URL");
|
2016-09-25 18:44:48 +02:00
|
|
|
|
|
|
|
# Artifact request are sent with SOAP trough POST
|
2017-01-07 08:45:36 +01:00
|
|
|
my $art_request = $req->content;
|
2016-09-25 18:44:48 +02:00
|
|
|
my $art_response;
|
|
|
|
|
|
|
|
# Create Login object
|
|
|
|
my $login = $self->createLogin( $self->lassoServer );
|
|
|
|
|
|
|
|
# Process request message
|
|
|
|
unless ( $self->processArtRequestMsg( $login, $art_request ) ) {
|
2017-02-15 07:41:50 +01:00
|
|
|
$self->userLogger->error(
|
|
|
|
"Unable to process artifact request message");
|
2016-09-25 18:44:48 +02:00
|
|
|
return PE_SAML_ART_ERROR;
|
|
|
|
}
|
|
|
|
|
|
|
|
# Check Destination
|
|
|
|
return PE_SAML_DESTINATION_ERROR
|
|
|
|
unless ( $self->checkDestination( $login->request, $url ) );
|
|
|
|
|
|
|
|
# Create artifact response
|
2016-12-15 06:55:09 +01:00
|
|
|
unless ( $art_response = $self->createArtifactResponse( $req, $login ) )
|
|
|
|
{
|
2017-02-15 07:41:50 +01:00
|
|
|
$self->logger("Unable to create artifact response message");
|
2016-09-25 18:44:48 +02:00
|
|
|
return PE_SAML_ART_ERROR;
|
|
|
|
}
|
|
|
|
|
2019-02-07 09:27:56 +01:00
|
|
|
$req->response( [
|
2016-12-24 09:47:39 +01:00
|
|
|
200,
|
|
|
|
[
|
|
|
|
'Content-Type' => 'application/xml',
|
|
|
|
'Content-Length' => length($art_response)
|
|
|
|
],
|
|
|
|
[$art_response]
|
|
|
|
]
|
|
|
|
);
|
|
|
|
$req->user('SOAP client');
|
|
|
|
return PE_SENDRESPONSE;
|
2016-09-25 18:44:48 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
# 2. IDP resolution
|
|
|
|
|
|
|
|
# Search a selected IdP
|
2016-09-26 21:12:40 +02:00
|
|
|
my ( $idp, $idp_cookie ) = $self->getIDP($req);
|
2016-09-25 18:44:48 +02:00
|
|
|
|
2018-07-18 10:31:54 +02:00
|
|
|
# Use Common Domain Cookie
|
|
|
|
if ( !$idp
|
|
|
|
and $self->conf->{samlCommonDomainCookieActivation}
|
|
|
|
and $self->conf->{samlCommonDomainCookieReader} )
|
|
|
|
{
|
|
|
|
$self->logger->debug(
|
|
|
|
"Will try to use Common Domain Cookie for IDP resolution");
|
|
|
|
|
|
|
|
# Add current URL to CDC Reader URL
|
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
2018-09-04 14:39:19 +02:00
|
|
|
# Use SAML Discovery Protocol
|
|
|
|
if ( !$idp
|
|
|
|
and $self->conf->{samlDiscoveryProtocolActivation}
|
|
|
|
and defined $self->conf->{samlDiscoveryProtocolURL} )
|
|
|
|
{
|
|
|
|
$self->logger->debug(
|
|
|
|
"Will try to use SAML Discovery Protocol for IDP resolution");
|
|
|
|
|
2019-04-10 09:21:55 +02:00
|
|
|
if ( $req->urldc ) {
|
|
|
|
$req->pdata->{_url} = encode_base64( $req->urldc, '' );
|
2019-02-28 17:51:50 +01:00
|
|
|
}
|
2018-09-04 14:39:19 +02:00
|
|
|
my $disco_url = $self->conf->{samlDiscoveryProtocolURL};
|
2019-04-10 17:21:33 +02:00
|
|
|
my $portal = $self->conf->{portal};
|
2018-09-04 14:39:19 +02:00
|
|
|
$disco_url .= ( $disco_url =~ /\?/ ? '&' : '?' )
|
|
|
|
. build_urlencoded(
|
|
|
|
entityID => $self->getMetaDataURL( 'samlEntityID', 0, 1 ),
|
2018-10-19 11:29:11 +02:00
|
|
|
return => $portal,
|
2018-09-04 14:39:19 +02:00
|
|
|
returnIDParam => 'idp'
|
|
|
|
);
|
|
|
|
|
|
|
|
if ( defined $self->conf->{samlDiscoveryProtocolPolicy} ) {
|
2018-10-19 11:29:11 +02:00
|
|
|
my $dppolicy = $self->conf->{samlDiscoveryProtocolPolicy};
|
|
|
|
$disco_url .= "&" . build_urlencoded( policy => $dppolicy );
|
2018-09-04 14:39:19 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
if ( defined $self->conf->{samlDiscoveryProtocolIsPassive} ) {
|
2018-10-19 11:29:11 +02:00
|
|
|
my $dpispassive = $self->conf->{samlDiscoveryProtocolIsPassive};
|
2018-09-04 14:39:19 +02:00
|
|
|
$disco_url .= "&"
|
|
|
|
. build_urlencoded(
|
2018-10-19 11:29:11 +02:00
|
|
|
isPassive => $dpispassive
|
2018-09-04 14:39:19 +02:00
|
|
|
? "true"
|
2018-09-11 10:03:09 +02:00
|
|
|
: "false"
|
|
|
|
);
|
2018-09-04 14:39:19 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
$self->logger->debug("Redirect user to $disco_url");
|
|
|
|
|
|
|
|
$req->urldc($disco_url);
|
|
|
|
|
|
|
|
$req->steps( [] );
|
|
|
|
return PE_REDIRECT;
|
|
|
|
}
|
|
|
|
|
2018-07-18 08:02:48 +02:00
|
|
|
# If IDP was not resolved, let the user choose its IDP
|
|
|
|
unless ($idp) {
|
2017-02-15 07:41:50 +01:00
|
|
|
$self->logger->debug("Redirecting user to IDP list");
|
2016-09-25 18:44:48 +02:00
|
|
|
|
|
|
|
# Control url parameter
|
2016-11-16 16:27:01 +01:00
|
|
|
my $urlcheck = $self->p->controlUrl($req);
|
2016-09-25 18:44:48 +02:00
|
|
|
return $urlcheck unless ( $urlcheck == PE_OK );
|
|
|
|
|
|
|
|
# IDP list
|
2019-02-05 23:12:17 +01:00
|
|
|
my @list = ();
|
2019-02-05 22:39:09 +01:00
|
|
|
my $portalPath = $self->conf->{portal};
|
|
|
|
|
2016-09-25 18:44:48 +02:00
|
|
|
foreach ( keys %{ $self->idpList } ) {
|
2019-02-05 22:39:09 +01:00
|
|
|
my $idpName = $self->{idpList}->{$_}->{name};
|
2019-02-05 23:12:17 +01:00
|
|
|
$idpName = $self->{idpList}->{$_}->{displayName}
|
|
|
|
if $self->{idpList}->{$_}->{displayName};
|
|
|
|
my $icon = $self->{idpList}->{$_}->{icon};
|
2019-04-10 21:54:03 +02:00
|
|
|
my $order = $self->{idpList}->{$_}->{order} // 0;
|
2019-02-05 22:39:09 +01:00
|
|
|
my $img_src = '';
|
|
|
|
|
|
|
|
if ($icon) {
|
|
|
|
$img_src =
|
|
|
|
( $icon =~ m#^https?://# )
|
|
|
|
? $icon
|
|
|
|
: $portalPath . $self->p->staticPrefix . "/common/" . $icon;
|
|
|
|
}
|
2019-02-05 23:12:17 +01:00
|
|
|
$self->logger->debug( "IDP "
|
|
|
|
. $self->{idpList}->{$_}->{name}
|
2019-04-10 17:21:33 +02:00
|
|
|
. " -> DisplayName : $idpName with Icon : $img_src at order : $order"
|
|
|
|
);
|
2016-09-25 18:44:48 +02:00
|
|
|
push @list,
|
|
|
|
{
|
2019-04-10 16:14:49 +02:00
|
|
|
val => $_,
|
|
|
|
name => $idpName,
|
|
|
|
icon => $img_src,
|
|
|
|
order => $order,
|
2016-09-25 18:44:48 +02:00
|
|
|
};
|
|
|
|
}
|
2019-04-10 09:21:55 +02:00
|
|
|
@list =
|
2019-04-10 17:21:33 +02:00
|
|
|
sort {
|
2019-04-10 19:48:26 +02:00
|
|
|
$a->{order} <=> $b->{order}
|
2019-04-10 17:21:33 +02:00
|
|
|
or $a->{name} cmp $b->{name}
|
|
|
|
or $a->{val} cmp $b->{val}
|
|
|
|
} @list;
|
2018-07-05 22:56:16 +02:00
|
|
|
$req->data->{list} = \@list;
|
|
|
|
$req->data->{confirmRemember} = 1;
|
2016-09-25 18:44:48 +02:00
|
|
|
|
|
|
|
# Delete existing IDP resolution cookie
|
2017-01-13 15:35:02 +01:00
|
|
|
$req->addCookie(
|
|
|
|
$self->p->cookie(
|
|
|
|
name => $self->conf->{samlIdPResolveCookie},
|
|
|
|
value => 0,
|
|
|
|
domain => $self->conf->{domain},
|
|
|
|
secure => 0,
|
2017-03-15 11:01:29 +01:00
|
|
|
expires => 'Wed, 21 Oct 2015 00:00:00 GMT',
|
2017-01-13 15:35:02 +01:00
|
|
|
)
|
|
|
|
);
|
2016-09-25 18:44:48 +02:00
|
|
|
|
|
|
|
#TODO: check this
|
2018-07-05 22:56:16 +02:00
|
|
|
$req->data->{login} = 1;
|
2018-06-29 17:51:39 +02:00
|
|
|
return PE_IDPCHOICE;
|
2016-09-25 18:44:48 +02:00
|
|
|
}
|
|
|
|
|
2018-07-18 08:02:48 +02:00
|
|
|
# Store choosen IDP in cookie
|
2016-09-25 18:44:48 +02:00
|
|
|
unless ( $idp_cookie and $idp eq $idp_cookie ) {
|
2017-02-15 07:41:50 +01:00
|
|
|
$self->logger->debug("Build cookie to remember $idp as IDP choice");
|
2016-09-25 18:44:48 +02:00
|
|
|
|
|
|
|
# Control url parameter
|
2016-11-20 06:35:06 +01:00
|
|
|
my $urlcheck = $self->p->controlUrl($req);
|
2016-09-25 18:44:48 +02:00
|
|
|
return $urlcheck unless ( $urlcheck == PE_OK );
|
|
|
|
|
|
|
|
# User can choose temporary (0) or persistent cookie (1)
|
2016-11-16 16:27:01 +01:00
|
|
|
my $cookie_type = $req->param("cookie_type") || "0";
|
2016-09-25 18:44:48 +02:00
|
|
|
|
2017-03-15 11:01:29 +01:00
|
|
|
# Cookie available 1 year
|
2017-01-13 15:35:02 +01:00
|
|
|
$req->addCookie(
|
|
|
|
$self->p->cookie(
|
2017-03-15 11:01:29 +01:00
|
|
|
name => $self->conf->{samlIdPResolveCookie},
|
|
|
|
value => $idp,
|
|
|
|
domain => $self->conf->{domain},
|
|
|
|
secure => $self->conf->{securedCookie},
|
|
|
|
( $cookie_type ? ( max_age => 31557600 ) : () ),
|
2017-01-13 15:35:02 +01:00
|
|
|
)
|
|
|
|
);
|
2016-09-25 18:44:48 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
# 3. Build authentication request
|
|
|
|
|
|
|
|
# IDP conf key
|
|
|
|
my $idpConfKey = $self->idpList->{$idp}->{confKey};
|
|
|
|
|
|
|
|
unless ($idpConfKey) {
|
2017-02-18 15:25:48 +01:00
|
|
|
$self->userLogger->error("$idp do not match any IDP in configuration");
|
2016-09-25 18:44:48 +02:00
|
|
|
return PE_SAML_UNKNOWN_ENTITY;
|
|
|
|
}
|
|
|
|
|
2017-02-15 07:41:50 +01:00
|
|
|
$self->logger->debug("$idp match $idpConfKey IDP in configuration");
|
2016-09-25 18:44:48 +02:00
|
|
|
|
|
|
|
# IDP ForceAuthn
|
2017-02-15 07:41:50 +01:00
|
|
|
my $forceAuthn = $self->conf->{samlIDPMetaDataOptions}->{$idpConfKey}
|
2016-09-25 18:44:48 +02:00
|
|
|
->{samlIDPMetaDataOptionsForceAuthn};
|
|
|
|
|
|
|
|
# IDP IsPassive
|
2017-02-15 07:41:50 +01:00
|
|
|
my $isPassive = $self->conf->{samlIDPMetaDataOptions}->{$idpConfKey}
|
2016-09-25 18:44:48 +02:00
|
|
|
->{samlIDPMetaDataOptionsIsPassive};
|
|
|
|
|
|
|
|
# IDP NameIDFormat
|
2017-02-15 07:41:50 +01:00
|
|
|
my $nameIDFormat = $self->conf->{samlIDPMetaDataOptions}->{$idpConfKey}
|
2016-09-25 18:44:48 +02:00
|
|
|
->{samlIDPMetaDataOptionsNameIDFormat};
|
|
|
|
$nameIDFormat = $self->getNameIDFormat($nameIDFormat) if $nameIDFormat;
|
|
|
|
|
|
|
|
# IDP ProxyRestriction
|
2017-02-15 07:41:50 +01:00
|
|
|
my $allowProxiedAuthn = $self->conf->{samlIDPMetaDataOptions}->{$idpConfKey}
|
2016-09-25 18:44:48 +02:00
|
|
|
->{samlIDPMetaDataOptionsAllowProxiedAuthn};
|
|
|
|
|
|
|
|
# IDP HTTP method
|
2017-02-15 07:41:50 +01:00
|
|
|
my $method = $self->conf->{samlIDPMetaDataOptions}->{$idpConfKey}
|
2016-09-25 18:44:48 +02:00
|
|
|
->{samlIDPMetaDataOptionsSSOBinding};
|
|
|
|
$method = $self->getHttpMethod($method);
|
|
|
|
|
|
|
|
# If no method defined, get first HTTP method
|
|
|
|
unless ( defined $method ) {
|
|
|
|
my $protocolType = Lasso::Constants::MD_PROTOCOL_TYPE_SINGLE_SIGN_ON;
|
|
|
|
$method =
|
|
|
|
$self->getFirstHttpMethod( $self->lassoServer, $idp, $protocolType );
|
|
|
|
}
|
|
|
|
|
|
|
|
# Failback to HTTP-REDIRECT
|
|
|
|
unless ( defined $method and $method != -1 ) {
|
2017-02-15 07:41:50 +01:00
|
|
|
$self->logger->debug(
|
|
|
|
"No method found with IDP $idpConfKey for SSO profile");
|
2016-09-25 18:44:48 +02:00
|
|
|
$method = $self->getHttpMethod("redirect");
|
|
|
|
}
|
|
|
|
|
2017-02-15 07:41:50 +01:00
|
|
|
$self->logger->debug( "Use method "
|
2016-09-25 18:44:48 +02:00
|
|
|
. $self->getHttpMethodString($method)
|
2017-02-15 07:41:50 +01:00
|
|
|
. " with IDP $idpConfKey for SSO profile" );
|
2016-09-25 18:44:48 +02:00
|
|
|
|
|
|
|
# Set signature
|
2017-02-15 07:41:50 +01:00
|
|
|
my $signSSOMessage = $self->conf->{samlIDPMetaDataOptions}->{$idpConfKey}
|
2016-12-01 23:25:05 +01:00
|
|
|
->{samlIDPMetaDataOptionsSignSSOMessage} // -1;
|
2016-09-25 18:44:48 +02:00
|
|
|
|
|
|
|
# Authentication Context
|
|
|
|
my $requestedAuthnContext =
|
|
|
|
$self->conf->{samlIDPMetaDataOptions}->{$idpConfKey}
|
2016-12-01 23:25:05 +01:00
|
|
|
->{samlIDPMetaDataOptionsRequestedAuthnContext} // '';
|
2016-09-25 18:44:48 +02:00
|
|
|
$requestedAuthnContext = $self->getAuthnContext($requestedAuthnContext)
|
|
|
|
if $requestedAuthnContext;
|
|
|
|
|
|
|
|
# Create SSO request
|
|
|
|
my $login = $self->createAuthnRequest(
|
2016-11-22 09:05:43 +01:00
|
|
|
$req, $self->lassoServer, $idp,
|
|
|
|
$method, $forceAuthn, $isPassive,
|
|
|
|
$nameIDFormat, $allowProxiedAuthn, $signSSOMessage,
|
|
|
|
$requestedAuthnContext
|
2016-09-25 18:44:48 +02:00
|
|
|
);
|
|
|
|
|
|
|
|
unless ($login) {
|
2017-02-18 15:25:48 +01:00
|
|
|
$self->userLogger->error(
|
2017-02-15 07:41:50 +01:00
|
|
|
"Could not create authentication request on $idpConfKey");
|
2016-09-25 18:44:48 +02:00
|
|
|
return PE_SAML_SSO_ERROR;
|
|
|
|
}
|
|
|
|
|
2017-02-15 07:41:50 +01:00
|
|
|
$self->logger->debug("Authentication request created");
|
2016-09-25 18:44:48 +02:00
|
|
|
|
|
|
|
# Keep assertion ID in memory to prevent replay
|
|
|
|
my $samlID = $login->request()->ID;
|
|
|
|
unless ( $self->storeReplayProtection($samlID) ) {
|
2017-02-15 07:41:50 +01:00
|
|
|
$self->logger->error("Unable to store assertion ID");
|
2016-09-25 18:44:48 +02:00
|
|
|
return PE_SAML_SSO_ERROR;
|
|
|
|
}
|
|
|
|
|
|
|
|
# Keep initial SAML request data in memory in case of proxing
|
2018-07-05 22:56:16 +02:00
|
|
|
if ( $req->data->{_proxiedSamlRequest} ) {
|
2016-09-25 18:44:48 +02:00
|
|
|
|
|
|
|
my $infos;
|
|
|
|
|
|
|
|
$infos->{type} = 'proxy';
|
|
|
|
$infos->{_utime} = time;
|
2018-07-05 22:56:16 +02:00
|
|
|
$infos->{Request} = $req->data->{_proxiedRequest};
|
|
|
|
$infos->{Method} = $req->data->{_proxiedMethod};
|
|
|
|
$infos->{RelayState} = $req->data->{_proxiedRelayState};
|
|
|
|
$infos->{Artifact} = $req->data->{_proxiedArtifact};
|
2016-09-25 18:44:48 +02:00
|
|
|
$infos->{ProxyID} = $samlID;
|
|
|
|
|
2017-03-03 18:25:03 +01:00
|
|
|
my $samlSessionInfo = $self->getSamlSession( undef, $infos );
|
2017-02-20 22:00:05 +01:00
|
|
|
|
|
|
|
return PE_SAML_SESSION_ERROR unless $samlSessionInfo;
|
2016-09-25 18:44:48 +02:00
|
|
|
|
2017-02-15 07:41:50 +01:00
|
|
|
$self->logger->debug(
|
|
|
|
"Keep initial SAML request data in memory for ID $samlID");
|
2016-09-25 18:44:48 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
# Send SSO request depending on request method
|
|
|
|
# HTTP-REDIRECT
|
|
|
|
if ( $method == $self->getHttpMethod('redirect')
|
|
|
|
or $method == $self->getHttpMethod('artifact-get') )
|
|
|
|
{
|
|
|
|
|
|
|
|
# Redirect user to response URL
|
|
|
|
my $sso_url = $login->msg_url;
|
2017-02-15 07:41:50 +01:00
|
|
|
$self->logger->debug("Redirect user to $sso_url");
|
2016-09-25 18:44:48 +02:00
|
|
|
|
|
|
|
$req->urldc($sso_url);
|
|
|
|
|
2016-12-04 22:57:25 +01:00
|
|
|
return PE_REDIRECT;
|
2016-09-25 18:44:48 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
# HTTP-POST
|
|
|
|
elsif ($method == $self->getHttpMethod('post')
|
|
|
|
or $method == $self->getHttpMethod('artifact-post') )
|
|
|
|
{
|
|
|
|
|
|
|
|
# Use autosubmit form
|
|
|
|
my $sso_url = $login->msg_url;
|
|
|
|
my $sso_body = $login->msg_body;
|
|
|
|
|
2016-11-22 09:05:43 +01:00
|
|
|
$req->postUrl($sso_url);
|
2016-09-25 18:44:48 +02:00
|
|
|
|
|
|
|
if ( $method == $self->getHttpMethod("artifact-post") ) {
|
|
|
|
$req->{postFields} = { 'SAMLart' => $sso_body };
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
$req->{postFields} = { 'SAMLRequest' => $sso_body };
|
|
|
|
}
|
|
|
|
|
|
|
|
# RelayState
|
|
|
|
$req->{postFields}->{'RelayState'} = $login->msg_relayState
|
|
|
|
if ( $login->msg_relayState );
|
|
|
|
|
2016-09-26 21:12:40 +02:00
|
|
|
# TODO: verify this
|
|
|
|
$req->steps( ['autoPost'] );
|
2016-11-22 13:34:09 +01:00
|
|
|
$req->continue(1);
|
2016-09-26 21:12:40 +02:00
|
|
|
return PE_OK;
|
2016-09-25 18:44:48 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
# No SOAP transport for SSO request
|
2016-12-26 15:35:30 +01:00
|
|
|
return PE_SAML_SSO_ERROR;
|
2016-09-25 18:44:48 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
sub authenticate {
|
2016-12-01 23:25:05 +01:00
|
|
|
PE_OK;
|
|
|
|
}
|
|
|
|
|
|
|
|
sub setAuthSessionInfo {
|
2016-09-25 18:44:48 +02:00
|
|
|
my ( $self, $req ) = @_;
|
2018-07-05 22:56:16 +02:00
|
|
|
my $login = $req->data->{_lassoLogin};
|
|
|
|
my $idp = $req->data->{_idp};
|
|
|
|
my $idpConfKey = $req->data->{_idpConfKey};
|
2016-09-26 21:12:40 +02:00
|
|
|
|
|
|
|
# Get SAML assertion
|
|
|
|
my $assertion = $self->getAssertion($login);
|
|
|
|
|
|
|
|
unless ($assertion) {
|
2017-02-18 15:25:48 +01:00
|
|
|
$self->userLogger->error("No assertion found");
|
2016-09-26 21:12:40 +02:00
|
|
|
return PE_SAML_SSO_ERROR;
|
|
|
|
}
|
|
|
|
|
|
|
|
# Force UTF-8
|
2017-02-15 07:41:50 +01:00
|
|
|
my $force_utf8 = $self->conf->{samlIDPMetaDataOptions}->{$idpConfKey}
|
2016-09-26 21:12:40 +02:00
|
|
|
->{samlIDPMetaDataOptionsForceUTF8};
|
|
|
|
|
|
|
|
# Try to get attributes if attribute statement is present in assertion
|
|
|
|
my $attr_statement = $assertion->AttributeStatement();
|
|
|
|
if ($attr_statement) {
|
|
|
|
|
|
|
|
# Get attributes
|
|
|
|
my @attributes = $attr_statement->Attribute();
|
|
|
|
|
|
|
|
# Wanted attributes are defined in IDP configuration
|
|
|
|
foreach (
|
|
|
|
keys
|
|
|
|
%{ $self->conf->{samlIDPMetaDataExportedAttributes}->{$idpConfKey} }
|
|
|
|
)
|
|
|
|
{
|
|
|
|
|
|
|
|
# Extract fields from exportedAttr value
|
|
|
|
my ( $mandatory, $name, $format, $friendly_name ) =
|
|
|
|
split( /;/,
|
|
|
|
$self->conf->{samlIDPMetaDataExportedAttributes}->{$idpConfKey}
|
|
|
|
->{$_} );
|
|
|
|
|
|
|
|
# Try to get value
|
|
|
|
my $value =
|
|
|
|
$self->getAttributeValue( $name, $format, $friendly_name,
|
|
|
|
\@attributes, $force_utf8 );
|
|
|
|
|
|
|
|
# Store value in sessionInfo
|
|
|
|
$req->{sessionInfo}->{$_} = $value if defined $value;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-12-19 21:47:44 +01:00
|
|
|
# Store other information in session
|
2016-09-26 21:12:40 +02:00
|
|
|
$req->{sessionInfo}->{_idp} = $idp;
|
|
|
|
$req->{sessionInfo}->{_idpConfKey} = $idpConfKey;
|
|
|
|
|
|
|
|
# Adapt _utime with SessionNotOnOrAfter
|
|
|
|
my $sessionNotOnOrAfter;
|
|
|
|
eval {
|
|
|
|
$sessionNotOnOrAfter =
|
|
|
|
$assertion->AuthnStatement()->SessionNotOnOrAfter();
|
|
|
|
};
|
|
|
|
|
|
|
|
if ( $@ or !$sessionNotOnOrAfter ) {
|
2017-02-15 07:41:50 +01:00
|
|
|
$self->logger->debug("No SessionNotOnOrAfter value found");
|
2016-09-26 21:12:40 +02:00
|
|
|
}
|
|
|
|
else {
|
|
|
|
|
|
|
|
my $samltime = $self->samldate2timestamp($sessionNotOnOrAfter);
|
|
|
|
my $utime = time();
|
|
|
|
my $timeout = $self->conf->{timeout};
|
|
|
|
my $adaptSessionUtime =
|
|
|
|
$self->conf->{samlIDPMetaDataOptions}->{$idpConfKey}
|
|
|
|
->{samlIDPMetaDataOptionsAdaptSessionUtime};
|
|
|
|
|
|
|
|
if ( ( $utime + $timeout > $samltime ) and $adaptSessionUtime ) {
|
|
|
|
|
|
|
|
# Use SAML time to determine the start of the session
|
|
|
|
my $new_utime = $samltime - $timeout;
|
|
|
|
$req->{sessionInfo}->{_utime} = $new_utime;
|
2017-02-15 07:41:50 +01:00
|
|
|
$self->logger->debug(
|
|
|
|
"Adapt _utime with SessionNotOnOrAfter value, new _utime: $new_utime"
|
2016-09-26 21:12:40 +02:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
# Establish federation (required for attribute request in UserDBSAML)
|
|
|
|
unless ( $self->acceptSSO($login) ) {
|
2017-02-15 07:41:50 +01:00
|
|
|
$self->userLogger->error(
|
|
|
|
"Error while accepting SSO from IDP $idpConfKey");
|
2016-09-26 21:12:40 +02:00
|
|
|
return PE_SAML_SSO_ERROR;
|
|
|
|
}
|
|
|
|
|
|
|
|
# Get created Lasso::Session and Lasso::Identity
|
|
|
|
my $session = $login->get_session;
|
|
|
|
my $identity = $login->get_identity;
|
|
|
|
|
|
|
|
# Dump Lasso objects in session
|
2018-06-29 06:50:31 +02:00
|
|
|
$req->{sessionInfo}->{ $self->lsDump } = $session->dump() if $session;
|
|
|
|
$req->{sessionInfo}->{ $self->liDump } = $identity->dump() if $identity;
|
2016-09-26 21:12:40 +02:00
|
|
|
|
|
|
|
# Keep SAML Token in session
|
2017-02-15 07:41:50 +01:00
|
|
|
my $store_samlToken = $self->conf->{samlIDPMetaDataOptions}->{$idpConfKey}
|
2016-09-26 21:12:40 +02:00
|
|
|
->{samlIDPMetaDataOptionsStoreSAMLToken};
|
|
|
|
if ($store_samlToken) {
|
2017-02-15 07:41:50 +01:00
|
|
|
$self->logger->debug("Store SAML Token in session");
|
2018-07-05 22:56:16 +02:00
|
|
|
$req->{sessionInfo}->{_samlToken} = $req->data->{_samlToken};
|
2016-09-26 21:12:40 +02:00
|
|
|
}
|
|
|
|
else {
|
2017-02-15 07:41:50 +01:00
|
|
|
$self->logger->debug("SAML Token will not be stored in session");
|
2016-09-26 21:12:40 +02:00
|
|
|
}
|
|
|
|
|
2018-07-05 22:56:16 +02:00
|
|
|
$req->data->{_lassoLogin} = $login;
|
2016-09-26 21:12:40 +02:00
|
|
|
push @{ $req->steps }, sub { $self->authFinish(@_) };
|
|
|
|
|
|
|
|
PE_OK;
|
|
|
|
}
|
|
|
|
|
|
|
|
# Inserted in $req->steps by authenticate()
|
|
|
|
sub authFinish {
|
|
|
|
my ( $self, $req ) = @_;
|
|
|
|
my %h;
|
|
|
|
|
|
|
|
# Real session was stored, get id and utime
|
|
|
|
my $id = $req->{id};
|
|
|
|
my $utime = $req->{sessionInfo}->{_utime};
|
|
|
|
|
|
|
|
# Get saved Lasso objects
|
2018-07-05 22:56:16 +02:00
|
|
|
my $nameid = $req->data->{_nameID};
|
|
|
|
my $session_index = $req->data->{_sessionIndex};
|
2016-09-26 21:12:40 +02:00
|
|
|
|
2017-02-15 07:41:50 +01:00
|
|
|
$self->logger->debug( "Store NameID "
|
2016-09-26 21:12:40 +02:00
|
|
|
. $nameid->dump
|
2017-02-15 07:41:50 +01:00
|
|
|
. " and SessionIndex $session_index for session $id" );
|
2016-09-26 21:12:40 +02:00
|
|
|
|
|
|
|
my $infos;
|
|
|
|
$infos->{type} = 'saml'; # Session type
|
|
|
|
$infos->{_utime} = $utime; # Creation time
|
|
|
|
$infos->{_saml_id} = $id; # SSO session id
|
|
|
|
$infos->{_nameID} = $nameid->dump; # SAML NameID
|
|
|
|
$infos->{_sessionIndex} = $session_index; # SAML SessionIndex
|
|
|
|
|
2017-02-20 22:00:05 +01:00
|
|
|
# Save SAML session
|
2017-03-03 18:25:03 +01:00
|
|
|
my $samlSessionInfo = $self->getSamlSession( undef, $infos );
|
2017-02-20 22:00:05 +01:00
|
|
|
|
|
|
|
return PE_SAML_SESSION_ERROR unless $samlSessionInfo;
|
2016-09-26 21:12:40 +02:00
|
|
|
|
|
|
|
my $session_id = $samlSessionInfo->id;
|
|
|
|
|
2017-02-15 07:41:50 +01:00
|
|
|
$self->logger->debug("Link session $id to SAML session $session_id");
|
2016-09-26 21:12:40 +02:00
|
|
|
|
|
|
|
return PE_OK;
|
2016-09-25 18:44:48 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
sub authLogout {
|
|
|
|
my ( $self, $req ) = @_;
|
2016-09-26 21:12:40 +02:00
|
|
|
my $idp = $req->sessionInfo->{_idp};
|
|
|
|
my $idpConfKey = $req->sessionInfo->{_idpConfKey};
|
|
|
|
my $session_id = $req->sessionInfo->{_session_id};
|
|
|
|
my $method;
|
|
|
|
|
|
|
|
# Real session was previously deleted,
|
|
|
|
# remove corresponding SAML sessions
|
|
|
|
$self->deleteSAMLSecondarySessions($session_id);
|
|
|
|
|
|
|
|
# Recover Lasso::Session dump
|
2018-06-29 06:50:31 +02:00
|
|
|
my $session_dump = $req->{sessionInfo}->{ $self->lsDump };
|
2016-09-26 21:12:40 +02:00
|
|
|
|
|
|
|
unless ($session_dump) {
|
2017-02-18 15:25:48 +01:00
|
|
|
$self->userLogger->error("Could not get session dump from session");
|
2016-09-26 21:12:40 +02:00
|
|
|
return PE_SAML_SLO_ERROR;
|
|
|
|
}
|
|
|
|
|
|
|
|
# IDP HTTP method
|
2017-02-15 07:41:50 +01:00
|
|
|
$method = $self->conf->{samlIDPMetaDataOptions}->{$idpConfKey}
|
2016-09-26 21:12:40 +02:00
|
|
|
->{samlIDPMetaDataOptionsSLOBinding};
|
|
|
|
$method = $self->getHttpMethod($method);
|
|
|
|
|
|
|
|
# If no method defined, get first HTTP method
|
|
|
|
unless ( defined $method ) {
|
|
|
|
my $protocolType = Lasso::Constants::MD_PROTOCOL_TYPE_SINGLE_LOGOUT;
|
2016-11-22 09:05:43 +01:00
|
|
|
$method =
|
|
|
|
$self->getFirstHttpMethod( $self->lassoServer, $idp, $protocolType );
|
2016-09-26 21:12:40 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
# Skip SLO if no method found
|
|
|
|
unless ( defined $method and $method != -1 ) {
|
2017-02-15 07:41:50 +01:00
|
|
|
$self->logger->debug(
|
|
|
|
"No method found with IDP $idpConfKey for SLO profile");
|
2016-09-26 21:12:40 +02:00
|
|
|
return PE_OK;
|
|
|
|
}
|
|
|
|
|
2017-02-15 07:41:50 +01:00
|
|
|
$self->logger->debug( "Use method "
|
2016-09-26 21:12:40 +02:00
|
|
|
. $self->getHttpMethodString($method)
|
2017-02-15 07:41:50 +01:00
|
|
|
. " with IDP $idpConfKey for SLO profile" );
|
2016-09-26 21:12:40 +02:00
|
|
|
|
|
|
|
# Set signature
|
2017-02-15 07:41:50 +01:00
|
|
|
my $signSLOMessage = $self->conf->{samlIDPMetaDataOptions}->{$idpConfKey}
|
2016-12-15 22:22:15 +01:00
|
|
|
->{samlIDPMetaDataOptionsSignSLOMessage} // 0;
|
2016-09-26 21:12:40 +02:00
|
|
|
|
|
|
|
# Build Logout Request
|
|
|
|
my $logout =
|
2016-12-09 11:25:05 +01:00
|
|
|
$self->createLogoutRequest( $req, $self->lassoServer, $session_dump,
|
|
|
|
$method, $signSLOMessage );
|
2016-09-26 21:12:40 +02:00
|
|
|
unless ($logout) {
|
2017-02-15 07:41:50 +01:00
|
|
|
$self->logger->error("Could not create logout request");
|
2016-09-26 21:12:40 +02:00
|
|
|
return PE_SAML_SLO_ERROR;
|
|
|
|
}
|
|
|
|
|
2017-02-15 07:41:50 +01:00
|
|
|
$self->logger->debug("Logout request created");
|
2016-09-26 21:12:40 +02:00
|
|
|
|
|
|
|
# Keep request ID in memory to prevent replay
|
|
|
|
unless ( $self->storeReplayProtection( $logout->request()->ID ) ) {
|
2017-02-15 07:41:50 +01:00
|
|
|
$self->logger->error("Unable to store Logout request ID");
|
2016-09-26 21:12:40 +02:00
|
|
|
return PE_SAML_SLO_ERROR;
|
|
|
|
}
|
|
|
|
|
|
|
|
# Send request depending on request method
|
|
|
|
# HTTP-REDIRECT
|
2016-12-27 09:40:12 +01:00
|
|
|
if ( $method == Lasso::Constants::HTTP_METHOD_REDIRECT
|
|
|
|
or $method == Lasso::Constants::HTTP_METHOD_ARTIFACT_GET )
|
|
|
|
{
|
2016-09-26 21:12:40 +02:00
|
|
|
|
|
|
|
# Redirect user to response URL
|
|
|
|
my $slo_url = $logout->msg_url;
|
2017-02-15 07:41:50 +01:00
|
|
|
$self->logger->debug("Redirect user to $slo_url");
|
2016-09-26 21:12:40 +02:00
|
|
|
|
|
|
|
$req->urldc($slo_url);
|
|
|
|
|
|
|
|
# Redirect done in Portal/Simple.pm
|
2016-12-23 18:00:17 +01:00
|
|
|
return PE_OK;
|
2016-09-26 21:12:40 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
# HTTP-POST
|
2016-12-27 09:40:12 +01:00
|
|
|
elsif ($method == Lasso::Constants::HTTP_METHOD_POST
|
|
|
|
or $method == Lasso::Constants::HTTP_METHOD_ARTIFACT_POST )
|
|
|
|
{
|
2016-09-26 21:12:40 +02:00
|
|
|
|
|
|
|
# Use autosubmit form
|
|
|
|
my $slo_url = $logout->msg_url;
|
|
|
|
my $slo_body = $logout->msg_body;
|
2017-02-15 07:41:50 +01:00
|
|
|
$self->logger->debug("Redirect user to $slo_url using autoPost");
|
2016-09-26 21:12:40 +02:00
|
|
|
|
|
|
|
$req->postUrl($slo_url);
|
2016-12-09 11:25:05 +01:00
|
|
|
$req->postFields( { 'SAMLRequest' => $slo_body } );
|
2016-09-26 21:12:40 +02:00
|
|
|
|
|
|
|
# RelayState
|
|
|
|
$self->postFields->{'RelayState'} = $logout->msg_relayState
|
|
|
|
if ( $logout->msg_relayState );
|
|
|
|
|
|
|
|
# Post done in Portal/Simple.pm
|
2016-12-23 17:03:36 +01:00
|
|
|
$req->steps( [ 'deleteSession', 'autoPost' ] );
|
2016-09-26 21:12:40 +02:00
|
|
|
return PE_OK;
|
|
|
|
}
|
|
|
|
|
|
|
|
# HTTP-SOAP
|
|
|
|
elsif ( $method == Lasso::Constants::HTTP_METHOD_SOAP ) {
|
|
|
|
|
|
|
|
my $slo_url = $logout->msg_url;
|
|
|
|
my $slo_body = $logout->msg_body;
|
|
|
|
|
|
|
|
# Send SOAP request and manage response
|
|
|
|
my $response = $self->sendSOAPMessage( $slo_url, $slo_body );
|
|
|
|
|
|
|
|
unless ($response) {
|
2017-02-15 07:41:50 +01:00
|
|
|
$self->logger->error("No logout response to SOAP request");
|
2016-09-26 21:12:40 +02:00
|
|
|
return PE_SAML_SLO_ERROR;
|
|
|
|
}
|
|
|
|
|
|
|
|
# Create Logout object
|
2016-11-22 09:05:43 +01:00
|
|
|
$logout = $self->createLogout( $self->lassoServer );
|
2016-09-26 21:12:40 +02:00
|
|
|
|
|
|
|
# Process logout response
|
|
|
|
my $result = $self->processLogoutResponseMsg( $logout, $response );
|
|
|
|
|
|
|
|
unless ($result) {
|
2017-02-15 07:41:50 +01:00
|
|
|
$self->logger->error("Fail to process logout response");
|
2016-09-26 21:12:40 +02:00
|
|
|
return PE_SAML_SLO_ERROR;
|
|
|
|
}
|
|
|
|
|
2017-02-15 07:41:50 +01:00
|
|
|
$self->logger->debug("Logout response is valid");
|
2016-09-26 21:12:40 +02:00
|
|
|
|
|
|
|
# Replay protection
|
|
|
|
my $samlID = $logout->response()->InResponseTo;
|
|
|
|
|
|
|
|
unless ( $self->replayProtection($samlID) ) {
|
|
|
|
|
|
|
|
# Logout request was already consumed or is expired
|
2017-02-18 15:25:48 +01:00
|
|
|
$self->userLogger->error("Message $samlID already used or expired");
|
2016-09-26 21:12:40 +02:00
|
|
|
return PE_SAML_SLO_ERROR;
|
|
|
|
}
|
|
|
|
|
|
|
|
return PE_OK;
|
|
|
|
}
|
2016-12-27 09:40:12 +01:00
|
|
|
else {
|
2017-02-18 15:25:48 +01:00
|
|
|
$self->userLogger->error("Lasso method $method not implemented here");
|
2016-12-27 09:40:12 +01:00
|
|
|
return PE_SAML_SLO_ERROR;
|
|
|
|
}
|
2016-09-25 18:44:48 +02:00
|
|
|
}
|
|
|
|
|
2016-12-25 16:41:23 +01:00
|
|
|
sub handleAuthRequests {
|
|
|
|
my ( $self, $req ) = @_;
|
|
|
|
if ( $req->uri =~ $self->sloRe ) {
|
|
|
|
return $self->extractFormInfo($req);
|
|
|
|
}
|
|
|
|
PE_OK;
|
|
|
|
}
|
|
|
|
|
2016-09-26 21:12:40 +02:00
|
|
|
# TODO: authForce
|
|
|
|
|
2016-09-25 18:44:48 +02:00
|
|
|
sub getDisplayType {
|
2016-09-26 21:12:40 +02:00
|
|
|
return "logo";
|
|
|
|
}
|
|
|
|
|
|
|
|
# Internal methods
|
|
|
|
|
|
|
|
# Try to find an IdP using :
|
|
|
|
# * HTTP parameter
|
|
|
|
# * "samlIdPResolveCookie" cookie
|
|
|
|
# * Rules
|
|
|
|
#
|
|
|
|
# @return Array containing :
|
|
|
|
# * IdP found (or undef)
|
|
|
|
# * Cookie value if exists
|
|
|
|
sub getIDP {
|
|
|
|
my ( $self, $req ) = @_;
|
|
|
|
my $idp;
|
|
|
|
my $idpName;
|
|
|
|
|
2017-01-04 17:36:54 +01:00
|
|
|
my $idp_cookie = $req->cookies->{ $self->{conf}->{samlIdPResolveCookie} };
|
2016-09-26 21:12:40 +02:00
|
|
|
|
|
|
|
# Case 1: Recover IDP from idp URL Parameter
|
2016-11-16 16:27:01 +01:00
|
|
|
unless ( $idp = $req->param("idp") ) {
|
2016-09-26 21:12:40 +02:00
|
|
|
|
|
|
|
# Case 2: Recover IDP from idpName URL Parameter
|
2016-11-16 16:27:01 +01:00
|
|
|
if ( $idpName = $req->param("idpName") ) {
|
2016-09-26 21:12:40 +02:00
|
|
|
foreach ( keys %{ $self->idpList } ) {
|
|
|
|
my $idpConfKey = $self->idpList->{$_}->{confKey};
|
|
|
|
if ( $idpName eq $idpConfKey ) {
|
|
|
|
$idp = $_;
|
2017-02-15 07:41:50 +01:00
|
|
|
$self->logger->debug(
|
|
|
|
"IDP $idp found from idpName URL Parameter ($idpName)");
|
2016-09-26 21:12:40 +02:00
|
|
|
last;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
# Case 3: Recover IDP from cookie
|
|
|
|
if ( !$idp and $idp = $idp_cookie ) {
|
2017-02-15 07:41:50 +01:00
|
|
|
$self->logger->debug("IDP $idp found in IDP resolution cookie");
|
2016-09-26 21:12:40 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
# Case 4: check all IDP resolution rules
|
|
|
|
# The first match win
|
|
|
|
else {
|
|
|
|
foreach ( keys %{ $self->idpList } ) {
|
|
|
|
my $idpConfKey = $self->idpList->{$_}->{confKey};
|
2017-03-22 20:03:49 +01:00
|
|
|
my $cond = $self->idpRules->{$idpConfKey} or next;
|
2017-03-28 23:07:49 +02:00
|
|
|
if ( $cond->( $req, $req->sessionInfo ) ) {
|
2017-02-15 07:41:50 +01:00
|
|
|
$self->logger->debug(
|
|
|
|
"IDP $idpConfKey resolution rule match");
|
2016-09-26 21:12:40 +02:00
|
|
|
$idp = $_;
|
|
|
|
last;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2017-02-15 07:41:50 +01:00
|
|
|
$self->logger->debug('No IDP found') unless ($idp);
|
2016-09-26 21:12:40 +02:00
|
|
|
}
|
2018-09-11 10:03:09 +02:00
|
|
|
else {
|
|
|
|
$self->logger->debug("IDP $idp found from idp URL Parameter");
|
|
|
|
}
|
2016-09-26 21:12:40 +02:00
|
|
|
|
2018-07-02 11:45:26 +02:00
|
|
|
# Case 6: auto select IDP if only one IDP defined
|
|
|
|
if ( scalar keys %{ $self->idpList } == 1 ) {
|
|
|
|
($idp) = keys %{ $self->idpList };
|
|
|
|
$self->logger->debug("Selecting the only defined SAML IDP: $idp");
|
|
|
|
}
|
|
|
|
|
2016-09-26 21:12:40 +02:00
|
|
|
# Alert when selected IDP is unknown
|
|
|
|
if ( $idp and !exists $self->idpList->{$idp} ) {
|
2018-09-04 14:39:19 +02:00
|
|
|
$self->logger->error("Required IDP $idp does not exists");
|
2016-09-26 21:12:40 +02:00
|
|
|
$idp = undef;
|
|
|
|
}
|
|
|
|
|
|
|
|
return ( $idp, $idp_cookie );
|
2016-09-25 18:44:48 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
1;
|