lemonldap-ng/modules/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/AuthSAML.pm

1292 lines
40 KiB
Perl
Raw Normal View History

2009-04-07 22:38:24 +02:00
## @file
2010-02-04 13:30:18 +01:00
# SAML Service Provider - Authentication
2009-04-07 22:38:24 +02:00
## @class
2010-02-04 13:30:18 +01:00
# SAML Service Provider - Authentication
2009-04-07 22:38:24 +02:00
package Lemonldap::NG::Portal::AuthSAML;
use strict;
use Lemonldap::NG::Portal::Simple;
2010-01-29 11:44:56 +01:00
use Lemonldap::NG::Portal::_SAML; #inherits
use Lemonldap::NG::Common::Conf::SAML::Metadata;
use POSIX;
2009-04-07 22:38:24 +02:00
our $VERSION = '0.1';
2010-03-01 11:45:04 +01:00
our @ISA = qw(Lemonldap::NG::Portal::_SAML);
2009-04-07 22:38:24 +02:00
## @apmethod int authInit()
2010-01-29 18:33:35 +01:00
# Load Lasso and metadata
2009-04-07 22:38:24 +02:00
# @return Lemonldap::NG::Portal error code
sub authInit {
my $self = shift;
2010-01-29 11:44:56 +01:00
# Load SAML service
return PE_ERROR unless $self->loadService();
2010-01-29 11:44:56 +01:00
# Load SAML identity providers
return PE_ERROR unless $self->loadIDPs();
2010-02-04 17:02:02 +01:00
2010-01-29 11:44:56 +01:00
PE_OK;
2009-04-07 22:38:24 +02:00
}
## @apmethod int extractFormInfo()
2010-02-04 13:30:18 +01:00
# Check authentication statement or create authentication request
2009-04-07 22:38:24 +02:00
# @return Lemonldap::NG::Portal error code
sub extractFormInfo {
2010-02-08 11:06:21 +01:00
my $self = shift;
2010-02-04 17:02:02 +01:00
my $server = $self->{_lassoServer};
my $login;
2010-02-18 18:22:04 +01:00
my $logout;
2010-02-04 13:30:18 +01:00
my $idp;
2010-02-18 18:22:04 +01:00
my $method;
my $request;
my $response;
my $artifact;
my $relaystate;
2010-02-04 13:30:18 +01:00
# Try to recover IDP from IDP cookie
my %cookies = fetch CGI::Cookie;
my $idp_cookie = $cookies{ $self->{samlIdPResolveCookie} };
if ($idp_cookie) {
$idp = $idp_cookie->value;
$self->lmLog( "IDP $idp found in IDP resolution cookie", 'debug' );
}
2010-02-18 18:22:04 +01:00
# 1. Get HTTP request informations to know
# if we are receving SAML request or response
2010-03-01 11:45:04 +01:00
my $url = $self->url();
2010-02-18 18:22:04 +01:00
my $request_method = $self->request_method();
my $content_type = $self->content_type();
my $saml_acs_art_url = $self->getMetaDataURL(
"samlSPSSODescriptorAssertionConsumerServiceHTTPArtifact");
my $saml_acs_post_url = $self->getMetaDataURL(
"samlSPSSODescriptorAssertionConsumerServiceHTTPPost");
my $saml_acs_get_url = $self->getMetaDataURL(
"samlSPSSODescriptorAssertionConsumerServiceHTTPRedirect");
2010-02-18 18:22:04 +01:00
my $saml_slo_soap_url =
$self->getMetaDataURL( "samlSPSSODescriptorSingleLogoutServiceSOAP", 1 );
my $saml_slo_soap_url_ret =
$self->getMetaDataURL( "samlSPSSODescriptorSingleLogoutServiceSOAP", 2 );
my $saml_slo_get_url =
$self->getMetaDataURL( "samlSPSSODescriptorSingleLogoutServiceHTTP", 1 );
my $saml_slo_get_url_ret =
$self->getMetaDataURL( "samlSPSSODescriptorSingleLogoutServiceHTTP", 2 );
my $saml_ars_url = $self->getMetaDataURL(
"samlSPSSODescriptorArtifactResolutionServiceArtifact");
2010-02-18 18:22:04 +01:00
# 1.1 SSO assertion consumer
if ( $url =~ /^($saml_acs_art_url|$saml_acs_post_url|$saml_acs_get_url)$/i )
{
2010-02-18 18:22:04 +01:00
$self->lmLog( "URL $url detected as an SSO assertion consumer URL",
'debug' );
2010-02-09 21:49:23 +01:00
# Create Login object
$login = $self->createLogin($server);
# Do we check signature?
if ($idp) {
my $checkSSOMessageSignature =
$self->{samlIDPMetaDataOptions}->{$idp}
->{samlIDPMetaDataOptionsCheckSSOMessageSignature};
unless ($checkSSOMessageSignature) {
$self->lmLog(
"SSO message signature from IDP $idp will not be checked",
'debug' );
$self->disableSignature($login);
}
}
else {
$self->lmLog(
"Will not check checkSSOMessageSignature option because no IDP was found",
'debug'
);
}
# Get relayState
$relaystate = $self->param('RelayState');
2010-02-18 18:22:04 +01:00
# 1.1.1 HTTP REDIRECT
if ( $request_method =~ /^GET$/ ) {
$method = Lasso::Constants::HTTP_METHOD_REDIRECT;
$self->lmLog( "SSO method: HTTP-REDIRECT", 'debug' );
if ( $self->param('SAMLResponse') ) {
2010-03-01 11:45:04 +01:00
# Response in query string
$response = $self->query_string();
$self->lmLog( "HTTP-REDIRECT: SAML Response $response",
'debug' );
}
if ( $self->param('SAMLRequest') ) {
# Request in query string
$request = $self->query_string();
$self->lmLog( "HTTP-REDIRECT: SAML Request $request", 'debug' );
}
if ( $self->param('SAMLart') ) {
# Artifcat in query string
$artifact = $self->query_string();
$self->lmLog( "HTTP-REDIRECT: SAML Artifact $artifact",
'debug' );
# Resolve Artifact
$method = Lasso::Constants::HTTP_METHOD_ARTIFACT_GET;
my $message =
$self->resolveArtifact( $login, $artifact, $method );
# Request or response ?
if ( $message =~ /samlp:response/i ) {
$response = $message;
}
else {
$request = $message;
}
}
}
elsif ( $request_method =~ /^POST$/ ) {
# 1.2.2 POST
if ( $content_type !~ /xml/ ) {
$method = Lasso::Constants::HTTP_METHOD_POST;
$self->lmLog( "SSO method: HTTP-POST", 'debug' );
if ( $self->param('SAMLResponse') ) {
# Response in body part
$response = $self->param('SAMLResponse');
$self->lmLog( "HTTP-POST: SAML Response $response",
'debug' );
}
if ( $self->param('SAMLRequest') ) {
# Request in body part
$request = $self->param('SAMLRequest');
$self->lmLog( "HTTP-POST: SAML Request $request", 'debug' );
}
if ( $self->param('SAMLart') ) {
# Artifcat in SAMLart param
$artifact = $self->param('SAMLart');
$self->lmLog( "HTTP-REDIRECT: SAML Artifact $artifact",
'debug' );
# Resolve Artifact
$method = Lasso::Constants::HTTP_METHOD_ARTIFACT_POST;
my $message =
$self->resolveArtifact( $login, $artifact, $method );
# Request or response ?
if ( $message =~ /samlp:response/i ) {
$response = $message;
}
else {
$request = $message;
}
}
}
# 1.2.3 SOAP
else {
$method = Lasso::Constants::HTTP_METHOD_SOAP;
$self->lmLog( "SSO method: HTTP-SOAP", 'debug' );
# SOAP is always a request
$request = $self->param('POSTDATA');
$self->lmLog( "HTTP-SOAP: SAML Request $request", 'debug' );
}
}
if ($response) {
# Process authentication response
my $result;
if ($artifact) {
$result = $self->processArtResponseMsg( $login, $response );
}
else {
$result = $self->processAuthnResponseMsg( $login, $response );
}
unless ($result) {
2010-03-01 17:14:13 +01:00
$self->lmLog( "SSO: Fail to process authentication response",
'error' );
return PE_ERROR;
}
2010-03-01 17:14:13 +01:00
$self->lmLog( "SSO: authentication response is valid", 'debug' );
2010-03-01 11:45:04 +01:00
# Get SAML response
my $saml_response = $login->response();
unless ($saml_response) {
$self->lmLog( "No SAML response found", 'error' );
return PE_ERROR;
}
2010-02-15 18:03:07 +01:00
2010-03-01 11:45:04 +01:00
# 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) ) {
2010-02-15 14:44:06 +01:00
2010-03-01 11:45:04 +01:00
# Assertion was already consumed or is expired
# Force authentication replay
$self->lmLog(
"Message $assertion_responded already used or expired, replay authentication",
2010-03-01 11:45:04 +01:00
'error'
);
delete $self->{urldc};
$self->{mustRedirect} = 1;
$self->{error} = $self->_subProcess(qw(autoRedirect));
return $self->{error};
}
2010-02-15 14:44:06 +01:00
}
2010-03-01 11:45:04 +01:00
else {
$self->lmLog(
2010-02-15 14:44:06 +01:00
"Assertion is not a response to a created authentication request, do not control replay",
2010-03-01 11:45:04 +01:00
'debug'
);
}
2010-03-01 11:45:04 +01:00
# Get SAML assertion
my $assertion = $self->getAssertion($login);
2010-02-15 18:03:07 +01:00
2010-03-01 11:45:04 +01:00
unless ($assertion) {
$self->lmLog( "No assertion found", 'error' );
return PE_ERROR;
}
2010-02-15 18:03:07 +01:00
2010-03-01 11:45:04 +01:00
# Check conditions - time and audience
unless (
$self->validateConditions( $assertion, $self->{samlEntityID} ) )
{
$self->lmLog( "Conditions not validated", 'error' );
2010-03-01 11:45:04 +01:00
return PE_ERROR;
}
2010-02-15 18:03:07 +01:00
2010-03-01 11:45:04 +01:00
# Check OneTimeUse flag
my $oneTimeUse = $assertion->Conditions()->OneTimeUse();
2010-02-15 18:03:07 +01:00
if ($oneTimeUse) {
$self->lmLog( "Found oneTimeUse flag in assertion conditions",
'debug' );
# Set a small cookie duration
$self->{cookieExpiration} = "+1m";
}
2010-03-01 11:45:04 +01:00
# Extract RelayState information
if ( $self->extractRelayState($relaystate) ) {
$self->lmLog( "RelayState $relaystate extracted", 'debug' );
2010-03-01 11:45:04 +01:00
}
# Update IDP from RelayState
if ( $self->{_idp} ) {
$idp = $self->{_idp};
2010-03-01 11:45:04 +01:00
$self->lmLog( "IDP $idp found in RelayState", 'debug' );
}
else {
2010-03-01 11:45:04 +01:00
$self->lmLog( "IDP was not found in RelayState", 'debug' );
2010-03-01 11:45:04 +01:00
}
unless ($idp) {
2010-03-01 11:45:04 +01:00
$self->lmLog(
"IDP was not found in RelayState or in IDP resolution cookie",
2010-03-01 11:45:04 +01:00
'error'
);
return PE_ERROR;
}
2010-03-24 14:01:14 +01:00
# Check if we accept direct login from IDP
my $allowLoginFromIDP =
$self->{samlIDPMetaDataOptions}->{$idp}
->{samlIDPMetaDataOptionsAllowLoginFromIDP};
if ( !$assertion_responded and !$allowLoginFromIDP ) {
$self->lmLog( "Direct login from IDP $idp is not allowed",
'error' );
return PE_ERROR;
}
2010-04-01 18:32:51 +02:00
# Check authentication context
my $requestedAuthnContext =
$self->{samlIDPMetaDataOptions}->{$idp}
->{samlIDPMetaDataOptionsRequestedAuthnContext};
$requestedAuthnContext =
$self->getAuthnContext($requestedAuthnContext)
if $requestedAuthnContext;
if ($requestedAuthnContext) {
$self->lmLog(
"Requested authentication context: $requestedAuthnContext",
'debug'
);
my $responseAuthnContext;
eval {
$responseAuthnContext =
$assertion->AuthnStatement()->AuthnContext()
->AuthnContextClassRef();
};
if ($@) {
$self->lmLog(
"Unable to get authentication context from $idp",
'error' );
return PE_ERROR;
}
$self->lmLog(
"Found authentication context: $responseAuthnContext",
'debug' );
if ( $responseAuthnContext ne $requestedAuthnContext ) {
$self->lmLog(
"Bad authentication context ($responseAuthnContext instead of $requestedAuthnContext)",
'error'
);
return PE_ERROR;
}
}
2010-03-01 11:45:04 +01:00
# Force redirection to portal if no urldc found
# (avoid displaying the whole SAML URL in user browser URL field)
$self->{mustRedirect} = 1 unless ( $self->{urldc} );
2010-03-01 11:45:04 +01:00
# Get NameID
my $nameid = $login->nameIdentifier;
2010-02-09 21:49:23 +01:00
2010-03-01 11:45:04 +01:00
# Set user
my $user = $nameid->content;
2010-02-09 21:49:23 +01:00
2010-03-01 11:45:04 +01:00
unless ($user) {
$self->lmLog( "No NameID value found", 'error' );
return PE_USERNOTFOUND;
}
2010-03-01 11:45:04 +01:00
$self->lmLog( "Find NameID: $user", 'debug' );
$self->{user} = $user;
2010-02-09 21:49:23 +01:00
2010-03-01 11:45:04 +01:00
# Store Lasso objects
$self->{_lassoLogin} = $login;
2010-02-09 21:49:23 +01:00
2010-03-01 11:45:04 +01:00
return PE_OK;
}
elsif ($request) {
# Do nothing
$self->lmLog(
"This module do not manage SSO request, see IssuerDBSAML",
'debug' );
return PE_OK;
}
else {
# This should not happen
$self->lmLog( "SSO request or response was not found", 'error' );
# Redirect user
$self->{mustRedirect} = 1;
$self->{error} = $self->_subProcess(qw(autoRedirect));
return $self->{error};
}
}
2010-02-08 11:06:21 +01:00
2010-02-18 18:22:04 +01:00
# 1.2 SLO
if ( $url =~
/^($saml_slo_soap_url|$saml_slo_soap_url_ret|$saml_slo_get_url|$saml_slo_get_url_ret)$/i
)
{
$self->lmLog( "URL $url detected as an SLO URL", 'debug' );
# Create Logout object
$logout = $self->createLogout($server);
# Do we check signature?
if ($idp) {
my $checkSLOMessageSignature =
$self->{samlIDPMetaDataOptions}->{$idp}
->{samlIDPMetaDataOptionsCheckSLOMessageSignature};
unless ($checkSLOMessageSignature) {
$self->lmLog(
"SLO message signature from IDP $idp will not be checked",
'debug' );
$self->disableSignature($logout);
}
}
else {
$self->lmLog(
"Will not check checkSLOMessageSignature option because no IDP was found",
'debug'
);
}
2010-02-26 10:12:18 +01:00
# Get relayState
$relaystate = $self->param('RelayState');
2010-02-18 18:22:04 +01:00
# 1.2.1 HTTP-REDIRECT
if ( $request_method =~ /^GET$/ ) {
2010-02-18 18:22:04 +01:00
$method = Lasso::Constants::HTTP_METHOD_REDIRECT;
$self->lmLog( "SLO method: HTTP-REDIRECT", 'debug' );
if ( $self->param('SAMLResponse') ) {
# Response in query string
$response = $self->query_string();
$self->lmLog( "HTTP-REDIRECT: SAML Response $response",
'debug' );
}
if ( $self->param('SAMLRequest') ) {
# Request in query string
$request = $self->query_string();
2010-02-18 18:22:04 +01:00
$self->lmLog( "HTTP-REDIRECT: SAML Request $request", 'debug' );
}
}
elsif ( $request_method =~ /^POST$/ ) {
2010-02-18 18:22:04 +01:00
# 1.2.2 POST
if ( $content_type !~ /xml/ ) {
2010-02-18 18:22:04 +01:00
$method = Lasso::Constants::HTTP_METHOD_POST;
$self->lmLog( "SLO method: HTTP-POST", 'debug' );
if ( $self->param('SAMLResponse') ) {
# Response in body part
$response = $self->param('SAMLResponse');
$self->lmLog( "HTTP-POST: SAML Response $response",
'debug' );
}
if ( $self->param('SAMLRequest') ) {
# Request in body part
$request = $self->param('SAMLRequest');
2010-02-18 18:22:04 +01:00
$self->lmLog( "HTTP-POST: SAML Request $request", 'debug' );
}
}
# 1.2.3 SOAP
else {
$method = Lasso::Constants::HTTP_METHOD_SOAP;
$self->lmLog( "SLO method: HTTP-SOAP", 'debug' );
# SOAP is always a request
$request = $self->param('POSTDATA');
$self->lmLog( "HTTP-SOAP: SAML Request $request", 'debug' );
}
}
if ($response) {
# Process logout response
my $result = $self->processLogoutResponseMsg( $logout, $response );
unless ($result) {
$self->lmLog( "Fail to process logout response", 'error' );
return PE_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_ERROR;
}
2010-02-18 18:22:04 +01:00
# If URL in RelayState, different from portal, redirect user
if ( $self->extractRelayState($relaystate) ) {
$self->lmLog( "RelayState $relaystate extracted", 'debug' );
$self->lmLog( "URL " . $self->{urldc} . " found in RelayState",
'debug' );
}
$self->_subProcess(qw(autoRedirect))
if ( $self->{urldc} and $self->{urldc} ne $self->{portal} );
# Else, inform user that logout is OK
2010-02-18 18:22:04 +01:00
return PE_LOGOUT_OK;
}
elsif ($request) {
# Logout error
my $logout_error = 0;
# Lasso::Session dump
my $session_dump;
# Process logout request
unless ( $self->processLogoutRequestMsg( $logout, $request ) ) {
$self->lmLog( "Fail to process logout request", 'error' );
$logout_error = 1;
2010-02-18 18:22:04 +01:00
}
$self->lmLog( "Logout request is valid", 'debug' );
# Get NameID and SessionIndex
my $name_id = $logout->request()->NameID;
my $session_index = $logout->request()->SessionIndex;
my $user = $name_id->content;
unless ($user) {
$self->lmLog( "Fail to get NameID content from logout request",
'error' );
$logout_error = 1;
2010-02-18 18:22:04 +01:00
}
$self->lmLog( "Logout request NameID content: $user", 'debug' );
# Get corresponding session
my $local_sessions =
$self->{globalStorage}
->searchOn( $self->{globalStorageOptions}, "_user", $user, );
if ( my @local_sessions_keys = keys %$local_sessions ) {
# A session was found
foreach (@local_sessions_keys) {
my $local_session = $_;
# Get session
$self->lmLog(
"Retrieve session $local_session for user $user",
'debug' );
my $sessionInfo =
$self->getApacheSession( $local_session, 1 );
# Get Lasso::Session dump
$session_dump = $sessionInfo->{_lassoSessionDump}
if $sessionInfo->{_lassoSessionDump};
# Delete Session
$self->lmLog(
"Delete session $local_session for user $user",
'debug' );
my $logout_result = $self->_deleteSession($sessionInfo);
$self->lmLog( "Local Logout result: $logout_result",
'debug' );
$logout_error = 1 unless $logout_result;
}
# Set session from dump
unless ( $self->setSessionFromDump( $logout, $session_dump ) ) {
$self->lmLog( "Cannot set session from dump in logout",
'error' );
$logout_error = 1;
}
2010-02-18 18:42:31 +01:00
2010-02-18 18:22:04 +01:00
}
else {
# No corresponding session found
$self->lmLog( "No local session found for user $user",
'debug' );
$logout_error = 1;
2010-02-18 18:22:04 +01:00
}
# Validate request if no previous error
unless ($logout_error) {
unless ( $self->validateLogoutRequest($logout) ) {
$self->lmLog( "SLO request is not valid", 'error' );
}
}
# Set RelayState
if ($relaystate) {
$logout->msg_relayState($relaystate);
$self->lmLog( "Set $relaystate in RelayState", 'debug' );
}
# Do we set signature?
if ($idp) {
my $signSLOMessage =
$self->{samlIDPMetaDataOptions}->{$idp}
->{samlIDPMetaDataOptionsSignSLOMessage};
unless ($signSLOMessage) {
$self->lmLog( "SLO message to IDP $idp will not be signed",
'debug' );
$self->disableSignature($logout);
}
else {
# Force signature here because it
# can have been disabled before
$self->forceSignature($logout);
}
}
else {
$self->lmLog(
"Will not check signSLOMessage option because no IDP was found",
'debug'
);
}
2010-02-18 18:22:04 +01:00
# Logout response
unless ( $self->buildLogoutResponseMsg($logout) ) {
$self->lmLog( "Unable to build SLO response", 'error' );
return PE_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;
$self->lmLog( "Redirect user to $slo_url", 'debug' );
$self->{urldc} = $slo_url;
2010-02-26 10:12:18 +01:00
$self->_subProcess(qw(autoRedirect));
# If we are here, there was a problem with GET request
$self->lmLog( "Logout response was not sent trough GET",
'error' );
return PE_ERROR;
}
# HTTP-POST
if ( $method == Lasso::Constants::HTTP_METHOD_POST ) {
# Use autosubmit form
my $slo_url = $logout->msg_url;
my $slo_body = $logout->msg_body;
$self->{postUrl} = $slo_url;
$self->{postFields} = { 'SAMLResponse' => $slo_body };
2010-02-26 10:12:18 +01:00
# RelayState
$self->{postFields} =
{ $self->{postFields}, 'RelayState' => $relaystate }
if ($relaystate);
$self->_subProcess(qw(autoPost));
# If we are here, there was a problem with POST response
$self->lmLog( "Logout response was not sent trough POST",
'error' );
return PE_ERROR;
}
# HTTP-SOAP
if ( $method == Lasso::Constants::HTTP_METHOD_SOAP ) {
my $slo_body = $logout->msg_body;
$self->lmLog( "SOAP response $slo_body", 'debug' );
$self->{SOAPMessage} = $slo_body;
$self->_subProcess(qw(returnSOAPMessage));
# If we are here, there was a problem with SOAP response
$self->lmLog( "Logout response was not sent trough SOAP",
'error' );
return PE_ERROR;
}
2010-02-18 18:22:04 +01:00
}
else {
# This should not happen
$self->lmLog( "SLO request or response was not found", 'error' );
# Redirect user
$self->{mustRedirect} = 1;
$self->{error} = $self->_subProcess(qw(autoRedirect));
return $self->{error};
}
}
# 1.3 Artefact
if ( $url =~ /^($saml_ars_url)$/i ) {
$self->lmLog( "URL $url detected as an artefact resolution service URL",
'debug' );
}
2010-02-04 13:30:18 +01:00
# 2. IDP resolution
2010-02-08 11:06:21 +01:00
2010-02-04 13:30:18 +01:00
# If no IDP resolve cookie, find another way to get it
# Case 1: IDP was choosen from portal IDP list
$idp ||= $self->param("idp");
2010-03-05 16:37:16 +01:00
# Case 2: check all IDP resolution rules
# The first match win
unless ($idp) {
foreach ( keys %{ $self->{_idpList} } ) {
my $cond =
$self->{samlIDPMetaDataOptions}->{$_}
->{samlIDPMetaDataOptionsResolutionRule};
next unless defined $cond;
if ( $self->safe->reval($cond) ) {
$self->lmLog( "IDP $_ resolution rule match", 'debug' );
$idp = $_;
last;
}
}
}
2010-02-04 17:02:02 +01:00
2010-02-04 13:30:18 +01:00
# Get confirmation flag
my $confirm_flag = $self->param("confirm");
# If confirmation is -1, or IDP was not resolve, let the user choose its IDP
if ( $confirm_flag == -1 or !$idp ) {
$self->lmLog( "No IDP found, redirecting user to IDP list", 'debug' );
# IDP list
2010-02-08 11:06:21 +01:00
my $html = "<h3>"
. &Lemonldap::NG::Portal::_i18n::msg( PM_SAML_IDPSELECT,
$ENV{HTTP_ACCEPT_LANGUAGE} )
. "</h3>\n<table>\n";
2010-02-04 13:30:18 +01:00
foreach ( keys %{ $self->{_idpList} } ) {
$html .=
2010-03-01 17:14:13 +01:00
'<tr><td><input type="radio" name="idp" onclick="stop()" value="'
. $_
2010-02-08 11:06:21 +01:00
. '" /></td><td>'
. $self->{_idpList}->{$_}->{name}
. '</td></tr>';
2010-02-04 13:30:18 +01:00
}
$html .=
2010-02-08 11:06:21 +01:00
'<tr><td><input type="checkbox" name="cookie_type" value="1"></td><td>'
. &Lemonldap::NG::Portal::_i18n::msg( PM_REMEMBERCHOICE,
$ENV{HTTP_ACCEPT_LANGUAGE} )
. "</td></tr></table>\n"
2010-02-08 11:06:21 +01:00
# Script to autoselect first choice
. '<script>$("[type=radio]:first").attr("checked","checked");</script>';
2010-02-04 13:30:18 +01:00
$self->info($html);
# Delete existing IDP resolution cookie
push @{ $self->{cookie} },
$self->cookie(
-name => $self->{samlIdPResolveCookie},
-value => 0,
-domain => $self->{domain},
-path => "/",
-secure => 0,
-expires => '-1d',
);
2010-02-08 11:06:21 +01:00
return PE_CONFIRM;
2010-02-04 13:30:18 +01:00
}
2010-02-08 11:06:21 +01:00
2010-02-04 13:30:18 +01:00
# If IDP is found but not confirmed, let the user confirm it
if ( $confirm_flag != 1 ) {
$self->lmLog( "IDP $idp selected, need user confirmation", 'debug' );
# Choosen IDP
2010-02-08 11:06:21 +01:00
my $html = '<h3>'
. &Lemonldap::NG::Portal::_i18n::msg( PM_SAML_IDPCHOOSEN,
$ENV{HTTP_ACCEPT_LANGUAGE} )
. "</h3>\n" . "<h4>"
. $self->{_idpList}->{$idp}->{name}
. "</h4>\n"
. "<p><i>"
. $self->{_idpList}->{$idp}->{entityID}
. "</i></p>\n"
. "<input type=\"hidden\" name=\"idp\" value=\"$idp\" />\n";
2010-02-04 13:30:18 +01:00
$self->info($html);
2010-02-08 11:06:21 +01:00
return PE_CONFIRM;
2010-02-04 13:30:18 +01:00
}
# Here confirmation is OK (confirm_flag == 1), store choosen IDP in cookie
unless ( $idp_cookie and ( $idp eq $idp_cookie->value ) ) {
$self->lmLog( "Build cookie to remember $idp as IDP choice", 'debug' );
2010-02-08 11:06:21 +01:00
# User can choose temporary (0) or persistent cookie (1)
my $cookie_type = $self->param("cookie_type") || "0";
push @{ $self->{cookie} },
$self->cookie(
-name => $self->{samlIdPResolveCookie},
-value => $idp,
-domain => $self->{domain},
-path => "/",
-secure => $self->{securedCookie},
-httponly => $self->{httpOnly},
-expires => $cookie_type ? "+365d" : "",
);
2010-02-04 13:30:18 +01:00
}
2010-02-08 11:06:21 +01:00
2010-02-04 13:30:18 +01:00
# 3. Build authentication request
2010-02-26 10:12:18 +01:00
# IDP entityID
$self->{_idp} = $idp;
my $IDPentityID = $self->{_idpList}->{$idp}->{entityID};
2010-02-26 10:12:18 +01:00
# IDP ForceAuthn
2010-03-05 09:54:01 +01:00
my $forceAuthn =
$self->{samlIDPMetaDataOptions}->{$idp}
->{samlIDPMetaDataOptionsForceAuthn};
2010-04-01 16:40:29 +02:00
# IDP IsPassive
my $isPassive =
$self->{samlIDPMetaDataOptions}->{$idp}
->{samlIDPMetaDataOptionsIsPassive};
# IDP NameIDFormat
2010-03-05 10:28:28 +01:00
my $nameIDFormat =
$self->{samlIDPMetaDataOptions}->{$idp}
->{samlIDPMetaDataOptionsNameIDFormat};
$nameIDFormat = $self->getNameIDFormat($nameIDFormat) if $nameIDFormat;
2010-03-05 09:54:01 +01:00
# IDP ProxyRestriction
my $allowProxiedAuthn =
$self->{samlIDPMetaDataOptions}->{$idp}
->{samlIDPMetaDataOptionsAllowProxiedAuthn};
# IDP HTTP method
$method =
$self->{samlIDPMetaDataOptions}->{$idp}
->{samlIDPMetaDataOptionsSSOBinding};
$method = $self->getHttpMethod($method) if $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( $server, $IDPentityID, $protocolType );
}
# Failback to HTTP-REDIRECT
unless ( defined $method and $method != -1 ) {
$self->lmLog( "No method found with IDP $idp for SSO profile",
'debug' );
$method = $self->getHttpMethod("redirect");
}
$self->lmLog( "Use method $method with IDP $idp for SSO profile", 'debug' );
# Set signature
my $signSSOMessage =
2010-04-01 11:55:33 +02:00
$self->{samlIDPMetaDataOptions}->{$idp}
->{samlIDPMetaDataOptionsSignSSOMessage};
2010-04-01 11:55:33 +02:00
2010-04-01 18:32:51 +02:00
# Authentication Context
my $requestedAuthnContext =
$self->{samlIDPMetaDataOptions}->{$idp}
->{samlIDPMetaDataOptionsRequestedAuthnContext};
$requestedAuthnContext = $self->getAuthnContext($requestedAuthnContext)
if $requestedAuthnContext;
2010-02-26 10:12:18 +01:00
# Create SSO request
2010-04-01 18:32:51 +02:00
$login = $self->createAuthnRequest(
$server, $IDPentityID, $method,
$forceAuthn, $isPassive, $nameIDFormat,
$allowProxiedAuthn, $signSSOMessage, $requestedAuthnContext
);
2010-02-04 17:02:02 +01:00
unless ($login) {
$self->lmLog( "Could not create authentication request on $IDPentityID",
2010-02-04 17:02:02 +01:00
'error' );
return PE_ERROR;
}
2010-02-04 17:02:02 +01:00
$self->lmLog( "Authentication request created", 'debug' );
2010-02-04 13:30:18 +01:00
2010-02-15 14:44:06 +01:00
# Keep assertion ID in memory to prevent replay
unless ( $self->storeReplayProtection( $login->request()->ID ) ) {
2010-02-15 14:44:06 +01:00
$self->lmLog( "Unable to store assertion ID", 'error' );
return PE_ERROR;
}
2010-02-26 10:12:18 +01:00
# Send SSO request depending on request method
# HTTP-REDIRECT
if ( $method == Lasso::Constants::HTTP_METHOD_REDIRECT ) {
# Redirect user to response URL
2010-03-01 11:45:04 +01:00
my $sso_url = $login->msg_url;
$self->lmLog( "Redirect user to $sso_url", 'debug' );
2010-03-01 11:45:04 +01:00
$self->{urldc} = $sso_url;
2010-02-26 10:12:18 +01:00
$self->_subProcess(qw(autoRedirect));
# If we are here, there was a problem with GET request
$self->lmLog( "SSO request was not sent trough GET", 'error' );
return PE_ERROR;
}
# HTTP-POST
if ( $method == Lasso::Constants::HTTP_METHOD_POST ) {
# Use autosubmit form
my $sso_url = $login->msg_url;
my $sso_body = $login->msg_body;
$self->{postUrl} = $sso_url;
$self->{postFields} = { 'SAMLRequest' => $sso_body };
# RelayState
$self->{postFields} =
{ $self->{postFields}, 'RelayState' => $login->msg_relayState }
if ( $login->msg_relayState );
$self->_subProcess(qw(autoPost));
# If we are here, there was a problem with POST request
$self->lmLog( "SSO request was not sent trough POST", 'error' );
return PE_ERROR;
}
# No SOAP transport for SSO request
2009-04-07 22:38:24 +02:00
}
## @apmethod int setAuthSessionInfo()
# Extract attributes sent in authentication statement
2009-04-07 22:38:24 +02:00
# @return Lemonldap::NG::Portal error code
sub setAuthSessionInfo {
my $self = shift;
my $server = $self->{_lassoServer};
my $login = $self->{_lassoLogin};
my $idp = $self->{_idp};
# Get SAML assertion
my $assertion = $self->getAssertion($login);
unless ($assertion) {
$self->lmLog( "No assertion found", 'error' );
return PE_ERROR;
}
# 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->{samlIDPMetaDataExportedAttributes}->{$idp} } )
2010-02-17 18:37:38 +01:00
{
# Extract fields from exportedAttr value
my ( $mandatory, $name, $format, $friendly_name ) =
split( /;/,
$self->{samlIDPMetaDataExportedAttributes}->{$idp}->{$_} );
# Try to get value
my $value =
$self->getAttributeValue( $name, $format, $friendly_name,
\@attributes );
# Store value in sessionInfo
$self->{sessionInfo}->{$_} = $value if defined $value;
}
}
# Store other informations in session
$self->{sessionInfo}->{_user} = $self->{user};
$self->{sessionInfo}->{_idp} = $idp;
$self->{sessionInfo}->{_idpEntityID} =
$self->{_idpList}->{$idp}->{entityID};
# Adapt _utime with SessionNotOnOrAfter
my $sessionNotOnOrAfter =
$assertion->AuthnStatement()->SessionNotOnOrAfter();
my ( $year, $mon, $mday, $hour, $min, $sec, $ztime ) =
( $sessionNotOnOrAfter =~
/(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(Z)?/ );
my $samltime = mktime( $sec, $min, $hour, $mday, $mon - 1, $year - 1900 );
$self->lmLog(
"Convert SessionNotOnOrAfter $sessionNotOnOrAfter in timestamp: $samltime",
'debug'
);
my $utime = time();
my $timeout = $self->{timeout};
my $adaptSessionUtime =
$self->{samlIDPMetaDataOptions}->{$idp}
->{samlIDPMetaDataOptionsAdaptSessionUtime};
if ( ( $utime + $timeout > $samltime ) and $adaptSessionUtime ) {
# Use SAML time to determine the start of the session
my $new_utime = $samltime - $timeout;
$self->{sessionInfo}->{_utime} = $new_utime;
$self->lmLog(
"Adapt _utime with SessionNotOnOrAfter value, new _utime: $new_utime",
'debug'
);
}
2010-03-03 17:54:23 +01:00
# Establish federation (required for attribute request in UserDBSAML)
unless ( $self->acceptSSO($login) ) {
$self->lmLog( "Error while accepting SSO from IDP", 'error' );
return PE_ERROR;
}
2010-02-17 18:37:38 +01:00
# Get created Lasso::Session and Lasso::Identity
my $session = $login->get_session;
my $identity = $login->get_identity;
# Dump Lasso objects in session
2010-02-17 18:37:38 +01:00
$self->{sessionInfo}->{_lassoSessionDump} = $session->dump() if $session;
$self->{sessionInfo}->{_lassoIdentityDump} = $identity->dump() if $identity;
2010-03-03 17:54:23 +01:00
$self->{_lassoLogin} = $login;
PE_OK;
}
## @apmethod int authenticate()
# Set authenticationLevel
# @return PE_OK
sub authenticate {
my $self = shift;
# Set authenticationLevel
$self->{sessionInfo}->{authenticationLevel} = 5;
2009-04-07 22:38:24 +02:00
PE_OK;
}
## @apmethod void authLogout()
# Logout SP
# @return nothing
2009-04-07 22:38:24 +02:00
sub authLogout {
my $self = shift;
my $idp = $self->{sessionInfo}->{_idp};
my $IDPentityID = $self->{sessionInfo}->{_idpEntityID};
my $method;
# Get Lasso Server
unless ( $self->{_lassoServer} ) {
$self->_sub('authInit');
}
my $server = $self->{_lassoServer};
2010-02-17 18:37:38 +01:00
# Recover Lasso::Session dump
my $session_dump = $self->{sessionInfo}->{_lassoSessionDump};
unless ($session_dump) {
$self->lmLog( "Could not get session dump from session", 'error' );
return PE_ERROR;
}
# IDP HTTP method
$method =
$self->{samlIDPMetaDataOptions}->{$idp}
->{samlIDPMetaDataOptionsSLOBinding};
$method = $self->getHttpMethod($method) if $method;
# If no method defined, get first HTTP method
unless ( defined $method ) {
my $protocolType = Lasso::Constants::MD_PROTOCOL_TYPE_SINGLE_LOGOUT;
$method =
$self->getFirstHttpMethod( $server, $IDPentityID, $protocolType );
}
# Failback to SOAP
unless ( defined $method and $method != -1 ) {
$self->lmLog( "No method found with IDP $idp for SLO profile",
'debug' );
$method = $self->getHttpMethod("soap");
}
$self->lmLog( "Use method $method with IDP $idp for SLO profile", 'debug' );
2010-02-26 10:12:18 +01:00
# Set signature
my $signSLOMessage =
$self->{samlIDPMetaDataOptions}->{$idp}
->{samlIDPMetaDataOptionsSignSLOMessage};
# Build Logout Request
my $logout =
$self->createLogoutRequest( $server, $session_dump, $method,
$signSLOMessage );
unless ($logout) {
2010-02-17 18:37:38 +01:00
$self->lmLog( "Could not create logout request", 'error' );
return PE_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_ERROR;
}
# Send request depending on request method
# HTTP-REDIRECT
if ( $method == Lasso::Constants::HTTP_METHOD_REDIRECT ) {
# Redirect user to response URL
2010-03-01 11:45:04 +01:00
my $slo_url = $logout->msg_url;
$self->lmLog( "Redirect user to $slo_url", 'debug' );
2010-03-01 11:45:04 +01:00
$self->{urldc} = $slo_url;
# Redirect done in Portal/Simple.pm
return;
}
# HTTP-POST
if ( $method == Lasso::Constants::HTTP_METHOD_POST ) {
# Use autosubmit form
my $slo_url = $logout->msg_url;
my $slo_body = $logout->msg_body;
$self->{postUrl} = $slo_url;
$self->{postFields} = { 'SAMLRequest' => $slo_body };
# Post done in Portal/Simple.pm
return;
}
# HTTP-SOAP
if ( $method == Lasso::Constants::HTTP_METHOD_SOAP ) {
my $slo_url = $logout->msg_url;
my $slo_body = $logout->msg_body;
2010-02-26 10:12:18 +01:00
# Send SOAP request and manage response
my $response = $self->sendSOAPMessage( $slo_url, $slo_body );
2010-03-03 17:54:23 +01:00
unless ($response) {
$self->lmLog( "No logout response to SOAP request", 'error' );
return PE_ERROR;
}
2010-02-26 10:12:18 +01:00
# 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_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_ERROR;
}
2010-03-01 11:45:04 +01:00
return;
}
2009-04-07 22:38:24 +02:00
}
## @apmethod boolean authForce()
# Check if authentication should be forced
# @return nothing
sub authForce {
my $self = shift;
my $url = $self->url();
my $saml_acs_art_url = $self->getMetaDataURL(
"samlSPSSODescriptorAssertionConsumerServiceHTTPArtifact");
my $saml_acs_post_url = $self->getMetaDataURL(
"samlSPSSODescriptorAssertionConsumerServiceHTTPPost");
my $saml_acs_get_url = $self->getMetaDataURL(
"samlSPSSODescriptorAssertionConsumerServiceHTTPRedirect");
my $saml_slo_soap_url =
$self->getMetaDataURL( "samlSPSSODescriptorSingleLogoutServiceSOAP", 1 );
my $saml_slo_soap_url_ret =
$self->getMetaDataURL( "samlSPSSODescriptorSingleLogoutServiceSOAP", 2 );
my $saml_slo_get_url =
$self->getMetaDataURL( "samlSPSSODescriptorSingleLogoutServiceHTTP", 1 );
my $saml_slo_get_url_ret =
$self->getMetaDataURL( "samlSPSSODescriptorSingleLogoutServiceHTTP", 2 );
return 1
if ( $url =~
/^($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)$/
);
return 0;
}
2009-04-07 22:38:24 +02:00
1;
2009-04-07 22:38:24 +02:00
__END__
=head1 NAME
=encoding utf8
2010-02-04 13:30:18 +01:00
Lemonldap::NG::Portal::AuthSAML - SAML Authentication backend
2009-04-07 22:38:24 +02:00
=head1 SYNOPSIS
use Lemonldap::NG::Portal::AuthSAML;
=head1 DESCRIPTION
2010-02-04 13:30:18 +01:00
Use SAML to authenticate users
2009-04-07 22:38:24 +02:00
=head1 SEE ALSO
2010-02-04 13:30:18 +01:00
L<Lemonldap::NG::Portal>, L<Lemonldap::NG::Portal::UserDBSAML>, L<Lemonldap::NG::Portal::_SAML>
2009-04-07 22:38:24 +02:00
=head1 AUTHOR
2010-02-04 13:30:18 +01:00
Xavier Guimard, E<lt>x.guimard@free.frE<gt>, Clement Oudot, E<lt>coudot@linagora.comE<gt>
2009-04-07 22:38:24 +02:00
=head1 COPYRIGHT AND LICENSE
Copyright (C) 2009 by Xavier Guimard
This library is free software; you can redistribute it and/or modify
it under the same terms as Perl itself, either Perl version 5.10.0 or,
at your option, any later version of Perl 5 you may have available.
=cut