#!/usr/bin/perl use strict; use Getopt::Long qw/GetOptionsFromArray/; use Pod::Usage; use Lemonldap::NG::Common::Conf; use LWP::UserAgent; use MIME::Base64; use Config::IniFiles; use XML::LibXML; sub toEntityIDkey { my ( $prefix, $entityID ) = @_; my $entityIDKey = $entityID; $entityIDKey =~ s/^https?:\/\///; $entityIDKey =~ s/[^a-zA-Z0-9]/-/g; $entityIDKey =~ s/-+$//g; return ( $prefix . $entityIDKey ); } # Get configuration from CLI opts and config file # CLI arguments take priority # Multi value arguments are extended instead of replaced sub get_config { my ($array) = @_; my %opts; # Get command line options my $result = GetOptionsFromArray( $array, \%opts, 'metadata|m=s', 'verbose|v', 'help|h', 'spconfprefix|s=s', 'idpconfprefix|i=s', 'remove|r', 'nagios|a', 'ignore-sp=s@', 'ignore-idp=s@', 'dry-run|n', 'configfile|c=s', ); pod2usage(1) if $opts{help}; my $config; if ( my $configfile = $opts{configfile} ) { my %iniconfig; tie %iniconfig, 'Config::IniFiles', ( -file => $configfile, -allowempty => 1 ); for my $section ( keys %iniconfig ) { # Copy tied hash into normal hash $config->{$section} = { %{ $iniconfig{$section} } }; } } # Handle scalar options for my $option ( qw/verbose spconfprefix remove dry-run metadata idpconfprefix nagios/) { if ( defined $opts{$option} ) { $config->{main}->{$option} = $opts{$option}; } } # Handle arrayref options by appending values from file to values from CLI for my $option (qw/ignore-sp ignore-idp/) { my $value_from_cmdline = $opts{$option} || []; my $value_from_conf = $config->{main}->{$option}; if ( ref($value_from_conf) ne "ARRAY" ) { if ($value_from_conf) { $config->{main}->{$option} = [$value_from_conf]; } else { $config->{main}->{$option} = []; } } if ( defined $value_from_cmdline ) { push @{ $config->{main}->{$option} }, @$value_from_cmdline; } } return $config; } #============================================================================== # Default values #============================================================================== # Set here attributes that are declared for your SP in the federation # They will be set as exported attributes for all IDP # my $exportedAttributes = { 'cn' => '0;cn', 'eduPersonPrincipalName' => '0;eduPersonPrincipalName', 'givenName' => '0;givenName', 'surname' => '0;surname', 'displayName' => '0;displayName', 'eduPersonAffiliation' => '0;eduPersonAffiliation', 'eduPersonPrimaryAffiliation' => '0;eduPersonPrimaryAffiliation', 'mail' => '0;mail', 'supannListeRouge' => '0;supannListeRouge', 'supannEtuCursusAnnee' => '0;supannEtuCursusAnnee', }; # Set here options that are applied on all SP from the federation my $spOptions = { 'samlSPMetaDataOptionsCheckSLOMessageSignature' => 1, 'samlSPMetaDataOptionsCheckSSOMessageSignature' => 1, 'samlSPMetaDataOptionsEnableIDPInitiatedURL' => 0, 'samlSPMetaDataOptionsEncryptionMode' => 'none', 'samlSPMetaDataOptionsForceUTF8' => 1, 'samlSPMetaDataOptionsNameIDFormat' => '', 'samlSPMetaDataOptionsNotOnOrAfterTimeout' => 72000, 'samlSPMetaDataOptionsOneTimeUse' => 0, 'samlSPMetaDataOptionsSessionNotOnOrAfterTimeout' => 72000, 'samlSPMetaDataOptionsSignSLOMessage' => 1, 'samlSPMetaDataOptionsSignSSOMessage' => 1 }; # Set here options that are applied on all IDP from the federation my $idpOptions = { 'samlIDPMetaDataOptionsAdaptSessionUtime' => 0, 'samlIDPMetaDataOptionsAllowLoginFromIDP' => 0, 'samlIDPMetaDataOptionsAllowProxiedAuthn' => 0, 'samlIDPMetaDataOptionsCheckAudience' => 1, 'samlIDPMetaDataOptionsCheckSLOMessageSignature' => 1, 'samlIDPMetaDataOptionsCheckSSOMessageSignature' => 1, 'samlIDPMetaDataOptionsCheckTime' => 1, 'samlIDPMetaDataOptionsEncryptionMode' => 'none', 'samlIDPMetaDataOptionsForceAuthn' => 0, 'samlIDPMetaDataOptionsForceUTF8' => 0, 'samlIDPMetaDataOptionsIsPassive' => 0, 'samlIDPMetaDataOptionsNameIDFormat' => 'transient', 'samlIDPMetaDataOptionsRelayStateURL' => 0, 'samlIDPMetaDataOptionsSignSLOMessage' => -1, 'samlIDPMetaDataOptionsSignSSOMessage' => -1, 'samlIDPMetaDataOptionsStoreSAMLToken' => 0 }; #============================================================================== # Main #============================================================================== our $verbose = 0; sub printlog { if ($verbose) { print @_; } } sub printlogerr { if ($verbose) { print STDERR @_; } } sub register_saml_sp { my ( $config, $lastConf, $confKey, $entityID, $partner_metadata, $requestedAttributes ) = @_; my $old_provider = { xml => $lastConf->{samlSPMetaDataXML}->{$confKey}->{samlSPMetaDataXML}, attributes => $lastConf->{samlSPMetaDataExportedAttributes}->{$confKey}, options => $lastConf->{samlSPMetaDataOptions}->{$confKey}, }; # Update metadata $lastConf->{samlSPMetaDataXML}->{$confKey}->{samlSPMetaDataXML} = $partner_metadata; my $requested_attributes_lmconf = _compute_requested_attributes( $config, $entityID, $requestedAttributes ); # Update attributes $lastConf->{samlSPMetaDataExportedAttributes}->{$confKey} = $requested_attributes_lmconf; $lastConf->{samlSPMetaDataOptions}->{$confKey} = _compute_sp_options( $config, $entityID ); # handle eduPersonTargetedID if ( $lastConf->{samlSPMetaDataExportedAttributes}->{$confKey} ->{eduPersonTargetedID} ) { delete $lastConf->{samlSPMetaDataExportedAttributes}->{$confKey} ->{eduPersonTargetedID}; $lastConf->{samlSPMetaDataOptions}->{$confKey} ->{samlSPMetaDataOptionsNameIDFormat} = 'persistent'; } return _provider_equal( $lastConf, $old_provider, $confKey, "SP" ); } sub _compute_idp_options { my ( $config, $entityID ) = @_; return _compute_options( $config, $entityID, "samlIDPMetaDataOptions", $idpOptions ); } sub _compute_sp_options { my ( $config, $entityID ) = @_; return _compute_options( $config, $entityID, "samlSPMetaDataOptions", $spOptions ); } sub _compute_options { my ( $config, $entityID, $option_prefix, $defaultOptions ) = @_; my $provider_options = {%$defaultOptions}; my $global_provider_options = $config->{"ALL"} || {}; for my $key ( grep /^$option_prefix/, keys %$global_provider_options ) { $provider_options->{$key} = $global_provider_options->{$key}; } my $entityid_provider_options = $config->{$entityID} || {}; for my $key ( grep /^$option_prefix/, keys %$entityid_provider_options ) { $provider_options->{$key} = $entityid_provider_options->{$key}; } return $provider_options; } sub _compute_requested_attributes { my ( $config, $entityID, $requestedAttributes ) = @_; my $result = {}; for my $name ( keys %$requestedAttributes ) { my $attrconf = $requestedAttributes->{$name}; my $is_required = $config->{"$entityID"}->{"attribute_required_$name"} // $config->{"$entityID"}->{"attribute_required"} // $config->{"ALL"}->{"attribute_required_$name"} // $config->{"ALL"}->{"attribute_required"} // $attrconf->{required}; my $req_attr_str = join( ';', ( $is_required // 0 ), $attrconf->{name}, ( $attrconf->{name_format} || '' ), ( $attrconf->{friendly_name} || '' ), ); $result->{$name} = $req_attr_str; } return $result; } sub register_saml_idp { my ( $config, $lastConf, $confKey, $entityID, $partner_metadata ) = @_; my $old_provider = { xml => $lastConf->{samlIDPMetaDataXML}->{$confKey}->{samlIDPMetaDataXML}, attributes => $lastConf->{samlIDPMetaDataExportedAttributes}->{$confKey}, options => $lastConf->{samlIDPMetaDataOptions}->{$confKey}, }; # Set Metadata $lastConf->{samlIDPMetaDataXML}->{$confKey}->{samlIDPMetaDataXML} = $partner_metadata; # Set attributes $lastConf->{samlIDPMetaDataExportedAttributes}->{$confKey} = _compute_idp_exported_attributes( $config, $entityID ); # Set options $lastConf->{samlIDPMetaDataOptions}->{$confKey} = _compute_idp_options( $config, $entityID ); return _provider_equal( $lastConf, $old_provider, $confKey, "IDP" ); } sub _compute_idp_exported_attributes { my ( $config, $entityID ) = @_; my $result; # Use default exported attributes from config or from hardcoded list if ( $config->{exportedAttributes} ) { $result = { %{ $config->{exportedAttributes} } }; } else { $result = {%$exportedAttributes}; } # Override according to configuration my $idp_config = $config->{"$entityID"} || {}; for my $key ( grep /^exported_attribute_/, keys(%$idp_config) ) { my ($attribute_name) = $key =~ m/exported_attribute_(.*)$/; $result->{$attribute_name} = $idp_config->{$key}; } return $result; } sub _provider_equal { my ( $lastConf, $old_provider, $confKey, $type ) = @_; my $xml_equal = $old_provider->{xml} eq $lastConf->{"saml${type}MetaDataXML"}->{$confKey} ->{"saml${type}MetaDataXML"}; my $attributes_equal = _hash_equal( $old_provider->{attributes}, $lastConf->{"saml${type}MetaDataExportedAttributes"}->{$confKey} ); my $options_equal = _hash_equal( $old_provider->{options}, $lastConf->{"saml${type}MetaDataOptions"}->{$confKey} ); return ( $xml_equal and $attributes_equal and $options_equal ); } sub _hash_equal { my ( $hash1, $hash2 ) = @_; return 0 unless $hash1; return 0 unless $hash2; return 0 unless ( %$hash1 == %$hash2 ); for my $key ( keys %$hash1 ) { return 0 unless ( $hash1->{$key} || '' ) eq ( $hash2->{$key} || '' ); } return 1; } sub transform_config { my ( $config, $lastConf, $xml_metadata ) = @_; my $spConfKeyPrefix = $config->{main}->{spconfprefix} || "sp-"; my $idpConfKeyPrefix = $config->{main}->{idpconfprefix} || "idp-"; # BlockList initialisation my @spIgnorelist = @{ $config->{main}->{'ignore-sp'} || [] }; my @idpIgnorelist = @{ $config->{main}->{'ignore-idp'} || [] }; # Stats initialization my $idpCounter = { 'found' => 0, 'updated' => 0, 'created' => 0, 'rejected' => 0, 'removed' => 0, 'ignored' => 0 }; my $spCounter = { 'found' => 0, 'updated' => 0, 'created' => 0, 'rejected' => 0, 'removed' => 0, 'ignored' => 0, }; # IDP and SP lists my ( $allIdpList, $allSpList, $mdIdpList, $mdSpList, $matchingIdpList, $matchingSpList ); # List current SAML partners foreach my $spConfKey ( keys %{ $lastConf->{samlSPMetaDataXML} } ) { my ( $tmp, $entityID ) = ( $lastConf->{samlSPMetaDataXML}->{$spConfKey}->{samlSPMetaDataXML} =~ /entityID=(['"])(.+?)\1/si ); $allSpList->{$entityID} = $spConfKey; if ( $spConfKey =~ /^$spConfKeyPrefix/ ) { $matchingSpList->{$entityID} = $spConfKey; } printlog("Existing SAML partner found: [SP] $entityID ($spConfKey)\n"); } foreach my $idpConfKey ( keys %{ $lastConf->{samlIDPMetaDataXML} } ) { my ( $tmp, $entityID ) = ( $lastConf->{samlIDPMetaDataXML}->{$idpConfKey}->{samlIDPMetaDataXML} =~ /entityID=(['"])(.+?)\1/si ); $allIdpList->{$entityID} = $idpConfKey; if ( $idpConfKey =~ /^$idpConfKeyPrefix/ ) { $matchingIdpList->{$entityID} = $idpConfKey; } printlog( "Existing SAML partner found: [IDP] $entityID ($idpConfKey)\n"); } my $dom = XML::LibXML->load_xml( string => $xml_metadata ); # Remove extensions foreach ( $dom->findnodes('//md:Extensions') ) { $_->unbindNode; } # Browse all partners foreach my $partner ( $dom->findnodes('/md:EntitiesDescriptor/md:EntityDescriptor') ) { my $entityID = $partner->getAttribute('entityID'); # Add required XML namespaces $partner->setNamespace( "urn:oasis:names:tc:SAML:2.0:metadata", "md", 0 ); $partner->setNamespace( "urn:oasis:names:tc:SAML:2.0:assertion", "saml", 0 ); $partner->setNamespace( "http://www.w3.org/2000/09/xmldsig#", "ds", 0 ); # Check IDP or SP if ( my $idp = $partner->findnodes('./md:IDPSSODescriptor') ) { $idpCounter->{found}++; $mdIdpList->{$entityID} = 1; # Check if SAML 2.0 is supported if ( $partner->findnodes( './md:IDPSSODescriptor/md:SingleSignOnService[contains(@Binding,"urn:oasis:names:tc:SAML:2.0:")]' ) ) { # Read metadata my $partner_metadata = $partner->toString; $partner_metadata =~ s/\n//g; # test if IDP entityID is inside the block list if ( grep { $entityID eq $_ } @idpIgnorelist ) { printlog("IDP $entityID won't be update/added \n"); $idpCounter->{ignored}++; } else { # Check if entityID already in configuration if ( defined $matchingIdpList->{$entityID} ) { my $confKey = $matchingIdpList->{$entityID}; my $equal = register_saml_idp( $config, $lastConf, $confKey, $entityID, $partner_metadata ); if ($equal) { printlog("IDP $entityID has not changed\n"); } else { printlog("Update IDP $entityID in configuration\n"); $idpCounter->{updated}++; } } elsif ( not defined $allIdpList->{$entityID} ) { # Create a new partner my $confKey = toEntityIDkey( $idpConfKeyPrefix, $entityID ); register_saml_idp( $config, $lastConf, $confKey, $entityID, $partner_metadata ); printlog( "Declare new IDP $entityID" . " (configuration key $confKey)\n" ); $idpCounter->{created}++; } else { my $confKey = $allIdpList->{$entityID}; printlog( "Skipping existing IDP $entityID" . " (configuration key $confKey)\n" ); $idpCounter->{ignored}++; } } } else { printlogerr( "[WARN] IDP $entityID is not compatible with SAML 2.0," . " it will not be imported.\n" ); $idpCounter->{rejected}++; } } if ( my $sp = $partner->findnodes('./md:SPSSODescriptor') ) { $spCounter->{found}++; $mdSpList->{$entityID} = 1; # Check if SAML 2.0 is supported if ( $partner->findnodes( './md:SPSSODescriptor/md:AssertionConsumerService[contains(@Binding,"urn:oasis:names:tc:SAML:2.0:")]' ) ) { # Read requested attributes my $requestedAttributes = {}; if ( $partner->findnodes( './md:SPSSODescriptor/md:AttributeConsumingService/md:RequestedAttribute' ) ) { foreach my $requestedAttribute ( $partner->findnodes( './md:SPSSODescriptor/md:AttributeConsumingService/md:RequestedAttribute' ) ) { my $name = $requestedAttribute->getAttribute("Name"); my $friendlyname = $requestedAttribute->getAttribute("FriendlyName"); my $nameformat = $requestedAttribute->getAttribute("NameFormat"); my $required = ( $requestedAttribute->getAttribute("isRequired") =~ /true/i ) ? 1 : 0; $requestedAttributes->{$friendlyname} = { required => $required, name => $name, name_format => $nameformat, friendly_name => $friendlyname }; printlog( "Attribute $friendlyname ($name)" . " requested by SP $entityID\n" ); } } else { $requestedAttributes = { cn => { required => 1, name => 'cn' }, uid => { required => 1, name => 'uid' }, mail => { required => 1, name => 'mail' }, }; } # Remove AttributeConsumingService node foreach ( $partner->findnodes( './md:SPSSODescriptor/md:AttributeConsumingService') ) { $_->unbindNode; } # Read metadata my $partner_metadata = $partner->toString; $partner_metadata =~ s/\n//g; # test if IDP entityID is inside the block list if ( grep { $entityID eq $_ } @spIgnorelist ) { printlog("SP $entityID won't be update/added \n"); $spCounter->{ignored}++; } else { # Check if entityID already in configuration my $confKey; if ( defined $matchingSpList->{$entityID} ) { $confKey = $matchingSpList->{$entityID}; my $equal = register_saml_sp( $config, $lastConf, $confKey, $entityID, $partner_metadata, $requestedAttributes ); if ($equal) { printlog("SP $entityID has not changed\n"); } else { printlog("Update SP $entityID in configuration\n"); $spCounter->{updated}++; } } elsif ( not defined $allSpList->{$entityID} ) { # Create a new partner $confKey = toEntityIDkey( $spConfKeyPrefix, $entityID ); register_saml_sp( $config, $lastConf, $confKey, $entityID, $partner_metadata, $requestedAttributes ); printlog( "Declare new SP $entityID" . " (configuration key $confKey)\n" ); $spCounter->{created}++; } else { my $entityID = $allSpList->{$entityID}; printlog( "Skipping existing SP $entityID " . "(configuration key $confKey)\n" ); $spCounter->{ignored}++; } } } else { printlogerr( "[WARN] SP $entityID is not compatible with SAML 2.0," . " it will not be imported.\n" ); $spCounter->{rejected}++; } } } # Remove partners if ( $config->{main}->{remove} ) { foreach my $entityID ( keys %$matchingIdpList ) { my $idpConfKey = $matchingIdpList->{$entityID}; unless ( defined $mdIdpList->{$entityID} ) { if ( grep { $entityID eq $_ } @idpIgnorelist ) { $idpCounter->{ignored}++; printlog("IDP $idpConfKey won't be deleted \n"); } else { delete $lastConf->{samlIDPMetaDataXML}->{$idpConfKey}; delete $lastConf->{samlIDPMetaDataExportedAttributes} ->{$idpConfKey}; delete $lastConf->{samlIDPMetaDataOptions}->{$idpConfKey}; $idpCounter->{removed}++; printlog("Remove IDP $idpConfKey\n"); } } } foreach my $entityID ( keys %$matchingSpList ) { my $spConfKey = $matchingSpList->{$entityID}; unless ( defined $mdSpList->{$entityID} ) { if ( grep { $entityID eq $_ } @spIgnorelist ) { $spCounter->{ignored}++; printlog("SP $spConfKey won't be deleted \n"); } else { delete $lastConf->{samlSPMetaDataXML}->{$spConfKey}; delete $lastConf->{samlSPMetaDataExportedAttributes} ->{$spConfKey}; delete $lastConf->{samlSPMetaDataOptions}->{$spConfKey}; $spCounter->{removed}++; printlog("Remove SP $spConfKey\n"); } } } } return ( $spCounter, $idpCounter ); } sub main { my $config = get_config( \@ARGV ); # Get merged config my %opts = %{ $config->{main} }; $verbose = $opts{verbose}; pod2usage( -message => "Missing metadata URL (-m)", -exitval => 2 ) if !$opts{metadata}; my $conf = Lemonldap::NG::Common::Conf->new(); my $lastConf = $conf->getConf(); printlog( "Read configuration " . $lastConf->{cfgNum} . "\n" ); # Download metadata file my $ua = LWP::UserAgent->new; $ua->timeout(10); $ua->env_proxy; my $metadata_file = $opts{metadata}; printlog("Try to download metadata file at $metadata_file\n"); my $response = $ua->get($metadata_file); if ( $response->is_success ) { printlog("Metadata file found\n"); } else { die $response->status_line; } my $xml_metadata = $response->decoded_content; my ( $spCounter, $idpCounter ) = transform_config( $config, $lastConf, $xml_metadata ); my $numConf = "DRY-RUN"; my $exitCode = 0; if ( !$opts{'dry-run'} ) { # Register configuration printlog("[INFO] run mod EntityID will be inserted\n"); $numConf = $conf->saveConf( $lastConf, ( cfgNumFixed => 1 ) ); printlog("[OK] Configuration $numConf saved\n"); unless ($numConf > 0) { print "[ERROR] Unable to save configuration\n"; $exitCode = 1; } } else { printlog("[INFO] Dry-run mod no EntityID inserted\n"); } if ( $opts{nagios} ) { print "Metadata loaded inside Conf: [" . $numConf . "]|idp_found=" . $idpCounter->{found} . ", idp_updated=" . $idpCounter->{updated} . ", idp_created=" . $idpCounter->{created} . ", idp_removed=" . $idpCounter->{removed} . ", idp_rejected=" . $idpCounter->{rejected} . ", idp_ignored=" . $idpCounter->{ignored} . ", sp_found=" . $spCounter->{found} . ", sp_updated=" . $spCounter->{updated} . ", sp_created=" . $spCounter->{created} . ", sp_removed=" . $spCounter->{removed} . ", sp_rejected=" . $spCounter->{rejected} . ", sp_ignored=" . $spCounter->{ignored} . "\n"; } else { print "[IDP]\tFound: " . $idpCounter->{found} . "\tUpdated: " . $idpCounter->{updated} . "\tCreated: " . $idpCounter->{created} . "\tRemoved: " . $idpCounter->{removed} . "\tRejected: " . $idpCounter->{rejected} . "\tIgnored: " . $idpCounter->{ignored} . "\n"; print "[SP]\tFound: " . $spCounter->{found} . "\tUpdated: " . $spCounter->{updated} . "\tCreated: " . $spCounter->{created} . "\tRemoved: " . $spCounter->{removed} . "\tRejected: " . $spCounter->{rejected} . "\tIgnored: " . $spCounter->{ignored} . "\n"; } exit $exitCode; } # If run as a script unless (caller) { main(); } __END__ Script to import SAML metadata bundle file into LL::NG configuration\n\n"; Usage: $0 -m \n\n"; Options:\n"; =encoding UTF-8 =head1 NAME importMetadata - Script to import SAML federation metadata into LL::NG configuration =head1 SYNOPSIS importMetadata -m [options] Options: -m, --metadata URL of metadata document -i, --idpconfprefix Prefix used to set IDP configuration key -s, --spconfprefix Prefix used to set SP configuration key --ignore-sp ignore SP matching this entityID (can be specified multiple times) --ignore-idp ignore IdP matching this entityID (can be specified multiple times) -a, --nagios output statistics in Nagios format -r, --remove remove provider from LemonLDAP::NG if it does not appear in metadata -n, --dry-run print statistics but do not apply changes -c, --config-file use provided configuration file -v, --verbose increase verbosity of output -h, --help print full documentation =head1 OPTIONS =over =item B<-m I>, B<--metadata=I> Specifies the of the metadata document to import =item B<-i I>, B<--idpconfprefix=I> Prefix each IDP found the metadata document with the when registring them into LemonLDAP::NG =item B<-s I>, B<--spconfprefix=I> Prefix each SP found the metadata document with the when registring them into LemonLDAP::NG =item B<--ignore-sp=I> Ignore the specified Service Provider . It will not be added, updated or deleted from LemonLDAP::NG configuration =item B<--ignore-idp=I> Ignore the specified Identity Provider . It will not be added, updated or deleted from LemonLDAP::NG configuration =item B<-a>, B<--nagios> After each run, print statistics about added/modified/deleted items in Nagios format =item B<-r>, B<--remove> If this option is used, after a successful import, existing SP/IDPs who match the configuration prefix will be removed from LemonLDAP::NG if they were not present in the imported metadata =item B<-n>, B<--dry-run> This option prevents the modified configuration from being saved. It can be used for testing. =item B<-c>, B<--config-file> Using a configuration file lets you do advanced configuration on a global per-provider basis. The configuration file is stored in .ini format. Here is an example file # main script options, these will be overriden by the CLI options [main] dry-run=1 verbose=1 metadata=http://url/to/metadata.xml ; Multi-value options ignore-idp=entity-id-to-ignore-1 ignore-idp=entity-id-to-ignore-2 # Default exported attributes for IDPs [exportedAttributes] cn=0;cn eduPersonPrincipalName=0;eduPersonPrincipalName ... # options that apply to all providers [ALL] ; Disable signature requirement on requests samlSPMetaDataOptionsCheckSSOMessageSignature=0 samlSPMetaDataOptionsCheckSLOMessageSignature=0 ; Store SAML assertions in session samlIDPMetaDataOptionsStoreSAMLToken=1 ; Mark ePPN as always required attribute_required_eduPersonPrincipalName=1 ... # Specific provider configurations [https://test-sp.federation.renater.fr] ; All attributes are optional for this provider attribute_required=0 ; Override some options samlSPMetaDataOptionsNameIDFormat=persistent [https://idp.renater.fr/idp/shibboleth] ; declare an extra attribute from this provider exported_attribute_eduPersonAffiliation=1;uid =item B<-v>, B<--verbose> Increase verbosity during script execution =item B<-h>, B<--help> Displays the script's documentation =back =head1 SEE ALSO L =head1 AUTHORS =over =item Clement Oudot, Eclement@oodo.netE =back =head1 BUG REPORT Use OW2 system to report bug or ask for features: L =head1 DOWNLOAD Lemonldap::NG is available at L