Skip to content

Commit f38df60

Browse files
authored
Merge pull request #160 from timlegge/post-support-2023-02-06
Initial support for sending requests via HTTP-POST
2 parents 0b378cb + c815c58 commit f38df60

File tree

7 files changed

+378
-23
lines changed

7 files changed

+378
-23
lines changed

lib/Net/SAML2/Binding/POST.pm

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ package Net::SAML2::Binding::POST;
44
# VERSION
55

66
use Moose;
7+
use Carp qw(croak);
78

89
# ABSTRACT: Net::SAML2::Binding::POST - HTTP POST binding for SAML
910

@@ -27,6 +28,8 @@ Net::SAML2::Binding::POST - HTTP POST binding for SAML2
2728
use Net::SAML2::XML::Sig;
2829
use MIME::Base64 qw/ decode_base64 /;
2930
use Crypt::OpenSSL::Verify;
31+
use MIME::Base64;
32+
use URI::Escape;
3033

3134
with 'Net::SAML2::Role::VerifyXML';
3235

@@ -49,6 +52,9 @@ path to the CA certificate for verification
4952
has 'cert_text' => (isa => 'Str', is => 'ro');
5053
has 'cacert' => (isa => 'Maybe[Str]', is => 'ro');
5154

55+
has 'cert' => (isa => 'Str', is => 'ro', required => 0, predicate => 'has_cert');
56+
has 'key' => (isa => 'Str', is => 'ro', required => 0, predicate => 'has_key');
57+
5258
=head2 handle_response( $response )
5359
5460
Decodes and verifies the response provided, which should be the raw
@@ -76,4 +82,50 @@ sub handle_response {
7682
return $xml;
7783
}
7884

85+
=head2 sign_xml( $request )
86+
87+
Sign and encode the SAMLRequest.
88+
89+
=cut
90+
91+
sub sign_xml {
92+
my ($self, $request) = @_;
93+
94+
croak("Need to have a cert specified") unless $self->has_cert;
95+
croak("Need to have a key specified") unless $self->has_key;
96+
97+
my $signer = XML::Sig->new({
98+
key => $self->key,
99+
cert => $self->cert,
100+
no_xml_declaration => 1,
101+
}
102+
);
103+
104+
my $signed_message = $signer->sign($request);
105+
106+
# saml-schema-protocol-2.0.xsd Schema hack
107+
#
108+
# The real fix here is to fix XML::Sig to accept a XPATH to
109+
# place the signature in the correct location. Or use XML::LibXML
110+
# here to do so
111+
#
112+
# The protocol schema defines a sequence which requires the order
113+
# of the child elements in a Protocol based message:
114+
#
115+
# The dsig:Signature (should it exist) MUST follow the saml:Issuer
116+
#
117+
# 1: saml:Issuer
118+
# 2: dsig:Signature
119+
#
120+
# Seems like an oversight in the SAML schema specifiation but...
121+
122+
$signed_message =~ s!(<dsig:Signature.*?</dsig:Signature>)!!s;
123+
my $signature = $1;
124+
$signed_message =~ s/(<\/saml\d*:Issuer>)/$1$signature/;
125+
126+
my $encoded_request = encode_base64($signed_message, "\n");
127+
128+
return $encoded_request;
129+
130+
}
79131
__PACKAGE__->meta->make_immutable;

lib/Net/SAML2/SP.pm

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -424,6 +424,37 @@ sub artifact_request {
424424
return $artifact_request;
425425
}
426426

427+
=head2 sp_post_binding ( $idp, $param )
428+
429+
Returns a POST binding object for this SP, configured against the
430+
given IDP for Single Sign On. $param specifies the name of the query
431+
parameter involved - typically C<SAMLRequest>.
432+
433+
=cut
434+
435+
sub sp_post_binding {
436+
my ($self, $idp, $param) = @_;
437+
438+
unless ($idp) {
439+
croak("Unable to create a post binding without an IDP");
440+
}
441+
442+
$param //= 'SAMLRequest';
443+
444+
my $post = Net::SAML2::Binding::POST->new(
445+
url => $idp->sso_url('urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST'),
446+
cert => ($self->cert,),
447+
$self->authnreq_signed ? (
448+
key => $self->key,
449+
) : (
450+
insecure => 1,
451+
),
452+
param => $param,
453+
);
454+
455+
return $post;
456+
}
457+
427458
=head2 sso_redirect_binding( $idp, $param )
428459
429460
Returns a Redirect binding object for this SP, configured against the

t/23-post-binding.t

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
use strict;
2+
use warnings;
3+
use Test::Lib;
4+
use Test::Net::SAML2;
5+
use URI;
6+
use MIME::Base64 qw/decode_base64/;
7+
8+
use Net::SAML2::IdP;
9+
use Net::SAML2::Binding::Redirect;
10+
use XML::Sig;
11+
12+
my $sp = net_saml2_sp();
13+
14+
my $metadata = path('t/idp-metadata.xml')->slurp;
15+
16+
my $idp = Net::SAML2::IdP->new_from_xml(
17+
xml => $metadata,
18+
cacert => 't/cacert.pem'
19+
);
20+
isa_ok($idp, "Net::SAML2::IdP");
21+
22+
my $sso_url = $idp->sso_url($idp->binding('post'));
23+
is(
24+
$sso_url,
25+
'http://sso.dev.venda.com/opensso/SSOPOST/metaAlias/idp',
26+
'POST URI is correct'
27+
);
28+
29+
my $authnreq = $sp->authn_request(
30+
$idp->entityid,
31+
$idp->format('persistent')
32+
)->as_xml;
33+
34+
my $xp = get_xpath($authnreq);
35+
36+
my $post = $sp->sp_post_binding($idp, 'SAMLRequest');
37+
isa_ok($post, 'Net::SAML2::Binding::POST');
38+
39+
my $post_request = $post->sign_xml($authnreq);
40+
41+
my $request = decode_base64($post_request);
42+
43+
like(
44+
$request,
45+
qr#\Q<saml2:Issuer>Some%20entity%20ID</saml2:Issuer>\E#,
46+
"Authn request checks out"
47+
);
48+
49+
my $signer = XML::Sig->new();
50+
ok($signer->verify($request), "Valid Signature");
51+
52+
my %logout_params;
53+
54+
my $logoutreq = $sp->logout_request(
55+
$idp->slo_url('urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST'),
56+
'timlegge@cpan.org',
57+
$idp->format || undef,
58+
'94750270472009384017107023022',
59+
\%logout_params,
60+
)->as_xml;
61+
62+
$post = $sp->sp_post_binding($idp, 'SAMLRequest');
63+
64+
$post_request = $post->sign_xml($logoutreq);
65+
$request = decode_base64($post_request);
66+
67+
like(
68+
$request,
69+
qr#\Q<samlp:SessionIndex>94750270472009384017107023022</samlp:SessionIndex>\E#,
70+
"LogoutRequest checks out"
71+
);
72+
73+
$signer = XML::Sig->new();
74+
ok($signer->verify($request), "Valid Signature");
75+
76+
done_testing;

0 commit comments

Comments
 (0)