File Coverage

blib/lib/Net/SAML2/IdP.pm
Criterion Covered Total %
statement 84 102 82.3
branch 12 26 46.1
condition 5 9 55.5
subroutine 16 17 94.1
pod 9 9 100.0
total 126 163 77.3


line stmt bran cond sub pod time code
1             package Net::SAML2::IdP;
2 6     6   42658 use Moose;
  6         481862  
  6         56  
3 6     6   48631 use MooseX::Types::URI qw/ Uri /;
  6         131246  
  6         76  
4              
5             our $VERSION = '0.42';
6              
7             # ABSTRACT: Net::SAML2::IdP - SAML Identity Provider object
8              
9              
10 6     6   13797 use Crypt::OpenSSL::Verify;
  6         23677  
  6         222  
11 6     6   60 use Crypt::OpenSSL::X509;
  6         17  
  6         282  
12 6     6   615 use HTTP::Request::Common;
  6         15681  
  6         524  
13 6     6   738 use LWP::UserAgent;
  6         23724  
  6         203  
14 6     6   727 use XML::LibXML;
  6         55675  
  6         61  
15 6     6   1574 use Net::SAML2::XML::Util qw/ no_comments /;
  6         15  
  6         9703  
16              
17              
18             has 'entityid' => (isa => 'Str', is => 'ro', required => 1);
19             has 'cacert' => (isa => 'Maybe[Str]', is => 'ro', required => 1);
20             has 'sso_urls' => (isa => 'HashRef[Str]', is => 'ro', required => 1);
21             has 'slo_urls' => (isa => 'Maybe[HashRef[Str]]', is => 'ro');
22             has 'art_urls' => (isa => 'Maybe[HashRef[Str]]', is => 'ro');
23             has 'certs' => (isa => 'HashRef[Str]', is => 'ro', required => 1);
24             has 'formats' => (isa => 'HashRef[Str]', is => 'ro', required => 1);
25             has 'sls_force_lcase_url_encoding' => (isa => 'Bool', is => 'ro', required => 0);
26             has 'sls_double_encoded_response' => (isa => 'Bool', is => 'ro', required => 0);
27             has 'default_format' => (isa => 'Str', is => 'ro', required => 1);
28              
29              
30             sub new_from_url {
31 0     0 1 0 my($class, %args) = @_;
32              
33 0         0 my $req = GET $args{url};
34 0         0 my $ua = LWP::UserAgent->new;
35              
36 0 0       0 if ( defined $args{ssl_opts} ) {
37 0         0 require LWP::Protocol::https;
38 0         0 $ua->ssl_opts( %{$args{ssl_opts}} );
  0         0  
39             }
40              
41 0         0 my $res = $ua->request($req);
42 0 0       0 if (! $res->is_success ) {
43 0         0 my $msg = "no metadata: " . $res->code . ": " . $res->message . "\n";
44 0         0 die $msg;
45             }
46              
47 0         0 my $xml = $res->content;
48              
49             return $class->new_from_xml(
50             xml => $xml,
51             cacert => $args{cacert},
52             sls_force_lcase_url_encoding => $args{sls_force_lcase_url_encoding},
53             sls_double_encoded_response => $args{sls_double_encoded_response},
54 0         0 );
55             }
56              
57              
58             sub new_from_xml {
59 7     7 1 7691 my($class, %args) = @_;
60              
61 7         52 my $dom = no_comments($args{xml});
62              
63 7         131 my $xpath = XML::LibXML::XPathContext->new($dom);
64 7         64 $xpath->registerNs('md', 'urn:oasis:names:tc:SAML:2.0:metadata');
65 7         32 $xpath->registerNs('ds', 'http://www.w3.org/2000/09/xmldsig#');
66              
67 7         14 my $data;
68              
69 7         50 for my $sso (
70             $xpath->findnodes(
71             '//md:EntityDescriptor/md:IDPSSODescriptor/md:SingleSignOnService')
72             )
73             {
74 21         712 my $binding = $sso->getAttribute('Binding');
75 21         240 $data->{SSO}->{$binding} = $sso->getAttribute('Location');
76             }
77              
78 7         107 for my $slo (
79             $xpath->findnodes(
80             '//md:EntityDescriptor/md:IDPSSODescriptor/md:SingleLogoutService')
81             )
82             {
83 21         1043 my $binding = $slo->getAttribute('Binding');
84 21         204 $data->{SLO}->{$binding} = $slo->getAttribute('Location');
85             }
86              
87 7         89 for my $art (
88             $xpath->findnodes(
89             '//md:EntityDescriptor/md:IDPSSODescriptor/md:ArtifactResolutionService')
90             )
91             {
92 7         488 my $binding = $art->getAttribute('Binding');
93 7         86 $data->{Art}->{$binding} = $art->getAttribute('Location');
94             }
95              
96 7         89 for my $format (
97             $xpath->findnodes('//md:EntityDescriptor/md:IDPSSODescriptor/md:NameIDFormat'))
98             {
99 49         691 $format = $format->string_value;
100 49         128 $format =~ s/^\s+|\s+$//g;
101 49         1216 my($short_format)
102             = $format =~ /urn:oasis:names:tc:SAML:(?:2.0|1.1):nameid-format:(.*)$/;
103 49 50       134 if(defined $short_format) {
104 49         149 $data->{NameIDFormat}->{$short_format} = $format;
105 49 100       137 $data->{DefaultFormat} = $short_format unless exists $data->{DefaultFormat};
106             }
107             }
108              
109             # NameIDFormat is an optional field and not provided in all metadata xml
110             # Microsoft in particular does not provide this field
111 7 50       37 if(!defined($data->{NameIDFormat})){
112 0         0 $data->{NameIDFormat}->{unspecified} = 'urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified';
113 0 0       0 $data->{DefaultFormat} = 'unspecified' unless exists $data->{DefaultFormat};
114             }
115              
116 7         34 for my $key (
117             $xpath->findnodes('//md:EntityDescriptor/md:IDPSSODescriptor/md:KeyDescriptor'))
118             {
119 7   50     310 my $use = $key->getAttribute('use') || 'signing';
120              
121             # We can't select by ds:KeyInfo/ds:X509Data/ds:X509Certificate
122             # because of https://rt.cpan.org/Public/Bug/Display.html?id=8784
123 7         120 my ($text)
124             = $key->findvalue("//*[local-name()='X509Certificate']")
125             =~ /^\s*(.+?)\s*$/s;
126              
127             # rewrap the base64 data from the metadata; it may not
128             # be wrapped at 64 characters as PEM requires
129 7         2436 $text =~ s/\n//g;
130              
131 7         21 my @lines;
132 7         94 while(length $text > 64) {
133 142         910 push @lines, substr $text, 0, 64, '';
134             }
135 7         21 push @lines, $text;
136              
137 7         56 $text = join "\n", @lines;
138              
139             # form a PEM certificate
140 7         97 $data->{Cert}->{$use}
141             = sprintf("-----BEGIN CERTIFICATE-----\n%s\n-----END CERTIFICATE-----\n",
142             $text);
143             }
144              
145             my $self = $class->new(
146             entityid => $xpath->findvalue('//md:EntityDescriptor/@entityID'),
147             sso_urls => $data->{SSO},
148             slo_urls => $data->{SLO} || {},
149             art_urls => $data->{Art} || {},
150             certs => $data->{Cert},
151             formats => $data->{NameIDFormat},
152             default_format => $data->{DefaultFormat},
153             cacert => $args{cacert},
154             sls_force_lcase_url_encoding => $args{sls_force_lcase_url_encoding},
155             sls_double_encoded_response => $args{sls_double_encoded_response},
156 7   50     27 );
      50        
157              
158 7         240 return $self;
159             }
160              
161              
162             sub BUILD {
163 7     7 1 22 my($self) = @_;
164              
165 7 50       285 if ($self->cacert) {
166 7         186 my $ca = Crypt::OpenSSL::Verify->new($self->cacert, { strict_certs => 0, });
167              
168 7         48 for my $use (keys %{$self->certs}) {
  7         288  
169 7         238 my $cert = Crypt::OpenSSL::X509->new_from_string($self->certs->{$use});
170             ## BUGBUG this is failing for valid things ...
171 7         28 eval { $ca->verify($cert) };
  7         922  
172 7 50       2747 if ($@) {
173 0         0 warn "Can't verify IdP '$use' cert: $@\n";
174             }
175             }
176             }
177             }
178              
179              
180             sub sso_url {
181 8     8 1 4262 my($self, $binding) = @_;
182 8         287 return $self->sso_urls->{$binding};
183             }
184              
185              
186             sub slo_url {
187 4     4 1 14 my ($self, $binding) = @_;
188 4 50       144 return $self->slo_urls ? $self->slo_urls->{$binding} : undef;
189             }
190              
191              
192             sub art_url {
193 3     3 1 11 my ($self, $binding) = @_;
194 3 50       102 return $self->art_urls ? $self->art_urls->{$binding} : undef;
195             }
196              
197              
198             sub cert {
199 6     6 1 1420 my($self, $use) = @_;
200 6         198 return $self->certs->{$use};
201             }
202              
203              
204             sub binding {
205 9     9 1 4365 my($self, $name) = @_;
206              
207 9         33 my $bindings = {
208             redirect => 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect',
209             soap => 'urn:oasis:names:tc:SAML:2.0:bindings:SOAP',
210             };
211              
212 9 50       33 if(exists $bindings->{$name}) {
213 9         52 return $bindings->{$name};
214             }
215              
216 0         0 return;
217             }
218              
219              
220             sub format {
221 6     6 1 18 my($self, $short_name) = @_;
222              
223 6 100 66     208 if(defined $short_name && exists $self->formats->{$short_name}) {
    50          
224 4         101 return $self->formats->{$short_name};
225             }
226             elsif($self->default_format) {
227 2         100 return $self->formats->{$self->default_format};
228             }
229              
230 0           return;
231             }
232              
233             __PACKAGE__->meta->make_immutable;
234              
235             __END__
236              
237             =pod
238              
239             =encoding UTF-8
240              
241             =head1 NAME
242              
243             Net::SAML2::IdP - Net::SAML2::IdP - SAML Identity Provider object
244              
245             =head1 VERSION
246              
247             version 0.42
248              
249             =head1 SYNOPSIS
250              
251             my $idp = Net::SAML2::IdP->new_from_url(
252             url => $url,
253             cacert => $cacert,
254             ssl_opts => # Optional options supported by LWP::Protocol::https
255             {
256             SSL_ca_file => '/your/directory/cacert.pem',
257             SSL_ca_path => '/etc/ssl/certs',
258             verify_hostname => 1,
259             }
260             );
261             my $sso_url = $idp->sso_url('urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect');
262              
263             =head1 NAME
264              
265             Net::SAML2::IdP - SAML Identity Provider object
266              
267             =head1 METHODS
268              
269             =head2 new( )
270              
271             Constructor
272              
273             =over
274              
275             =item B<entityid>
276              
277             =item B<sls_force_lcase_url_encoding>
278              
279             Specifies that the IdP requires the encoding of a URL to be in lowercase.
280             Necessary for a HTTP-Redirect of a LogoutResponse from Azure in particular.
281             True (1) or False (0). Some web frameworks and underlying http requests assume
282             that the encoding should be in the standard uppercase (%2F not %2f)
283              
284             =item B<sls_double_encoded_response>
285              
286             Specifies that the IdP response sent to the HTTP-Redirect is double encoded.
287             The double encoding requires it to be decoded prior to processing.
288              
289             =back
290              
291             =head2 new_from_url( url => $url, cacert => $cacert, ssl_opts => {} )
292              
293             Create an IdP object by retrieving the metadata at the given URL.
294              
295             Dies if the metadata can't be retrieved with reason.
296              
297             =head2 new_from_xml( xml => $xml, cacert => $cacert )
298              
299             Constructor. Create an IdP object using the provided metadata XML
300             document.
301              
302             =head2 BUILD ( hashref of the parameters passed to the constructor )
303              
304             Called after the object is created to validate the IdP using the cacert
305              
306             =head2 sso_url( $binding )
307              
308             Returns the url for the SSO service using the given binding. Binding
309             name should be the full URI.
310              
311             =head2 slo_url( $binding )
312              
313             Returns the url for the Single Logout Service using the given
314             binding. Binding name should be the full URI.
315              
316             =head2 art_url( $binding )
317              
318             Returns the url for the Artifact Resolution service using the given
319             binding. Binding name should be the full URI.
320              
321             =head2 cert( $use )
322              
323             Returns the IdP's certificate for the given use (e.g. C<signing>).
324              
325             =head2 binding( $name )
326              
327             Returns the full Binding URI for the given binding name (i.e. C<redirect> or C<soap>).
328             Includes this module's currently-supported bindings.
329              
330             =head2 format( $short_name )
331              
332             Returns the full NameID Format URI for the given short name.
333              
334             If no short name is provided, returns the URI for the default format,
335             the one listed first by the IdP.
336              
337             If no NameID formats were advertised by the IdP, returns undef.
338              
339             =head1 AUTHOR
340              
341             Chris Andrews <chrisa@cpan.org>
342              
343             =head1 COPYRIGHT AND LICENSE
344              
345             This software is copyright (c) 2021 by Chris Andrews and Others, see the git log.
346              
347             This is free software; you can redistribute it and/or modify it under
348             the same terms as the Perl 5 programming language system itself.
349              
350             =cut