File Coverage

blib/lib/Net/SAML2/Binding/Redirect.pm
Criterion Covered Total %
statement 77 96 80.2
branch 11 28 39.2
condition 3 9 33.3
subroutine 15 15 100.0
pod 2 2 100.0
total 108 150 72.0


line stmt bran cond sub pod time code
1             package Net::SAML2::Binding::Redirect;
2              
3 13     13   108 use strict;
  13         34  
  13         492  
4 13     13   73 use warnings;
  13         32  
  13         430  
5              
6 13     13   74 use Moose;
  13         28  
  13         114  
7 13     13   98009 use MooseX::Types::URI qw/ Uri /;
  13         31  
  13         191  
8              
9             our $VERSION = '0.42';
10              
11             # ABSTRACT: Net::SAML2::Binding::Redirect - HTTP Redirect binding for SAML
12              
13              
14 13     13   27914 use MIME::Base64 qw/ encode_base64 decode_base64 /;
  13         37  
  13         940  
15 13     13   10744 use IO::Compress::RawDeflate qw/ rawdeflate /;
  13         410794  
  13         1292  
16 13     13   10627 use IO::Uncompress::RawInflate qw/ rawinflate /;
  13         177693  
  13         995  
17 13     13   137 use URI;
  13         35  
  13         328  
18 13     13   78 use URI::QueryParam;
  13         49  
  13         307  
19 13     13   7318 use Crypt::OpenSSL::RSA;
  13         67394  
  13         668  
20 13     13   120 use Crypt::OpenSSL::X509;
  13         39  
  13         799  
21 13     13   8723 use File::Slurp qw/ read_file /;
  13         236764  
  13         1037  
22 13     13   7465 use URI::Encode qw/uri_decode/;
  13         19859  
  13         11478  
