Skip to content

Commit 971434a

Browse files
committed
Add Object::Response to Net::SAML2
This is to accomodate responses which do not include Assertions Closes: #209 Signed-off-by: Wesley Schwengle <waterkip@cpan.org>
1 parent 90d2c80 commit 971434a

File tree

6 files changed

+294
-1
lines changed

6 files changed

+294
-1
lines changed

README

Lines changed: 1 addition & 1 deletion
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.78
5+
version 0.79
66

77
SYNOPSIS
88
See TUTORIAL.md for implementation documentation and

dist.ini

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ skip = warnings
5252
skip = strict
5353
skip = overload
5454
skip = base
55+
skip = feature
5556

5657
[Prereqs / RuntimeRequires]
5758
perl = 5.014

lib/Net/SAML2/Object/Response.pm

Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
package Net::SAML2::Object::Response;
2+
use Moose;
3+
4+
# VERSION
5+
6+
use overload '""' => 'to_string';
7+
8+
# ABSTRACT: A response object
9+
10+
use MooseX::Types::DateTime qw/ DateTime /;
11+
use MooseX::Types::Common::String qw/ NonEmptySimpleStr /;
12+
use DateTime;
13+
use DateTime::HiRes;
14+
use DateTime::Format::XSD;
15+
use Net::SAML2::XML::Util qw/ no_comments /;
16+
use Net::SAML2::XML::Sig;
17+
use XML::Enc;
18+
use XML::LibXML::XPathContext;
19+
use List::Util qw(first);
20+
use URN::OASIS::SAML2 qw(STATUS_SUCCESS URN_ASSERTION URN_PROTOCOL);
21+
use Carp qw(croak);
22+
23+
with 'Net::SAML2::Role::ProtocolMessage';
24+
25+
=head1 DESCRIPTION
26+
27+
A generic response object to be able to deal with an response from the IdP. If
28+
the status is successful you can grab an assertion and continue your flow.
29+
30+
=head1 SYNOPSIS
31+
32+
use Net::SAML2::Object::Response;
33+
34+
my $xml = ...;
35+
my $response = Net::SAML2::Object::Response->new_from_xml(xml => $xml);
36+
37+
if (!$response->is_success) {
38+
warn "Got a response but isn't successful";
39+
40+
my $status = $response->status;
41+
my $sub_status = $response->sub_status;
42+
43+
warn "We got a $status back with the following sub status $sub_status";
44+
}
45+
else {
46+
$response->to_assertion(
47+
# See Net::SAML2::Protocol::Assertion->new_from_xml for the other
48+
# construction options
49+
key_file => ...,
50+
key_name => ...,
51+
)
52+
}
53+
54+
=head1 ATTRIBUTES
55+
56+
=head2 status
57+
58+
Returns the status of the response
59+
60+
=head2 sub_status
61+
62+
Returns the sub status of the response
63+
64+
=head2 assertions
65+
66+
Returns the nodes of the assertion
67+
68+
=cut
69+
70+
has _dom => (
71+
is => 'ro',
72+
isa => 'XML::LibXML::Node',
73+
init_arg => 'dom',
74+
required => 1,
75+
);
76+
77+
has status => (
78+
is => 'ro',
79+
isa => 'Str',
80+
required => 1,
81+
);
82+
83+
has sub_status => (
84+
is => 'ro',
85+
isa => 'Str',
86+
required => 0,
87+
predicate => 'has_sub_status',
88+
);
89+
90+
has assertions => (
91+
is => 'ro',
92+
isa => 'XML::LibXML::NodeList',
93+
required => 0,
94+
predicate => 'has_assertions',
95+
);
96+
97+
=head1 METHODS
98+
99+
=head2 $self->new_from_xml(xml => $xml)
100+
101+
Creates the response object based on the response XML
102+
103+
=cut
104+
105+
sub new_from_xml {
106+
my $self = shift;
107+
my %args = @_;
108+
109+
my $xml = no_comments($args{xml});
110+
111+
my $xpath = XML::LibXML::XPathContext->new($xml);
112+
$xpath->registerNs('saml', URN_ASSERTION);
113+
$xpath->registerNs('samlp', URN_PROTOCOL);
114+
115+
my $response = $xpath->findnodes('/samlp:Response|/samlp:ArtifactResponse');
116+
croak("Unable to parse response") unless $response->size;
117+
$response = $response->get_node(1);
118+
119+
my $code_path = 'samlp:Status/samlp:StatusCode';
120+
if ($response->nodePath eq '/samlp:ArtifactResponse') {
121+
$code_path = "samlp:Response/$code_path";
122+
}
123+
124+
my $status = $xpath->findnodes($code_path, $response);
125+
croak("Unable to parse status from response") unless $status->size;
126+
127+
my $status_node = $status->get_node(1);
128+
$status = $status_node->getAttribute('Value');
129+
130+
my $substatus = $xpath->findvalue('samlp:StatusCode/@Value', $status_node);
131+
132+
my $nodes = $xpath->findnodes('//saml:EncryptedAssertion|//saml:Assertion', $response);
133+
134+
return $self->new(
135+
dom => $xml,
136+
status => $status,
137+
$substatus ? ( sub_status => $substatus) : (),
138+
issuer => $xpath->findvalue('saml:Issuer', $response),
139+
id => $response->getAttribute('ID'),
140+
in_response_to => $response->getAttribute('InResponseTo'),
141+
$nodes->size ? (assertions => $nodes) : (),
142+
);
143+
}
144+
145+
=head2 $self->to_string
146+
147+
Stringify the object to the full response XML
148+
149+
=cut
150+
151+
sub to_string {
152+
my $self = shift;
153+
return $self->_dom->toString;
154+
}
155+
156+
=head2 $self->to_assertion(%args)
157+
158+
Create a L<Net::SAML2::Protocol::Assertion> from the response. See
159+
L<Net::SAML2::Protocol::Assertion/new_from_xml> for more.
160+
161+
=cut
162+
163+
sub to_assertion {
164+
my $self = shift;
165+
my %args = @_;
166+
167+
if (!$self->has_assertions) {
168+
croak("There are no assertions found in the response object");
169+
}
170+
171+
return Net::SAML2::Protocol::Assertion->new_from_xml(%args,
172+
xml => $self->to_string,);
173+
}
174+
175+
1;
176+
177+
178+
__PACKAGE__->meta->make_immutable;
179+
180+
__END__
181+

