Working on SAML (#595)
This commit is contained in:
parent
59282e5a1a
commit
29453547e6
|
@ -23,6 +23,8 @@ our $VERSION = '2.0.0';
|
|||
|
||||
extends 'Lemonldap::NG::Portal::Auth::Base', 'Lemonldap::NG::Portal::Lib::SAML';
|
||||
|
||||
# INITIALIZATION
|
||||
|
||||
sub init {
|
||||
my ($self) = @_;
|
||||
|
||||
|
@ -30,6 +32,8 @@ sub init {
|
|||
return ( $self->SUPER::init and $self->loadIDPs );
|
||||
}
|
||||
|
||||
# RUNNING METHODS
|
||||
|
||||
sub extractFormInfo {
|
||||
my ( $self, $req ) = @_;
|
||||
|
||||
|
@ -170,7 +174,8 @@ sub extractFormInfo {
|
|||
);
|
||||
delete $req->{urldc};
|
||||
$req->mustRedirect(1);
|
||||
return $self->_subProcess(qw(autoRedirect));
|
||||
$req->steps( [] );
|
||||
return PE_OK;
|
||||
}
|
||||
}
|
||||
else {
|
||||
|
@ -360,7 +365,8 @@ sub extractFormInfo {
|
|||
|
||||
# Redirect user
|
||||
$req->mustRedirect(1);
|
||||
return $self->_subProcess(qw(autoRedirect));
|
||||
$req->steps( [] );
|
||||
return PE_OK;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -457,9 +463,12 @@ sub extractFormInfo {
|
|||
'debug' );
|
||||
}
|
||||
|
||||
return $self->_subProcess(qw(autoRedirect))
|
||||
if ( $req->urldc
|
||||
and $self->conf->{portal} !~ /\Q$req->{urldc}\E\/?/ );
|
||||
if ( $req->urldc
|
||||
and $self->conf->{portal} !~ /\Q$req->{urldc}\E\/?/ )
|
||||
{
|
||||
$req->steps( [] );
|
||||
return PE_OK;
|
||||
}
|
||||
|
||||
# Else, inform user that logout is OK
|
||||
return PE_LOGOUT_OK;
|
||||
|
@ -684,7 +693,8 @@ sub extractFormInfo {
|
|||
|
||||
$req->urldc($slo_url);
|
||||
|
||||
return $self->_subProcess(qw(autoRedirect));
|
||||
$req->steps( [] );
|
||||
return PE_OK;
|
||||
}
|
||||
|
||||
# HTTP-POST
|
||||
|
@ -701,7 +711,9 @@ sub extractFormInfo {
|
|||
$req->postFields->{'RelayState'} = $relaystate
|
||||
if ($relaystate);
|
||||
|
||||
return $self->_subProcess(qw(autoPost));
|
||||
# TODO: verify this
|
||||
$req->steps( ['autoPost'] );
|
||||
return PE_OK;
|
||||
}
|
||||
|
||||
# HTTP-SOAP
|
||||
|
@ -713,7 +725,9 @@ sub extractFormInfo {
|
|||
|
||||
$req->datas->{SOAPMessage} = $slo_body;
|
||||
|
||||
$self->_subProcess(qw(returnSOAPMessage));
|
||||
# TODO: check this
|
||||
$req->steps( ['returnSOAPMessage'] );
|
||||
return PE_OK;
|
||||
|
||||
# If we are here, there was a problem with SOAP response
|
||||
$self->lmLog( "Logout response was not sent trough SOAP",
|
||||
|
@ -729,7 +743,8 @@ sub extractFormInfo {
|
|||
|
||||
# Redirect user
|
||||
$req->mustRedirect(1);
|
||||
return $self->_subProcess(qw(autoRedirect));
|
||||
$req->steps( [] );
|
||||
return PE_OK;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -781,7 +796,7 @@ sub extractFormInfo {
|
|||
# 2. IDP resolution
|
||||
|
||||
# Search a selected IdP
|
||||
my ( $idp, $idp_cookie ) = $self->_sub('getIDP');
|
||||
my ( $idp, $idp_cookie ) = $self->getIDP($req);
|
||||
|
||||
# Get confirmation flag
|
||||
my $confirm_flag = $self->param("confirm");
|
||||
|
@ -1003,7 +1018,8 @@ sub extractFormInfo {
|
|||
|
||||
$req->urldc($sso_url);
|
||||
|
||||
return $self->_subProcess(qw(autoRedirect));
|
||||
$req->steps( [] );
|
||||
return PE_OK;
|
||||
}
|
||||
|
||||
# HTTP-POST
|
||||
|
@ -1028,7 +1044,9 @@ sub extractFormInfo {
|
|||
$req->{postFields}->{'RelayState'} = $login->msg_relayState
|
||||
if ( $login->msg_relayState );
|
||||
|
||||
return $self->_subProcess(qw(autoPost));
|
||||
# TODO: verify this
|
||||
$req->steps( ['autoPost'] );
|
||||
return PE_OK;
|
||||
}
|
||||
|
||||
# No SOAP transport for SSO request
|
||||
|
@ -1036,13 +1054,415 @@ sub extractFormInfo {
|
|||
|
||||
sub authenticate {
|
||||
my ( $self, $req ) = @_;
|
||||
my $server = $self->lassoServer;
|
||||
my $login = $req->datas->{_lassoLogin};
|
||||
my $idp = $req->datas->{_idp};
|
||||
my $idpConfKey = $req->datas->{_idpConfKey};
|
||||
|
||||
# Get SAML assertion
|
||||
my $assertion = $self->getAssertion($login);
|
||||
|
||||
unless ($assertion) {
|
||||
$self->lmLog( "No assertion found", 'error' );
|
||||
return PE_SAML_SSO_ERROR;
|
||||
}
|
||||
|
||||
# Force UTF-8
|
||||
my $force_utf8 =
|
||||
$self->conf->{samlIDPMetaDataOptions}->{$idpConfKey}
|
||||
->{samlIDPMetaDataOptionsForceUTF8};
|
||||
|
||||
# Try to get attributes if attribute statement is present in assertion
|
||||
my $attr_statement = $assertion->AttributeStatement();
|
||||
if ($attr_statement) {
|
||||
|
||||
# Get attributes
|
||||
my @attributes = $attr_statement->Attribute();
|
||||
|
||||
# Wanted attributes are defined in IDP configuration
|
||||
foreach (
|
||||
keys
|
||||
%{ $self->conf->{samlIDPMetaDataExportedAttributes}->{$idpConfKey} }
|
||||
)
|
||||
{
|
||||
|
||||
# Extract fields from exportedAttr value
|
||||
my ( $mandatory, $name, $format, $friendly_name ) =
|
||||
split( /;/,
|
||||
$self->conf->{samlIDPMetaDataExportedAttributes}->{$idpConfKey}
|
||||
->{$_} );
|
||||
|
||||
# Try to get value
|
||||
my $value =
|
||||
$self->getAttributeValue( $name, $format, $friendly_name,
|
||||
\@attributes, $force_utf8 );
|
||||
|
||||
# Store value in sessionInfo
|
||||
$req->{sessionInfo}->{$_} = $value if defined $value;
|
||||
}
|
||||
}
|
||||
|
||||
# Store other informations in session
|
||||
$req->{sessionInfo}->{_idp} = $idp;
|
||||
$req->{sessionInfo}->{_idpConfKey} = $idpConfKey;
|
||||
|
||||
# Adapt _utime with SessionNotOnOrAfter
|
||||
my $sessionNotOnOrAfter;
|
||||
eval {
|
||||
$sessionNotOnOrAfter =
|
||||
$assertion->AuthnStatement()->SessionNotOnOrAfter();
|
||||
};
|
||||
|
||||
if ( $@ or !$sessionNotOnOrAfter ) {
|
||||
$self->lmLog( "No SessionNotOnOrAfter value found", 'debug' );
|
||||
}
|
||||
else {
|
||||
|
||||
my $samltime = $self->samldate2timestamp($sessionNotOnOrAfter);
|
||||
my $utime = time();
|
||||
my $timeout = $self->conf->{timeout};
|
||||
my $adaptSessionUtime =
|
||||
$self->conf->{samlIDPMetaDataOptions}->{$idpConfKey}
|
||||
->{samlIDPMetaDataOptionsAdaptSessionUtime};
|
||||
|
||||
if ( ( $utime + $timeout > $samltime ) and $adaptSessionUtime ) {
|
||||
|
||||
# Use SAML time to determine the start of the session
|
||||
my $new_utime = $samltime - $timeout;
|
||||
$req->{sessionInfo}->{_utime} = $new_utime;
|
||||
$self->lmLog(
|
||||
"Adapt _utime with SessionNotOnOrAfter value, new _utime: $new_utime",
|
||||
'debug'
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
# Establish federation (required for attribute request in UserDBSAML)
|
||||
unless ( $self->acceptSSO($login) ) {
|
||||
$self->lmLog( "Error while accepting SSO from IDP $idpConfKey",
|
||||
'error' );
|
||||
return PE_SAML_SSO_ERROR;
|
||||
}
|
||||
|
||||
# Get created Lasso::Session and Lasso::Identity
|
||||
my $session = $login->get_session;
|
||||
my $identity = $login->get_identity;
|
||||
|
||||
# Dump Lasso objects in session
|
||||
$req->{sessionInfo}->{_lassoSessionDump} = $session->dump() if $session;
|
||||
$req->{sessionInfo}->{_lassoIdentityDump} = $identity->dump() if $identity;
|
||||
|
||||
# Keep SAML Token in session
|
||||
my $store_samlToken =
|
||||
$self->conf->{samlIDPMetaDataOptions}->{$idpConfKey}
|
||||
->{samlIDPMetaDataOptionsStoreSAMLToken};
|
||||
if ($store_samlToken) {
|
||||
$self->lmLog( "Store SAML Token in session", 'debug' );
|
||||
$req->{sessionInfo}->{_samlToken} = $req->datas->{_samlToken};
|
||||
}
|
||||
else {
|
||||
$self->lmLog( "SAML Token will not be stored in session", 'debug' );
|
||||
}
|
||||
|
||||
$req->datas->{_lassoLogin} = $login;
|
||||
push @{ $req->steps }, sub { $self->authFinish(@_) };
|
||||
|
||||
PE_OK;
|
||||
}
|
||||
|
||||
# Inserted in $req->steps by authenticate()
|
||||
sub authFinish {
|
||||
my ( $self, $req ) = @_;
|
||||
my %h;
|
||||
|
||||
# Real session was stored, get id and utime
|
||||
my $id = $req->{id};
|
||||
my $utime = $req->{sessionInfo}->{_utime};
|
||||
|
||||
# Get saved Lasso objects
|
||||
my $nameid = $req->datas->{_nameID};
|
||||
my $session_index = $req->datas->{_sessionIndex};
|
||||
|
||||
$self->lmLog(
|
||||
"Store NameID "
|
||||
. $nameid->dump
|
||||
. " and SessionIndex $session_index for session $id",
|
||||
'debug'
|
||||
);
|
||||
|
||||
# Save SAML session
|
||||
my $samlSessionInfo = $self->getSamlSession();
|
||||
|
||||
return PE_SAML_SESSION_ERROR unless $samlSessionInfo;
|
||||
|
||||
my $infos;
|
||||
$infos->{type} = 'saml'; # Session type
|
||||
$infos->{_utime} = $utime; # Creation time
|
||||
$infos->{_saml_id} = $id; # SSO session id
|
||||
$infos->{_nameID} = $nameid->dump; # SAML NameID
|
||||
$infos->{_sessionIndex} = $session_index; # SAML SessionIndex
|
||||
|
||||
$samlSessionInfo->update($infos);
|
||||
|
||||
my $session_id = $samlSessionInfo->id;
|
||||
|
||||
$self->lmLog( "Link session $id to SAML session $session_id", 'debug' );
|
||||
|
||||
return PE_OK;
|
||||
}
|
||||
|
||||
sub authLogout {
|
||||
my ( $self, $req ) = @_;
|
||||
my $idp = $req->sessionInfo->{_idp};
|
||||
my $idpConfKey = $req->sessionInfo->{_idpConfKey};
|
||||
my $session_id = $req->sessionInfo->{_session_id};
|
||||
my $method;
|
||||
|
||||
# Real session was previously deleted,
|
||||
# remove corresponding SAML sessions
|
||||
$self->deleteSAMLSecondarySessions($session_id);
|
||||
|
||||
my $server = $self->lassoServer;
|
||||
|
||||
# Recover Lasso::Session dump
|
||||
my $session_dump = $req->{sessionInfo}->{_lassoSessionDump};
|
||||
|
||||
unless ($session_dump) {
|
||||
$self->lmLog( "Could not get session dump from session", 'error' );
|
||||
return PE_SAML_SLO_ERROR;
|
||||
}
|
||||
|
||||
# IDP HTTP method
|
||||
$method =
|
||||
$self->conf->{samlIDPMetaDataOptions}->{$idpConfKey}
|
||||
->{samlIDPMetaDataOptionsSLOBinding};
|
||||
$method = $self->getHttpMethod($method);
|
||||
|
||||
# If no method defined, get first HTTP method
|
||||
no strict 'subs';
|
||||
unless ( defined $method ) {
|
||||
my $protocolType = Lasso::Constants::MD_PROTOCOL_TYPE_SINGLE_LOGOUT;
|
||||
$method = $self->getFirstHttpMethod( $server, $idp, $protocolType );
|
||||
}
|
||||
|
||||
# Skip SLO if no method found
|
||||
unless ( defined $method and $method != -1 ) {
|
||||
$self->lmLog( "No method found with IDP $idpConfKey for SLO profile",
|
||||
'debug' );
|
||||
return PE_OK;
|
||||
}
|
||||
|
||||
$self->lmLog(
|
||||
"Use method "
|
||||
. $self->getHttpMethodString($method)
|
||||
. " with IDP $idpConfKey for SLO profile",
|
||||
'debug'
|
||||
);
|
||||
|
||||
# Set signature
|
||||
my $signSLOMessage =
|
||||
$self->conf->{samlIDPMetaDataOptions}->{$idpConfKey}
|
||||
->{samlIDPMetaDataOptionsSignSLOMessage};
|
||||
|
||||
# Build Logout Request
|
||||
my $logout =
|
||||
$self->createLogoutRequest( $server, $session_dump, $method,
|
||||
$signSLOMessage );
|
||||
unless ($logout) {
|
||||
$self->lmLog( "Could not create logout request", 'error' );
|
||||
return PE_SAML_SLO_ERROR;
|
||||
}
|
||||
|
||||
$self->lmLog( "Logout request created", 'debug' );
|
||||
|
||||
# Keep request ID in memory to prevent replay
|
||||
unless ( $self->storeReplayProtection( $logout->request()->ID ) ) {
|
||||
$self->lmLog( "Unable to store Logout request ID", 'error' );
|
||||
return PE_SAML_SLO_ERROR;
|
||||
}
|
||||
|
||||
# Send request depending on request method
|
||||
# HTTP-REDIRECT
|
||||
if ( $method == Lasso::Constants::HTTP_METHOD_REDIRECT ) {
|
||||
|
||||
# Redirect user to response URL
|
||||
my $slo_url = $logout->msg_url;
|
||||
$self->lmLog( "Redirect user to $slo_url", 'debug' );
|
||||
|
||||
$req->urldc($slo_url);
|
||||
|
||||
# Redirect done in Portal/Simple.pm
|
||||
return PE_OK;
|
||||
}
|
||||
|
||||
# HTTP-POST
|
||||
elsif ( $method == Lasso::Constants::HTTP_METHOD_POST ) {
|
||||
|
||||
# Use autosubmit form
|
||||
my $slo_url = $logout->msg_url;
|
||||
my $slo_body = $logout->msg_body;
|
||||
|
||||
$req->postUrl($slo_url);
|
||||
$self->postFields( { 'SAMLRequest' => $slo_body } );
|
||||
|
||||
# RelayState
|
||||
$self->postFields->{'RelayState'} = $logout->msg_relayState
|
||||
if ( $logout->msg_relayState );
|
||||
|
||||
# Post done in Portal/Simple.pm
|
||||
return PE_OK;
|
||||
}
|
||||
|
||||
# HTTP-SOAP
|
||||
elsif ( $method == Lasso::Constants::HTTP_METHOD_SOAP ) {
|
||||
|
||||
my $slo_url = $logout->msg_url;
|
||||
my $slo_body = $logout->msg_body;
|
||||
|
||||
# Send SOAP request and manage response
|
||||
my $response = $self->sendSOAPMessage( $slo_url, $slo_body );
|
||||
|
||||
unless ($response) {
|
||||
$self->lmLog( "No logout response to SOAP request", 'error' );
|
||||
return PE_SAML_SLO_ERROR;
|
||||
}
|
||||
|
||||
# Create Logout object
|
||||
$logout = $self->createLogout($server);
|
||||
|
||||
# Process logout response
|
||||
my $result = $self->processLogoutResponseMsg( $logout, $response );
|
||||
|
||||
unless ($result) {
|
||||
$self->lmLog( "Fail to process logout response", 'error' );
|
||||
return PE_SAML_SLO_ERROR;
|
||||
}
|
||||
|
||||
$self->lmLog( "Logout response is valid", 'debug' );
|
||||
|
||||
# Replay protection
|
||||
my $samlID = $logout->response()->InResponseTo;
|
||||
|
||||
unless ( $self->replayProtection($samlID) ) {
|
||||
|
||||
# Logout request was already consumed or is expired
|
||||
$self->lmLog( "Message $samlID already used or expired", 'error' );
|
||||
return PE_SAML_SLO_ERROR;
|
||||
}
|
||||
|
||||
return PE_OK;
|
||||
}
|
||||
}
|
||||
|
||||
# TODO: authForce
|
||||
|
||||
sub getDisplayType {
|
||||
return "logo";
|
||||
}
|
||||
|
||||
# Internal methods
|
||||
|
||||
# Try to find an IdP using :
|
||||
# * HTTP parameter
|
||||
# * "samlIdPResolveCookie" cookie
|
||||
# * Rules
|
||||
# * Common Domain Cookie
|
||||
#
|
||||
# @return Array containing :
|
||||
# * IdP found (or undef)
|
||||
# * Cookie value if exists
|
||||
sub getIDP {
|
||||
my ( $self, $req ) = @_;
|
||||
my $idp;
|
||||
my $idpName;
|
||||
|
||||
my $idp_cookie;
|
||||
if ( $req->cookie
|
||||
&& $req->cookie =~ /$self->{conf}->{samlIdPResolveCookie}=([^,; ]+)/o )
|
||||
{
|
||||
$idp_cookie = $1;
|
||||
}
|
||||
|
||||
# Case 1: Recover IDP from idp URL Parameter
|
||||
unless ( $idp = $self->param("idp") ) {
|
||||
|
||||
# Case 2: Recover IDP from idpName URL Parameter
|
||||
if ( $idpName = $self->param("idpName") ) {
|
||||
foreach ( keys %{ $self->idpList } ) {
|
||||
my $idpConfKey = $self->idpList->{$_}->{confKey};
|
||||
if ( $idpName eq $idpConfKey ) {
|
||||
$idp = $_;
|
||||
$self->lmLog(
|
||||
"IDP $idp found from idpName URL Parameter ($idpName)",
|
||||
'debug'
|
||||
);
|
||||
last;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Case 3: Recover IDP from cookie
|
||||
if ( !$idp and $idp = $idp_cookie ) {
|
||||
$self->lmLog( "IDP $idp found in IDP resolution cookie", 'debug' );
|
||||
}
|
||||
|
||||
# Case 4: check all IDP resolution rules
|
||||
# The first match win
|
||||
else {
|
||||
foreach ( keys %{ $self->idpList } ) {
|
||||
my $idpConfKey = $self->idpList->{$_}->{confKey};
|
||||
my $cond =
|
||||
$self->conf->{samlIDPMetaDataOptions}->{$idpConfKey}
|
||||
->{samlIDPMetaDataOptionsResolutionRule};
|
||||
next unless defined $cond;
|
||||
if ( $self->safe->reval($cond) ) {
|
||||
$self->lmLog( "IDP $idpConfKey resolution rule match",
|
||||
'debug' );
|
||||
$idp = $_;
|
||||
last;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Case 5: use Common Domain Cookie
|
||||
if ( !$idp
|
||||
and $self->conf->{samlCommonDomainCookieActivation}
|
||||
and $self->conf->{samlCommonDomainCookieReader} )
|
||||
{
|
||||
$self->lmLog(
|
||||
"Will try to use Common Domain Cookie for IDP resolution",
|
||||
'debug' );
|
||||
|
||||
# Add current URL to CDC Reader URL
|
||||
my $return_url = encode_base64( $self->self_url(), '' );
|
||||
|
||||
my $cdc_reader_url = $self->conf->{samlCommonDomainCookieReader};
|
||||
|
||||
$cdc_reader_url .= (
|
||||
$self->conf->{samlCommonDomainCookieReader} =~ /\?/
|
||||
? '&u->confrl=' . $return_url
|
||||
: '?url=' . $return_url
|
||||
);
|
||||
|
||||
$self->lmLog( "Redirect user to $cdc_reader_url", 'debug' );
|
||||
|
||||
$req->urldc($cdc_reader_url);
|
||||
|
||||
$req->steps( [] );
|
||||
return PE_OK;
|
||||
}
|
||||
|
||||
$self->lmLog( 'No IDP found', 'debug' ) unless ($idp);
|
||||
}
|
||||
|
||||
# Alert when selected IDP is unknown
|
||||
if ( $idp and !exists $self->idpList->{$idp} ) {
|
||||
$self->userError("Required IDP $idp does not exists");
|
||||
$idp = undef;
|
||||
}
|
||||
|
||||
return ( $idp, $idp_cookie );
|
||||
}
|
||||
|
||||
1;
|
||||
|
|
Loading…
Reference in New Issue
Block a user