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

634 lines
19 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;
2009-04-07 22:38:24 +02:00
our $VERSION = '0.1';
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 Lasso
return PE_ERROR unless $self->loadLasso();
2010-01-29 18:33:35 +01:00
# Activate SOAP
$self->abort( 'To use SAML, you must activate SOAP (Soap => 1)', 'error' )
2010-02-08 11:06:21 +01:00
unless ( $self->{Soap} );
2010-01-29 18:33:35 +01:00
2010-02-05 17:14:05 +01:00
# Check presence of private key in configuration
unless ( $self->{samlServicePrivateKey} ) {
$self->lmLog( "SAML private key not found in configuration", 'error' );
2010-02-08 11:06:21 +01:00
return PE_ERROR;
}
2010-02-01 18:07:40 +01:00
2010-02-08 11:06:21 +01:00
# Get metadata from configuration
$self->lmLog( "Get Metadata for this service", 'debug' );
2010-02-04 13:30:18 +01:00
my $service_metadata = Lemonldap::NG::Common::Conf::SAML::Metadata->new();
2010-02-01 18:07:40 +01:00
2010-02-08 11:06:21 +01:00
# Create Lasso server with service metadata
my $server = $self->createServer(
2010-02-05 17:14:05 +01:00
$service_metadata->serviceToXML(
$ENV{DOCUMENT_ROOT} . "/skins/common/saml2-metadata.tpl", $self
),
2010-02-08 11:06:21 +01:00
$self->{samlServicePrivateKey},
);
2010-02-01 18:07:40 +01:00
2010-02-08 11:06:21 +01:00
unless ($server) {
$self->lmLog( 'Unable to create Lasso server', 'error' );
return PE_ERROR;
}
$self->lmLog( "Service created", 'debug' );
2010-02-01 18:07:40 +01:00
2010-02-08 11:06:21 +01:00
# Check presence of at least one identity provider in configuration
unless ( $self->{samlIDPMetaData}
and keys %{ $self->{samlIDPMetaData} } )
{
$self->lmLog( "No IDP found in configuration", 'error' );
return PE_ERROR;
}
# Load identity provider metadata
# IDP are listed in $self->{samlIDPMetaData}
# Each key is the IDP name
2010-02-08 11:06:21 +01:00
# Build IDP list for later use in extractFormInfo
foreach ( keys %{ $self->{samlIDPMetaData} } ) {
2010-02-04 13:30:18 +01:00
2010-02-08 11:06:21 +01:00
$self->lmLog( "Get Metadata for IDP $_", 'debug' );
# Get metadata from configuration
my $idp_metadata = Lemonldap::NG::Common::Conf::SAML::Metadata->new();
unless (
$idp_metadata->initializeFromConfHash(
$self->{samlIDPMetaData}->{$_}->{samlIDPMetaDataXML}
2010-02-08 11:06:21 +01:00
)
)
2010-02-04 13:30:18 +01:00
{
2010-02-08 11:06:21 +01:00
$self->lmLog( "Fail to read IDP $_ Metadata from configuration",
'error' );
2010-02-04 13:30:18 +01:00
return PE_ERROR;
}
2010-02-08 11:06:21 +01:00
# Add this IDP to Lasso::Server
my $result = $self->addIDP( $server, $idp_metadata->toXML() );
2010-02-04 13:30:18 +01:00
2010-02-08 11:06:21 +01:00
unless ($result) {
$self->lmLog( "Fail to use IDP $_ Metadata", 'error' );
return PE_ERROR;
}
2010-02-04 13:30:18 +01:00
# Store IDP entityID and Organization Name
my $entityID = $idp_metadata->{entityID};
my $name = $self->getOrganizationName( $server, $entityID )
|| ucfirst($_);
2010-02-04 13:30:18 +01:00
$self->{_idpList}->{$_}->{entityID} = $entityID;
$self->{_idpList}->{$_}->{name} = $name;
2010-02-08 11:06:21 +01:00
$self->lmLog( "IDP $_ added", 'debug' );
}
2010-01-29 11:44:56 +01:00
2010-02-04 17:02:02 +01:00
# Store Lasso::Server object
$self->{_lassoServer} = $server;
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-04 13:30:18 +01:00
my $idp;
2010-02-15 14:44:06 +01:00
my %h;
2010-02-04 13:30:18 +01:00
# 1. Get URL parameters to know if we are receving an assertion
my $url = $self->url();
my $saml_acs_art_url = (
split(
/;/,
$self->{samlSPSSODescriptorAssertionConsumerServiceHTTPArtifact}
)
)[3];
my $saml_acs_post_url = (
split(
/;/, $self->{samlSPSSODescriptorAssertionConsumerServiceHTTPPost}
)
)[3];
my $saml_acs_get_url = (
split(
/;/,
$self->{samlSPSSODescriptorAssertionConsumerServiceHTTPRedirect}
)
)[3];
if ( $url =~ /^($saml_acs_art_url|$saml_acs_post_url|$saml_acs_get_url)$/i )
{
$self->lmLog( "URL $url detected as an assertion consumer URL",
'debug' );
2010-02-09 21:49:23 +01:00
# Create Login object
$login = $self->createLogin($server);
# 1.1 HTTP REDIRECT
if ( $url =~ /^$saml_acs_get_url$/i ) {
# Response in query string
my $response = $self->query_string();
$self->lmLog( "HTTP-REDIRECT: SAML Response $response", 'debug' );
# Process authentication response
my $result = $self->processAuthnResponseMsg( $login, $response );
unless ($result) {
$self->lmLog(
"HTTP-REDIRECT: Fail to process authentication response",
'error' );
return PE_ERROR;
}
$self->lmLog( "HTTP-REDIRECT: authentication response is valid",
'debug' );
}
# 1.2 POST
# TODO
# 1.3 ARTEFACT (SOAP)
# TODO
2010-02-15 18:03:07 +01:00
# Get SAML response
my $saml_response = $login->response();
unless ($saml_response) {
$self->lmLog( "No SAML response found", 'error' );
return PE_ERROR;
}
# Replay protection if this is a response to a created authn request
my $assertion_responded = $saml_response->InResponseTo;
2010-02-15 14:44:06 +01:00
if ($assertion_responded) {
my $assertion_sessions =
$self->{globalStorage}->searchOn( $self->{globalStorageOptions},
"ID", $assertion_responded, );
if ( my @assertion_keys = keys %$assertion_sessions ) {
# A session was found
foreach (@assertion_keys) {
2010-02-17 13:02:11 +01:00
my $assertion_session = $_;
2010-02-15 14:44:06 +01:00
# Delete it
eval {
2010-02-17 13:02:11 +01:00
tie %h, $self->{globalStorage}, $_,
2010-02-15 14:44:06 +01:00
$self->{globalStorageOptions};
};
if ($@) {
$self->lmLog(
2010-02-17 13:02:11 +01:00
"Unable to recover assertion session $assertion_session (assertion ID $assertion_responded)",
2010-02-15 14:44:06 +01:00
'error'
);
return PE_ERROR;
}
eval { tied(%h)->delete(); };
if ($@) {
$self->lmLog(
2010-02-17 13:02:11 +01:00
"Unable to delete assertion session $assertion_session (assertion ID $assertion_responded)",
2010-02-15 14:44:06 +01:00
'error'
);
return PE_ERROR;
}
$self->lmLog(
2010-02-17 13:02:11 +01:00
"Assertion session $assertion_session (assertion ID $assertion_responded) was deleted",
2010-02-15 14:44:06 +01:00
'debug'
);
}
}
else {
# Assertion was already consumed or is expired
# Force authentication replay
$self->lmLog(
"Assertion $assertion_responded already used or expired, replay authentication",
'error'
);
delete $self->{urldc};
$self->{mustRedirect} = 1;
$self->{error} = $self->_subProcess(qw(autoRedirect));
return $self->{error};
}
}
else {
$self->lmLog(
"Assertion is not a response to a created authentication request, do not control replay",
'debug'
);
}
2010-02-15 18:03:07 +01:00
# Get SAML assertion
my $assertion = $self->getAssertion($login);
unless ($assertion) {
$self->lmLog( "No assertion found", 'error' );
return PE_ERROR;
}
# Check conditions - time and audience
2010-02-15 18:03:07 +01:00
unless (
$self->validateConditions( $assertion, $self->{samlEntityID} ) )
{
$self->lmLog( "Time or Audience conditions not validated",
'error' );
2010-02-15 18:03:07 +01:00
return PE_ERROR;
}
$self->lmLog( "Conditions validated", 'debug' );
# Check OneTimeUse flag
# TODO
# Check ProxyRestriction flag
# TODO
# Extract RelayState information
if ( $self->extractRelayState($login) ) {
$self->lmLog( "RelayState found in authentication assertion",
'debug' );
}
# Check IDP from RelayState
my $idp = $self->{_idp};
if ($idp) {
$self->lmLog( "IDP $idp found in RelayState", 'debug' );
}
else {
# 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->{_idp} = $idp;
$self->lmLog( "IDP $idp found in IDP resolution cookie",
'debug' );
}
else {
$self->lmLog(
"IDP was not found in RelayState or in IDP resolution cookie",
'error'
);
return PE_ERROR;
}
}
# 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-02-09 21:49:23 +01:00
# Get NameID
my $nameid = $login->nameIdentifier;
# Set user
my $user = $nameid->content;
unless ($user) {
$self->lmLog( "No NameID value found", 'error' );
return PE_USERNOTFOUND;
}
2010-02-09 21:49:23 +01:00
$self->lmLog( "Find NameID: $user", 'debug' );
$self->{user} = $user;
# Store Lasso::Login object
$self->{_lassoLogin} = $login;
2010-02-09 21:49:23 +01:00
return PE_OK;
}
2010-02-08 11:06:21 +01:00
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
# Get IDP resolution cookie
2010-02-08 11:06:21 +01:00
my %cookies = fetch CGI::Cookie;
2010-02-04 13:30:18 +01:00
my $idp_cookie = $cookies{ $self->{samlIdPResolveCookie} };
if ($idp_cookie) {
2010-02-08 11:06:21 +01:00
$idp = $idp_cookie->value;
2010-02-04 13:30:18 +01:00
$self->lmLog( "IDP $idp found in IDP resolution cookie", 'debug' );
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-02-04 17:02:02 +01:00
# TODO - other case (IP resolution, etc.)
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 .=
'<tr><td><input type="radio" name="idp" 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>$idp</h4>\n" . "<h5>("
. $self->{_idpList}->{$idp}->{entityID}
. ")</h5>\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
$self->{_idp} = $idp;
my $IDPentityID = $self->{_idpList}->{$idp}->{entityID};
$login = $self->createAuthnRequest( $server, $IDPentityID );
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
my $assertion_id = $login->request()->ID;
eval {
tie %h, $self->{globalStorage}, undef, $self->{globalStorageOptions};
};
if ( $@ or !$assertion_id ) {
$self->lmLog( "Unable to store assertion ID", 'error' );
return PE_ERROR;
}
$h{type} = 'assertion'; # Session type
$h{_utime} = time(); # Creation time
$h{ID} = $assertion_id;
my $assertion_session_id = $h{_session_id};
untie %h;
$self->lmLog(
"Keep assertion ID $assertion_id in assertion session $assertion_session_id",
'debug'
);
# Redirect user to IDP SSO URL
# Replace urldc value by SSO URL value and call sub autoRedirect
# TODO Manage other transport (POST, SOAP, ...)
my $sso_url = $login->msg_url;
$self->lmLog( "Redirect user to $sso_url", 'debug' );
$self->{urldc} = $sso_url;
$self->{error} = $self->_subProcess(qw(autoRedirect));
return $self->{error};
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->{samlIDPMetaData}->{$idp}->{samlIDPMetaDataExportedAttr} } ) {
# Extract fields from exportedAttr value
my ( $mandatory, $name, $format, $friendly_name ) =
split( /;/,
$self->{samlIDPMetaData}->{$idp}->{samlIDPMetaDataExportedAttr}->{$_} );
# 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;
# TODO adapt _utime with SessionNotOnOrAfter
2009-04-07 22:38:24 +02:00
PE_OK;
}
## @apmethod int authenticate()
# Accept SSO from IDP
2009-04-07 22:38:24 +02:00
# @return PE_OK
sub authenticate {
my $self = shift;
my $server = $self->{_lassoServer};
my $login = $self->{_lassoLogin};
# Accept SSO
unless ( $self->acceptSSO($login) ) {
$self->lmLog( "Error while accepting SSO from IDP", 'error' );
return PE_ERROR;
}
# Dump Lasso objects in session
$self->{sessionInfo}->{_lassoLoginDump} = $login->dump();
# 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 %h;
# Get Lasso Server
unless ( $self->{_lassoServer} ) {
$self->_sub('authInit');
}
my $server = $self->{_lassoServer};
# Build Logout Request if Logout is initiated by SP
# TODO - test logout initiater
my $idp = $self->{_idp};
my $IDPentityID = $self->{_idpList}->{$idp}->{entityID};
my $logout = $self->createLogoutRequest( $server, $IDPentityID );
unless ($logout) {
$self->lmLog( "Could not create logout request on $IDPentityID",
'error' );
return PE_ERROR;
}
$self->lmLog( "Logout request created", 'debug' );
# Keep request ID in memory to prevent replay
my $logout_request_id = $logout->request()->ID;
eval {
tie %h, $self->{globalStorage}, undef, $self->{globalStorageOptions};
};
if ( $@ or !$logout_request_id ) {
$self->lmLog( "Unable to store Logout request ID", 'error' );
return PE_ERROR;
}
$h{type} = 'assertion'; # Session type
$h{_utime} = time(); # Creation time
$h{ID} = $logout_request_id;
my $logout_request_session_id = $h{_session_id};
untie %h;
$self->lmLog(
"Keep Logout request ID $logout_request_id in assertion session $logout_request_session_id",
'debug'
);
# Redirect user to IDP SLO URL
# Replace urldc value by SLO URL value and call sub autoRedirect
# TODO Manage other transport (POST, SOAP, ...)
my $slo_url = $logout->msg_url;
$self->lmLog( "Redirect user to $slo_url", 'debug' );
$self->{urldc} = $slo_url;
$self->{error} = $self->_subProcess(qw(autoRedirect));
return;
2009-04-07 22:38:24 +02:00
}
1;
__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