Skip to content

Commit 89393ab

Browse files
authored
Merge pull request #55 from timlegge/master
Fix several issues and update documentation
2 parents 064f718 + 237ef74 commit 89393ab

File tree

7 files changed

+189
-17
lines changed

7 files changed

+189
-17
lines changed

Makefile.PL

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ my %WriteMakefileArgs = (
2525
"DateTime::Format::XSD" => 0,
2626
"DateTime::HiRes" => 0,
2727
"Exporter" => 0,
28-
"File::Slurp" => 0,
28+
"File::Slurper" => 0,
2929
"HTTP::Request::Common" => 0,
3030
"IO::Compress::RawDeflate" => 0,
3131
"IO::Uncompress::RawInflate" => 0,
@@ -66,7 +66,7 @@ my %WriteMakefileArgs = (
6666
"URI::URL" => 0,
6767
"XML::LibXML::XPathContext" => 0
6868
},
69-
"VERSION" => "0.49",
69+
"VERSION" => "0.50",
7070
"test" => {
7171
"TESTS" => "t/*.t t/author/*.t"
7272
}
@@ -83,7 +83,7 @@ my %FallbackPrereqs = (
8383
"DateTime::Format::XSD" => 0,
8484
"DateTime::HiRes" => 0,
8585
"Exporter" => 0,
86-
"File::Slurp" => 0,
86+
"File::Slurper" => 0,
8787
"HTTP::Request::Common" => 0,
8888
"IO::Compress::RawDeflate" => 0,
8989
"IO::Uncompress::RawInflate" => 0,

README

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ NAME
22
Net::SAML2 - SAML2 bindings and protocol implementation
33

44
VERSION
5-
version 0.49
5+
version 0.50
66

77
SYNOPSIS
88
See TUTORIAL.md for implementation documentation and
@@ -114,7 +114,7 @@ AUTHOR
114114
Chris Andrews <chrisa@cpan.org>
115115

116116
COPYRIGHT AND LICENSE
117-
This software is copyright (c) 2021 by Chris Andrews and Others, see the
117+
This software is copyright (c) 2022 by Chris Andrews and Others, see the
118118
git log.
119119

120120
This is free software; you can redistribute it and/or modify it under

TUTORIAL.md

Lines changed: 155 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -358,7 +358,7 @@ For the $saml_request_id you need to retrieve it from wherever it was stored dur
358358

359359
The call to $assertion->valid validates the following for the assertion:
360360

361-
1. That the $issuer configure in your application is the $audience of the assertion
361+
1. That the $issuer configured in your application is the $audience of the assertion
362362
2. That the $saml_request_id is the InResponseTo of the assertion
363363
3. That the current time is within the NotBefore and NotAfter datetimes of the assertion
364364

@@ -401,7 +401,160 @@ As a developer you need to review what is returned in the SAML2 Assertion from t
401401

402402
Regardless, the attributes of the assertion will contain those values, It is up to your application to use or ignore them as you see fit.
403403

404-
## Step 3: Generating the Service Provider (SP) Metadata (Optional)
404+
## Step 3: Service Provider initiated LogoutRequest (Optional)
405+
406+
The Service Provider (SP) can initiate a LogoutRequest to the Identity Provider (IdP). This is optional and the IdP can support Single Logout (SLO) which will initiate a process to logout all IdP logins sharing the session.
407+
408+
The process begins with the creation of the LogoutRequest XML with the correct values and then it is sent to the IdP via a Browser Redirect.
409+
410+
The following is from Foswiki's SamlLoginContrib function:
411+
```
412+
413+
# Foswiki's SamlLoginContrib stores the Assertions session_index
414+
my $sessionindex = $this->getSessionValue('saml_session_index');
415+
416+
my $idp = Net::SAML2::IdP->new_from_url(
417+
 url => $this->{Saml}{ metadata},
418+
 cacert => $this->{Saml}{ cacert },
419+
 );
420+
421+
my $logoutrequest = Net::SAML2::Protocol::LogoutRequest->new(
422+
 issuer => $this->{Saml}{ issuer },
423+
 nameid_format => 'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress',
424+
 destination => $idp->slo_url('urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect'),
425+
 nameid => $session->{users}->getLoginName($session->{user}),
426+
 session => $sessionindex,
427+
 );
428+
429+
 my $logoutreq = $logoutrequest->as_xml;
430+
431+
 my $redirect = Net::SAML2::Binding::Redirect->new(
432+
 key => $this->{Saml}{ sp_signing_key },
433+
 cert => $this->{Saml}{ sp_signing_cert },
434+
 destination => $idp->slo_url('urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect'),
435+
 param => 'SAMLRequest',
436+
 url => $idp->slo_url('urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect'),
437+
 );
438+
 my $url = $redirect->sign($logoutreq);
439+
440+
# The $url is then sent to the browser as a redirect to initiate the logout.
441+
442+
```
443+
The IdP will respond with a LogoutResponse that is sent to the browser via a HTTP-POST or an HTTP-Redirect depending on the SP's configuration at the IdP (the SP metadata would specify the slo_url that is supported).
444+
445+
The SP would process the LogoutResponse and if it was a sucessful response invalidate the user's session at the SP.
446+
447+
The following is from Foswiki's SamlLoginContrib function:
448+
```
449+
# Foswiki's SamlLoginContrib stores the Assertions session_index
450+
# my $sessionindex = $this->getAndClearSessionValue('saml_session_index');
451+
452+
 my $idp = Net::SAML2::IdP->new_from_url(
453+
 url => $this->{Saml}{metadata},
454+
 cacert => $this->{Saml}{cacert},
455+
 sls_force_lcase_url_encoding => $this->{Saml}{sls_force_lcase_url_encoding},
456+
 sls_double_encoded_response => $this->{Saml}{sls_double_encoded_response}
457+
 );
458+
459+
my $redirect = Net::SAML2::Binding::Redirect->new(
460+
url => $idp->slo_url('urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect'),
461+
key => $this->{Saml}{sp_signing_key},
462+
cert => $idp->cert('signing'),
463+
param => 'SAMLResponse',
464+
sls_force_lcase_url_encoding => $this->{Saml}{sls_force_lcase_url_encoding},
465+
sls_double_encoded_response => $this->{Saml}{sls_double_encoded_response}
466+
);
467+
468+
my ($response, $relaystate) = $redirect->verify($uri);
469+
470+
if ($response) {
471+
 my $logout = Net::SAML2::Protocol::LogoutResponse->new_from_xml(
472+
 xml => $response
473+
 );
474+
475+
 if ($logout->status eq 'urn:oasis:names:tc:SAML:2.0:status:Success') {
476+
 deleteSession(...)
477+
 }
478+
 }
479+
480+
```
481+
482+
### Creating the LogoutRequest
483+
484+
## Step 4: IdP initiated LogoutRequest (Optional)
485+
486+
The Identity Provider (IdP) can intiate a LogoutRequest to the Service Provider (SP). There is no guarantee that this IdP initiated message will even get received by the SP as it requires the IdP to send a HTTP-GET request directly to the SP. It works outside the typical browser interaction for SAML. Indeed for many internal applications there is no direct internet access allowed to the SP application.
487+
488+
The process begins when the SP receives an unsolicated HTTP-GET request from the IdP. The SP must decode that LogoutRequest and process it to logout the user locally.
489+
490+
### Handling the LogoutRequest
491+
492+
The SP needs to create the Net::SAML2::IdP object as is done above (in this case using new_from_xml but could be new_from_url).
493+
494+
```
495+
my $idp = Net::SAML2::IdP->new_from_xml(
496+
xml => $metadata, # URL where the xml is located
497+
cacert => $cacert2, # Filename of the Identity Providers CACert
498+
);
499+
500+
```
501+
Create the Net::SAML2::Binding::Redirect object. Note the sls_force_lcase_url_encoding is used if the IdP sends a URL that has meen URL encoded with lower case characters %2f instead of %2F.
502+
503+
```
504+
my $redirect = Net::SAML2::Binding::Redirect->new(
505+
key => 't/sign-nopw-cert.pem',
506+
cert => $idp->cert('signing'),
507+
sig_hash => 'sha256',
508+
param => 'SAMLRequest',
509+
# The ssl_url destination for redirect
510+
url => $idp->sso_url('urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect'),
511+
#sls_force_lcase_url_encoding => 1,
512+
);
513+
```
514+
Verify signature on the URL, decode the request and retrieve the XML request and RelayState.
515+
516+
```
517+
my ($request, $relaystate) = $redirect->verify($get_request);
518+
519+
```
520+
Create the LogoutRequest object from the decoded XML request.
521+
522+
```
523+
my $logoutrequest = Net::SAML2::Protocol::LogoutRequest->new_from_xml(xml => $request);
524+
525+
```
526+
527+
The data that the SP requires is in the resulting Net::SAML2::Protocol::LogoutRequest object. The SP should perform a local logout of the nameid specified in the LogoutRequest. The session *should* be the session that the IdP sent in the Assertion (need to review). In general however the user associated with the nameid should have their session invalidated and the should be user forced to login again on next access.
528+
529+
```
530+
$VAR1 = \bless( {
531+
'id' => '_754753ec-5845-4d3f-bc06-84dbebf64c38',
532+
'nameid' => 'timlegge@cpan.org',
533+
'destination' => 'https://net-saml2.local/logout',
534+
'issue_instant' => '2022-01-29T00:32:40Z',
535+
'issuer' => bless( do{\(my $o = 'http://keycloak.local/')}, 'URI::http' ),
536+
'session' => '_4f6b29af-0e3c-4970-4f40-9609-fe9843ca1dc0'
537+
}, 'Net::SAML2::Protocol::LogoutRequest' );
538+
539+
```
540+
The logout response should be sent to the IdP by the SP after the local user's session has been invalidated. The LogoutResponse is created by creating the Net::SAML2::Protocol::LogoutResponse object with the correct values. The response_to is the id from the LogoutRequest. It is the LogoutRequest to which the LogoutRespones is related. Below shows the issue and the destination as the opposite of the same values from the LogoutRequest. The issuer in the request is likely where the LogoutResponse should be sent (the destination). More properly the issuer should be the $sp->{issuer} and the destination the $idp->{slo_url}->{urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect}.
541+
542+
```
543+
my $logoutresponse = Net::SAML2::Protocol::LogoutResponse->new(
544+
issuer => $logoutrequest->{destination},
545+
destination => $logoutrequest->{issuer},
546+
status => "urn:oasis:names:tc:SAML:2.0:status:Success",
547+
response_to => $logoutrequest->{id},
548+
);
549+
```
550+
551+
Once you have created the LogoutResponse you sign the XML version of the LogoutResponse using the Net::SAML2::Binding::Redirect. This results in a URL that the SP must use in a GET request to inform the IdP that the session was properly invalidated by the SP.
552+
553+
```
554+
my $logoutrequestsigned = $redirect->sign($logoutresponse->as_xml);
555+
```
556+
557+
## Step 5: Generating the Service Provider (SP) Metadata (Optional)
405558

406559
Some Identity Providers allow you to import a XML file that has the Service Provider settings. This allows you to ensure that the settings defined in your application are the same as those configured as the Service Provider settings in Identity Provider.
407560

cpanfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ requires "DateTime" => "0";
99
requires "DateTime::Format::XSD" => "0";
1010
requires "DateTime::HiRes" => "0";
1111
requires "Exporter" => "0";
12-
requires "File::Slurp" => "0";
12+
requires "File::Slurper" => "0";
1313
requires "HTTP::Request::Common" => "0";
1414
requires "IO::Compress::RawDeflate" => "0";
1515
requires "IO::Uncompress::RawInflate" => "0";

lib/Net/SAML2/Binding/Redirect.pm

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ use URI;
3838
use URI::QueryParam;
3939
use Crypt::OpenSSL::RSA;
4040
use Crypt::OpenSSL::X509;
41-
use File::Slurp qw/ read_file /;
41+
use File::Slurper qw/ read_text /;
4242
use URI::Encode qw/uri_decode/;
4343

4444
=head2 new( ... )
@@ -127,7 +127,7 @@ sub sign {
127127
$u->query_param($self->param, $req);
128128
$u->query_param('RelayState', $relaystate) if defined $relaystate;
129129

130-
my $key_string = read_file($self->key);
130+
my $key_string = read_text($self->key);
131131
my $rsa_priv = Crypt::OpenSSL::RSA->new_private_key($key_string);
132132

133133
if ( exists $self->{ sig_hash } && grep { $_ eq $self->{ sig_hash } } ('sha224', 'sha256', 'sha384', 'sha512'))
@@ -180,6 +180,19 @@ sub verify {
180180
my $saml_request;
181181
my $sig = $u->query_param_delete('Signature');
182182

183+
# During the verify the only query parameters that should be in the query are
184+
# 'SAMLRequest', 'RelayState', 'Sig', 'SigAlg' the other parameter values are
185+
# deleted from the URI query that was created from the URL that was passed
186+
# to the verify function
187+
my @signed_params = ('SAMLRequest', 'RelayState', 'Sig', 'SigAlg');
188+
189+
for my $key ($u->query_param) {
190+
if (grep /$key/, @signed_params ) {
191+
next;
192+
}
193+
$u->query_param_delete($key);
194+
}
195+
183196
# Some IdPs (PingIdentity) seem to double encode the LogoutResponse URL
184197
if (defined $self->sls_double_encoded_response and $self->sls_double_encoded_response == 1) {
185198
#if ($sigalg =~ m/%/) {

lib/Net/SAML2/IdP.pm

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -177,10 +177,10 @@ sub new_from_xml {
177177
{
178178
my $use = $key->getAttribute('use') || 'signing';
179179

180-
# We can't select by ds:KeyInfo/ds:X509Data/ds:X509Certificate
181-
# because of https://rt.cpan.org/Public/Bug/Display.html?id=8784
180+
$key->setNamespace('http://www.w3.org/2000/09/xmldsig#', 'ds');
181+
182182
my ($text)
183-
= $key->findvalue("//*[local-name()='X509Certificate']")
183+
= $key->findvalue("ds:KeyInfo/ds:X509Data/ds:X509Certificate", $key)
184184
=~ /^\s*(.+?)\s*$/s;
185185

186186
# rewrap the base64 data from the metadata; it may not

lib/Net/SAML2/Protocol/LogoutRequest.pm

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ sent regardless
6363

6464
has 'session' => (isa => NonEmptySimpleStr, is => 'ro', required => 1);
6565
has 'nameid' => (isa => NonEmptySimpleStr, is => 'ro', required => 1);
66-
has 'nameid_format' => (isa => NonEmptySimpleStr, is => 'ro', required => 1);
66+
has 'nameid_format' => (isa => NonEmptySimpleStr, is => 'ro', required => 0);
6767
has 'destination' => (isa => NonEmptySimpleStr, is => 'ro', required => 0);
6868

6969
=head2 new_from_xml( ... )
@@ -91,13 +91,19 @@ sub new_from_xml {
9191
$xpath->registerNs('saml', 'urn:oasis:names:tc:SAML:2.0:assertion');
9292
$xpath->registerNs('samlp', 'urn:oasis:names:tc:SAML:2.0:protocol');
9393

94-
my $self = $class->new(
94+
my %params = (
9595
id => $xpath->findvalue('/samlp:LogoutRequest/@ID'),
9696
session => $xpath->findvalue('/samlp:LogoutRequest/samlp:SessionIndex'),
9797
issuer => $xpath->findvalue('/samlp:LogoutRequest/saml:Issuer'),
9898
nameid => $xpath->findvalue('/samlp:LogoutRequest/saml:NameID'),
99-
nameid_format => $xpath->findvalue('/samlp:LogoutRequest/saml:NameID/@Format'),
100-
destination => $xpath->findvalue('/samlp:LogoutRequest/saml:NameID/@NameQualifier'),
99+
destination => $xpath->findvalue('/samlp:LogoutRequest/@Destination'),
100+
);
101+
102+
my $nameid_format = $xpath->findvalue('/samlp:LogoutRequest/saml:NameID/@Format');
103+
if ( $nameid_format ne '' ) { $params{nameid_format} = $nameid_format; }
104+
105+
my $self = $class->new(
106+
%params
101107
);
102108

103109
return $self;

0 commit comments

Comments
 (0)