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   105 use strict;
  13         38  
  13         480  
4 13     13   73 use warnings;
  13         30  
  13         403  
5              
6 13     13   101 use Moose;
  13         29  
  13         105  
7 13     13   96470 use MooseX::Types::URI qw/ Uri /;
  13         38  
  13         200  
8              
9             our $VERSION = '0.43';
10              
11             # ABSTRACT: Net::SAML2::Binding::Redirect - HTTP Redirect binding for SAML
12              
13              
14 13     13   27604 use MIME::Base64 qw/ encode_base64 decode_base64 /;
  13         46  
  13         928  
15 13     13   10755 use IO::Compress::RawDeflate qw/ rawdeflate /;
  13         406480  
  13         1309  
16 13     13   8616 use IO::Uncompress::RawInflate qw/ rawinflate /;
  13         172043  
  13         1279  
17 13     13   124 use URI;
  13         31  
  13         327  
18 13     13   101 use URI::QueryParam;
  13         33  
  13         279  
19 13     13   7431 use Crypt::OpenSSL::RSA;
  13         66198  
  13         658  
20 13     13   113 use Crypt::OpenSSL::X509;
  13         29  
  13         706  
21 13     13   8036 use File::Slurp qw/ read_file /;
  13         231215  
  13         977  
22 13     13   6914 use URI::Encode qw/uri_decode/;
  13         19177  
  13         11767  
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 645 my ($self, $request, $relaystate) = @_;
36              
37 2         19 my $input = "$request";
38 2         7 my $output = '';
39              
40 2         16 rawdeflate \$input => \$output;
41 2         4488 my $req = encode_base64($output, '');
42              
43 2         88 my $u = URI->new($self->url);
44 2         232 $u->query_param($self->param, $req);
45 2 100       551 $u->query_param('RelayState', $relaystate) if defined $relaystate;
46              
47 2         439 my $key_string = read_file($self->key);
48 2         1107 my $rsa_priv = Crypt::OpenSSL::RSA->new_private_key($key_string);
49              
50 2 50 33     20 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         14 $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         842 my $to_sign = $u->query;
71 2         3395 my $sig = encode_base64($rsa_priv->sign($to_sign), '');
72 2         17 $u->query_param('Signature', $sig);
73              
74 2         1166 my $url = $u->as_string;
75 2         51 return $url;
76             }
77              
78              
79             sub verify {
80 2     2 1 728 my ($self, $url) = @_;
81 2         11 my $u = URI->new($url);
82              
83             # verify the response
84 2         179 my $sigalg = $u->query_param('SigAlg');
85              
86 2         542 my $cert = Crypt::OpenSSL::X509->new_from_string($self->cert);
87 2         94 my $rsa_pub = Crypt::OpenSSL::RSA->new_public_key($cert->pubkey);
88              
89 2         1435 my $signed;
90             my $saml_request;
91 2         34 my $sig = $u->query_param_delete('Signature');
92              
93             # Some IdPs (PingIdentity) seem to double encode the LogoutResponse URL
94 2 50 33     1076 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         10 $signed = $u->query;
102 2         90 $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     401 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         13 $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         12 $rsa_pub->use_sha1_hash;
125             } else {
126 0         0 die "Unsupported Signature Algorithim: $sigalg";
127             }
128              
129 2 50       173 die "bad sig" unless $rsa_pub->verify($signed, $sig);
130              
131             # unpack the SAML request
132 2         15 my $deflated = decode_base64($saml_request);
133 2         5 my $request = '';
134 2         17 rawinflate \$deflated => \$request;
135              
136             # unpack the relaystate
137 2         3996 my $relaystate = $u->query_param('RelayState');
138              
139 2         354 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.43
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