23              
24              
25             has 'key' => (isa => 'Str', is => 'ro', required => 1);
26             has 'cert' => (isa => 'Str', is => 'ro', required => 1);
27             has 'url' => (isa => Uri, is => 'ro', required => 1, coerce => 1);
28             has 'param' => (isa => 'Str', is => 'ro', required => 1);
29             has 'sig_hash' => (isa => 'Str', is => 'ro', required => 0);
30             has 'sls_force_lcase_url_encoding' => (isa => 'Bool', is => 'ro', required => 0);
31             has 'sls_double_encoded_response' => (isa => 'Bool', is => 'ro', required => 0);
32              
33              
34             sub sign {
35 2     2 1 1304 my ($self, $request, $relaystate) = @_;
36              
37 2         9 my $input = "$request";
38 2         5 my $output = '';
39              
40 2         18 rawdeflate \$input => \$output;
41 2         4674 my $req = encode_base64($output, '');
42              
43 2         108 my $u = URI->new($self->url);
44 2         329 $u->query_param($self->param, $req);
45 2 100       618 $u->query_param('RelayState', $relaystate) if defined $relaystate;
46              
47 2         402 my $key_string = read_file($self->key);
48 2         1354 my $rsa_priv = Crypt::OpenSSL::RSA->new_private_key($key_string);
49              
50 2 50 33     26 if ( exists $self->{ sig_hash } && grep { $_ eq $self->{ sig_hash } } ('sha224', 'sha256', 'sha384', 'sha512'))
  0         0  
51             {
52 0 0       0 if ($self->{ sig_hash } eq 'sha224') {
    0          
    0          
    0          
53 0         0 $rsa_priv->use_sha224_hash;
54             } elsif ($self->{ sig_hash } eq 'sha256') {
55 0         0 $rsa_priv->use_sha256_hash;
56             } elsif ($self->{ sig_hash } eq 'sha384') {
57 0         0 $rsa_priv->use_sha384_hash;
58             } elsif ($self->{ sig_hash } eq 'sha512') {
59 0         0 $rsa_priv->use_sha512_hash;
60             } else {
61 0         0 die "Unsupported Signing Hash";
62             }
63 0         0 $u->query_param('SigAlg', 'http://www.w3.org/2001/04/xmldsig-more#rsa-' . $self->{ sig_hash });
64             }
65             else { #$self->{ sig_hash } eq 'sha1' or something unsupported
66 2         18 $rsa_priv->use_sha1_hash;
67 2         10 $u->query_param('SigAlg', 'http://www.w3.org/2000/09/xmldsig#rsa-sha1');
68             }
69              
70 2         917 my $to_sign = $u->query;
71 2         3475 my $sig = encode_base64($rsa_priv->sign($to_sign), '');
72 2         16 $u->query_param('Signature', $sig);
73              
74 2         1163 my $url = $u->as_string;
75 2         56 return $url;
76             }
77              
78              
79             sub verify {
80 2     2 1 1390 my ($self, $url) = @_;
81 2         12 my $u = URI->new($url);
82              
83             # verify the response
84 2         217 my $sigalg = $u->query_param('SigAlg');
85              
86 2         501 my $cert = Crypt::OpenSSL::X509->new_from_string($self->cert);
87 2         108 my $rsa_pub = Crypt::OpenSSL::RSA->new_public_key($cert->pubkey);
88              
89 2         1587 my $signed;
90             my $saml_request;
91 2         35 my $sig = $u->query_param_delete('Signature');
92              
93             # Some IdPs (PingIdentity) seem to double encode the LogoutResponse URL
94 2 50 33     1137 if (defined $self->sls_double_encoded_response and $self->sls_double_encoded_response == 1) {
95             #if ($sigalg =~ m/%/) {
96 0         0 $signed = uri_decode($u->query);
97 0         0 $sig = uri_decode($sig);
98 0         0 $sigalg = uri_decode($sigalg);
99 0         0 $saml_request = uri_decode($u->query_param($self->param));
100             } else {
101 2         8 $signed = $u->query;
102 2         93 $saml_request = $u->query_param($self->param);
103             }
104              
105             # What can we say about this one Microsoft Azure uses lower case in the
106             # URL encoding %2f not %2F. As it is signed as %2f the resulting signed
107             # needs to change it to lowercase if the application layer reencoded the URL.
108 2 50 33     371 if (defined $self->sls_force_lcase_url_encoding and $self->sls_force_lcase_url_encoding == 1) {
109             # TODO: This is a hack.
110 0         0 $signed =~ s/(%..)/lc($1)/ge;
  0         0  
111             }
112              
113 2         19 $sig = decode_base64($sig);
114              
115 2 50       25 if ($sigalg eq 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha256') {
    50          
    50          
    50          
    50          
116 0         0 $rsa_pub->use_sha256_hash;
117             } elsif ($sigalg eq 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha224') {
118 0         0 $rsa_pub->use_sha224_hash;
119             } elsif ($sigalg eq 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha384') {
120 0         0 $rsa_pub->use_sha384_hash;
121             } elsif ($sigalg eq 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha512') {
122 0         0 $rsa_pub->use_sha512_hash;
123             } elsif ($sigalg eq 'http://www.w3.org/2000/09/xmldsig#rsa-sha1') {
124 2         14 $rsa_pub->use_sha1_hash;
125             } else {
126 0         0 die "Unsupported Signature Algorithim: $sigalg";
127             }
128              
129 2 50       180 die "bad sig" unless $rsa_pub->verify($signed, $sig);
130              
131             # unpack the SAML request
132 2         12 my $deflated = decode_base64($saml_request);
133 2         5 my $request = '';
134 2         18 rawinflate \$deflated => \$request;
135              
136             # unpack the relaystate
137 2         4177 my $relaystate = $u->query_param('RelayState');
138              
139 2         481 return ($request, $relaystate);
140             }
141              
142             __PACKAGE__->meta->make_immutable;
143              
144             __END__
145              
146             =pod
147              
148             =encoding UTF-8
149              
150             =head1 NAME
151              
152             Net::SAML2::Binding::Redirect - Net::SAML2::Binding::Redirect - HTTP Redirect binding for SAML
153              
154             =head1 VERSION
155              
156             version 0.42
157              
158             =head1 SYNOPSIS
159              
160             my $redirect = Net::SAML2::Binding::Redirect->new(
161             key => '/path/to/SPsign-nopw-key.pem', # Service Provider (SP) private key
162             url => $sso_url, # Service Provider Single Sign Out URL
163             param => 'SAMLRequest' OR 'SAMLResponse', # Type of request
164             cert => $idp->cert('signing') # Identity Provider (IdP) certificate
165             sig_hash => 'sha1', 'sha224', 'sha256', 'sha384', 'sha512' # Signature to sign request
166             );
167              
168             my $url = $redirect->sign($authnreq);
169              
170             my $ret = $redirect->verify($url);
171              
172             =head1 NAME
173              
174             Net::SAML2::Binding::Redirect
175              
176             =head1 METHODS
177              
178             =head2 new( ... )
179              
180             Constructor. Creates an instance of the Redirect binding.
181              
182             Arguments:
183              
184             =over
185              
186             =item B<key>
187              
188             The SP's (Service Provider) also known as your application's signing key
189             that your application uses to sign the AuthnRequest. Some IdPs may not
190             verify the signature.
191              
192             =item B<cert>
193              
194             IdP's (Identity Provider's) certificate that is used to verify a signed
195             Redirect from the IdP. It is used to verify the signature of the Redirect
196             response.
197              
198             =item B<url>
199              
200             IdP's SSO (Single Sign Out) service url for the Redirect binding
201              
202             =item B<param>
203              
204             query param name to use (SAMLRequest, SAMLResponse)
205              
206             =item B<sig_hash>
207              
208             RSA hash to use to sign request
209              
210             Supported:
211              
212             sha1, sha224, sha256, sha384, sha512
213              
214             sha1 is current default but will change by version 44
215              
216             =item B<sls_force_lcase_url_encoding>
217              
218             Specifies that the IdP requires the encoding of a URL to be in lowercase.
219             Necessary for a HTTP-Redirect of a LogoutResponse from Azure in particular.
220             True (1) or False (0). Some web frameworks and underlying http requests assume
221             that the encoding should be in the standard uppercase (%2F not %2f)
222              
223             =item B<sls_double_encoded_response>
224              
225             Specifies that the IdP response sent to the HTTP-Redirect is double encoded.
226             The double encoding requires it to be decoded prior to processing.
227              
228             =back
229              
230             =head2 sign( $request, $relaystate )
231              
232             Signs the given request, and returns the URL to which the user's
233             browser should be redirected.
234              
235             Accepts an optional RelayState parameter, a string which will be
236             returned to the requestor when the user returns from the
237             authentication process with the IdP.
238              
239             =head2 verify( $url )
240              
241             Decode a Redirect binding URL.
242              
243             Verifies the signature on the response.
244              
245             =head1 AUTHOR
246              
247             Chris Andrews <chrisa@cpan.org>
248              
249             =head1 COPYRIGHT AND LICENSE
250              
251             This software is copyright (c) 2021 by Chris Andrews and Others, see the git log.
252              
253             This is free software; you can redistribute it and/or modify it under
254             the same terms as the Perl 5 programming language system itself.
255              
256             =cut