t/29-response.t

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
use strict;
2+
use warnings;
3+
use Test::Lib;
4+
use Test::Net::SAML2;
5+
6+
use Net::SAML2::Object::Response;
7+
use URN::OASIS::SAML2 qw(STATUS_RESPONDER STATUS_AUTH_FAILED);
8+
9+
sub get_object {
10+
my $xml = path(shift)->slurp;
11+
my $response = Net::SAML2::Object::Response->new_from_xml(xml => $xml);
12+
isa_ok($response, 'Net::SAML2::Object::Response');
13+
return $response;
14+
}
15+
16+
{
17+
my $response = get_object('t/data/digid-anul-artifact-response.xml');
18+
ok(!$response->has_assertions, "We don't have an assertion");
19+
ok(!$response->success, "Unsuccessful response");
20+
is($response->status, STATUS_RESPONDER(), "... because its a status:Responder");
21+
is($response->sub_status, STATUS_AUTH_FAILED(), "... and substatus is also correct");
22+
}
23+
24+
25+
{
26+
my $response = get_object('t/data/eherkenning-assertion.xml');
27+
ok($response->has_assertions, "We have an assertion");
28+
ok($response->success, "It was successful");
29+
is($response->assertions->size, 3, "Got the correct amount or assertions");
30+
31+
my $assertion = $response->to_assertion();
32+
isa_ok($assertion, "Net::SAML2::Protocol::Assertion");
33+
}
34+
35+
36+
{
37+
my $response = get_object('t/data/response-no-assertion.xml');
38+
ok(!$response->has_assertions, "We don't have an assertion");
39+
ok(!$response->success, "Unsuccessful response");
40+
is($response->status, STATUS_RESPONDER(), "... because its a status:Responder");
41+
}
42+
done_testing;
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
<?xml version="1.0"?>
2+
<samlp:ArtifactResponse xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" xmlns:ds="http://www.w3.org/2000/09/xmldsig#" xmlns:ec="http://www.w3.org/2001/10/xml-exc-c14n#" ID="_5ede0a3002fb0efe627efc4bf3a076b7a3810a80" Version="2.0" IssueInstant="2024-04-16T14:52:40Z" InResponseTo="NETSAML2_10aad5b7776e047b40637fec9793fa1b8259e7e7845ca379cd8a01008e1b93f8">
3+
<saml:Issuer>https://was-preprod1.digid.nl/saml/idp/metadata</saml:Issuer>
4+
<ds:Signature>
5+
<ds:SignedInfo>
6+
<ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
7+
<ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
8+
<ds:Reference URI="#_5ede0a3002fb0efe627efc4bf3a076b7a3810a80">
9+
<ds:Transforms>
10+
<ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/>
11+
<ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#">
12+
<ec:InclusiveNamespaces PrefixList="ds saml samlp xs"/>
13+
</ds:Transform>
14+
</ds:Transforms>
15+
<ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
16+
<ds:DigestValue>value here</ds:DigestValue>
17+
</ds:Reference>
18+
</ds:SignedInfo>
19+
<ds:SignatureValue>some sig here</ds:SignatureValue>
20+
<ds:KeyInfo>
21+
<ds:KeyName>7593b799e735055fcd479caa35d44d455576cefc</ds:KeyName>
22+
<ds:X509Data>
23+
<ds:X509Certificate>Some cert here</ds:X509Certificate>
24+
</ds:X509Data>
25+
</ds:KeyInfo>
26+
</ds:Signature>
27+
<samlp:Status>
28+
<samlp:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success"/>
29+
</samlp:Status>
30+
<samlp:Response ID="_bee5133d91cddac59c1943a451915948a0563d5b" Version="2.0" IssueInstant="2024-04-16T14:52:40Z" InResponseTo="NETSAML2_730fbcbf9aca0d79eb18aeee3ad03908f805a3b441d586753c6dd68e53cb5d73">
31+
<saml:Issuer>https://was-preprod1.digid.nl/saml/idp/metadata</saml:Issuer>
32+
<samlp:Status>
33+
<samlp:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Responder">
34+
<samlp:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:AuthnFailed"/>
35+
</samlp:StatusCode>
36+
</samlp:Status>
37+
</samlp:Response>
38+
</samlp:ArtifactResponse>

t/data/response-no-assertion.xml

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<samlp:Response ID="_ae896f90-5ecb-4d00-8738-91337236a165" Version="2.0"
2+
IssueInstant="2024-04-10T13:17:32.119Z" Destination="[our SAML callback url]"
3+
Consent="urn:oasis:names:tc:SAML:2.0:consent:unspecified"
4+
InResponseTo="NETSAML2_d54e304a5472d4429f20e3d44a7a224b"
5+
xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol">
6+
<Issuer xmlns="urn:oasis:names:tc:SAML:2.0:assertion">https://[IdP testing
7+
domain]/adfs/services/trust</Issuer>
8+
<ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
9+
<ds:SignedInfo>
10+
<ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#" />
11+
<ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256" />
12+
<ds:Reference URI="#_ae896f90-5ecb-4d00-8738-91337236a165">
13+
<ds:Transforms>
14+
<ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature" />
15+
<ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#" />
16+
</ds:Transforms>
17+
<ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256" />
18+
<ds:DigestValue>K4r9/0ZZqW32TG+jT9tGsKwYKwNzssSSKIo8gvWPCfo=</ds:DigestValue>
19+
</ds:Reference>
20+
</ds:SignedInfo>
21+
<ds:SignatureValue>[the signature value]</ds:SignatureValue>
22+
<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
23+
<ds:X509Data>
24+
<ds:X509Certificate>[the certificate]</ds:X509Certificate>
25+
</ds:X509Data>
26+
</KeyInfo>
27+
</ds:Signature>
28+
<samlp:Status>
29+
<samlp:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Responder" />
30+
</samlp:Status>
31+
</samlp:Response>

0 commit comments

Comments
 (0)