File Coverage

blib/lib/Net/SAML2/IdP.pm
Criterion Covered Total %
statement 105 106 99.0
branch 20 24 83.3
condition 6 7 85.7
subroutine 18 18 100.0
pod 8 8 100.0
total 157 163 96.3


line stmt bran cond sub pod time code
1             package Net::SAML2::IdP;
2 11     11   48594 use Moose;
  11         956386  
  11         101  
3              
4             our $VERSION = '0.73'; # VERSION
5              
6 11     11   85620 use MooseX::Types::URI qw/ Uri /;
  11         262133  
  11         146  
7              
8             # ABSTRACT: SAML Identity Provider object
9              
10              
11 11     11   24236 use Crypt::OpenSSL::Verify;
  11         111227  
  11         361  
12 11     11   98 use Crypt::OpenSSL::X509;
  11         30  
  11         480  
13 11     11   2434 use HTTP::Request::Common;
  11         38049  
  11         932  
14 11     11   1624 use LWP::UserAgent;
  11         58174  
  11         330  
15 11     11   1560 use XML::LibXML;
  11         79825  
  11         83  
16 11     11   1744 use Try::Tiny;
  11         34  
  11         804  
17 11     11   1099 use Net::SAML2::XML::Util qw/ no_comments /;
  11         39  
  11         19519  
18              
19              
20             has 'entityid' => (isa => 'Str', is => 'ro', required => 1);
21             has 'cacert' => (isa => 'Maybe[Str]', is => 'ro', required => 1);
22             has 'sso_urls' => (isa => 'HashRef[Str]', is => 'ro', required => 1);
23             has 'slo_urls' => (isa => 'Maybe[HashRef[Str]]', is => 'ro');
24             has 'art_urls' => (isa => 'Maybe[HashRef[Str]]', is => 'ro');
25             has 'certs' => (isa => 'HashRef[ArrayRef[Str]]', is => 'ro', required => 1);
26              
27             has 'formats' => (
28             isa => 'HashRef[Str]',
29             is => 'ro',
30             required => 0,
31             default => sub { {} }
32             );
33             has 'default_format' => (isa => 'Str', is => 'ro', required => 0);
34             has 'debug' => (isa => 'Bool', is => 'ro', required => 0, default => 0);
35              
36              
37             sub new_from_url {
38 5     5 1 21690 my ($class, %args) = @_;
39              
40 5         118 my $req = GET $args{url};
41 5         8941 my $ua = $args{ua};
42 5 100       16 if (!$ua) {
43 4         22 $ua = LWP::UserAgent->new;
44 4 100       3905 if (defined $args{ssl_opts}) {
45 1         545 require LWP::Protocol::https;
46 1         74880 $ua->ssl_opts(%{ $args{ssl_opts} });
  1         12  
47             }
48             }
49              
50 5         66 my $res = $ua->request($req);
51 5 100       158 if (!$res->is_success) {
52 1         28 die(
53             sprintf(
54             "Error retrieving metadata: %s (%s)\n",
55             $res->message, $res->code
56             )
57             );
58             }
59              
60 4         140 my $xml = $res->decoded_content;
61              
62             return $class->new_from_xml(
63             xml => $xml,
64             cacert => $args{cacert},
65 4         80 );
66             }
67              
68              
69             sub new_from_xml {
70 19     19 1 14749 my($class, %args) = @_;
71              
72 19         113 my $dom = no_comments($args{xml});
73              
74 19         324 my $xpath = XML::LibXML::XPathContext->new($dom);
75 19         151 $xpath->registerNs('md', 'urn:oasis:names:tc:SAML:2.0:metadata');
76 19         82 $xpath->registerNs('ds', 'http://www.w3.org/2000/09/xmldsig#');
77              
78 19         41 my $data;
79              
80 19         51 my $basepath = '//md:EntityDescriptor/md:IDPSSODescriptor';
81              
82 19         176 for my $sso ($xpath->findnodes("$basepath/md:SingleSignOnService")) {
83 55         1652 my $binding = $sso->getAttribute('Binding');
84 55         613 $data->{SSO}->{$binding} = $sso->getAttribute('Location');
85             }
86              
87 19         293 for my $slo ($xpath->findnodes("$basepath/md:SingleLogoutService")) {
88 46         2048 my $binding = $slo->getAttribute('Binding');
89 46         455 $data->{SLO}->{$binding} = $slo->getAttribute('Location');
90             }
91              
92 19         440 for my $art ($xpath->findnodes("$basepath/md:ArtifactResolutionService")) {
93 15         1000 my $binding = $art->getAttribute('Binding');
94 15         187 $data->{Art}->{$binding} = $art->getAttribute('Location');
95             }
96              
97 19         327 for my $format ($xpath->findnodes("$basepath/md:NameIDFormat")) {
98 95         1305 $format = $format->string_value;
99 95         243 $format =~ s/^\s+//g;
100 95         1308 $format =~ s/\s+$//g;
101              
102 95         531 my($short_format)
103             = $format =~ /urn:oasis:names:tc:SAML:(?:2.0|1.1):nameid-format:(.*)$/;
104              
105 95 50       308 if(defined $short_format) {
106 95         268 $data->{NameIDFormat}{$short_format} = $format;
107 95 100       302 $data->{DefaultFormat} = $short_format unless exists $data->{DefaultFormat};
108             }
109             }
110              
111 19         140 my %certs = ();
112 19         112 for my $key ($xpath->findnodes("$basepath/md:KeyDescriptor")) {
113 24         763 my $use = $key->getAttribute('use');
114 24         348 my $pem = $class->_get_pem_from_keynode($key);
115 24 100       90 if (!$use) {
116 1         5 push(@{$certs{signing}}, $pem);
  1         4  
117 1         2 push(@{$certs{encryption}}, $pem);
  1         3  
118             }
119             else {
120 23         51 push(@{$certs{$use}}, $pem);
  23         117  
121             }
122             }
123              
124             return $class->new(
125             entityid => $xpath->findvalue('//md:EntityDescriptor/@entityID'),
126             sso_urls => $data->{SSO},
127             slo_urls => $data->{SLO} || {},
128             art_urls => $data->{Art} || {},
129             certs => \%certs,
130             cacert => $args{cacert},
131             debug => $args{debug},
132             $data->{DefaultFormat}
133             ? (
134             default_format => $data->{DefaultFormat},
135             formats => $data->{NameIDFormat},
136             )
137 19 100 100     72 : (),
      100        
138             );
139              
140             }
141              
142             sub _get_pem_from_keynode {
143 24     24   52 my $self = shift;
144 24         47 my $node = shift;
145              
146 24         185 $node->setNamespace('http://www.w3.org/2000/09/xmldsig#', 'ds');
147              
148 24         675 my ($text)
149             = $node->findvalue("ds:KeyInfo/ds:X509Data/ds:X509Certificate", $node)
150             =~ /^\s*(.+?)\s*$/s;
151              
152             # rewrap the base64 data from the metadata; it may not
153             # be wrapped at 64 characters as PEM requires
154 24         6397 $text =~ s/\n//g;
155              
156 24         65 my @lines;
157 24         286 while(length $text > 64) {
158 428         2535 push @lines, substr $text, 0, 64, '';
159             }
160 24         66 push @lines, $text;
161              
162 24         175 $text = join "\n", @lines;
163              
164 24         185 return "-----BEGIN CERTIFICATE-----\n$text\n-----END CERTIFICATE-----\n";
165             }
166              
167              
168             # BUILDARGS ( hashref of the parameters passed to the constructor )
169             #
170             # Called after the object is created to validate the IdP using the cacert
171             #
172              
173             around BUILDARGS => sub {
174             my $orig = shift;
175             my $self = shift;
176              
177             my %params = @_;
178              
179             if ($params{cacert}) {
180             my $ca = Crypt::OpenSSL::Verify->new($params{cacert}, { strict_certs => 0, });
181              
182             my %certificates;
183             my @errors;
184             for my $use (keys %{$params{certs}}) {
185             my $certs = $params{certs}{$use};
186             for my $pem (@{$certs}) {
187             my $cert = Crypt::OpenSSL::X509->new_from_string($pem);
188             try {
189             $ca->verify($cert);
190             push(@{$certificates{$use}}, $pem);
191             }
192             catch { push (@errors, $_); };
193             }
194             }
195              
196             if ( $params{debug} && @errors ) {
197             warn "Can't verify IdP cert(s): " . join(", ", @errors);
198             }
199              
200             $params{certs} = \%certificates;
201             }
202              
203             return $self->$orig(%params);
204             };
205              
206              
207             sub sso_url {
208 19     19 1 4635 my($self, $binding) = @_;
209 19         631 return $self->sso_urls->{$binding};
210             }
211              
212              
213             sub slo_url {
214 8     8 1 5483 my ($self, $binding) = @_;
215 8 50       279 return $self->slo_urls ? $self->slo_urls->{$binding} : undef;
216             }
217              
218              
219             sub art_url {
220 6     6 1 25 my ($self, $binding) = @_;
221 6 50       203 return $self->art_urls ? $self->art_urls->{$binding} : undef;
222             }
223              
224              
225             sub cert {
226 16     16 1 9069 my($self, $use) = @_;
227 16         480 return $self->certs->{$use};
228             }
229              
230              
231             sub binding {
232 20     20 1 6445 my($self, $name) = @_;
233              
234 20         86 my $bindings = {
235             post => 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST',
236             redirect => 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect',
237             soap => 'urn:oasis:names:tc:SAML:2.0:bindings:SOAP',
238             };
239              
240 20 50       71 if(exists $bindings->{$name}) {
241 20         113 return $bindings->{$name};
242             }
243              
244 0         0 return;
245             }
246              
247              
248             sub format {
249 12     12 1 47 my($self, $short_name) = @_;
250              
251 12 100 66     411 if(defined $short_name && exists $self->formats->{$short_name}) {
    100          
252 6         153 return $self->formats->{$short_name};
253             }
254             elsif($self->default_format) {
255 5         126 return $self->formats->{$self->default_format};
256             }
257              
258 1         5 return;
259             }
260              
261             __PACKAGE__->meta->make_immutable;
262              
263             __END__
264              
265             =pod
266              
267             =encoding UTF-8
268              
269             =head1 NAME
270              
271             Net::SAML2::IdP - SAML Identity Provider object
272              
273             =head1 VERSION
274              
275             version 0.73
276              
277             =head1 SYNOPSIS
278              
279             my $idp = Net::SAML2::IdP->new_from_url(
280             url => $url,
281             cacert => $cacert,
282             ssl_opts => # Optional options supported by LWP::Protocol::https
283             {
284             SSL_ca_file => '/your/directory/cacert.pem',
285             SSL_ca_path => '/etc/ssl/certs',
286             verify_hostname => 1,
287             }
288             );
289             my $sso_url = $idp->sso_url('urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect');
290              
291             Note that LWP::UserAgent is used which means that environment variables
292             may affect the use of https see:
293              
294             =over
295              
296             =item * L<PERL_LWP_SSL_CA_FILE and HTTPS_CA_FILE|https://metacpan.org/pod/LWP::UserAgent#SSL_ca_file-=%3E-$path>
297              
298             =item * L<PERL_LWP_SSL_CA_PATH and HTTPS_CA_DIR|https://metacpan.org/pod/LWP::UserAgent#SSL_ca_path-=%3E-$path>
299              
300             =back
301              
302             =head1 NAME
303              
304             Net::SAML2::IdP - SAML Identity Provider object
305              
306             =head1 METHODS
307              
308             =head2 new( )
309              
310             Constructor
311              
312             =over
313              
314             =item B<entityid>
315              
316             =back
317              
318             =head2 new_from_url( url => $url, cacert => $cacert, ssl_opts => {} )
319              
320             Create an IdP object by retrieving the metadata at the given URL.
321              
322             Dies if the metadata can't be retrieved with reason.
323              
324             =head2 new_from_xml( xml => $xml, cacert => $cacert )
325              
326             Constructor. Create an IdP object using the provided metadata XML
327             document.
328              
329             =head2 sso_url( $binding )
330              
331             Returns the url for the SSO service using the given binding. Binding
332             name should be the full URI.
333              
334             =head2 slo_url( $binding )
335              
336             Returns the url for the Single Logout Service using the given
337             binding. Binding name should be the full URI.
338              
339             =head2 art_url( $binding )
340              
341             Returns the url for the Artifact Resolution service using the given
342             binding. Binding name should be the full URI.
343              
344             =head2 cert( $use )
345              
346             Returns the IdP's certificates for the given use (e.g. C<signing>).
347              
348             IdP's are generated from the metadata it is possible for multiple certificates
349             to be contained in the metadata and therefore possible for them to be there to
350             be multiple verified certs in $self->certs. At this point any certs in the IdP
351             have been verified and are valid for the specified use. All certs are of type
352             $use are returned.
353              
354             =head2 binding( $name )
355              
356             Returns the full Binding URI for the given binding name (i.e. C<redirect> or C<soap>).
357             Includes this module's currently-supported bindings.
358              
359             =head2 format( $short_name )
360              
361             Returns the full NameID Format URI for the given short name.
362              
363             If no short name is provided, returns the URI for the default format,
364             the one listed first by the IdP.
365              
366             If no NameID formats were advertised by the IdP, returns undef.
367              
368             =head1 AUTHORS
369              
370             =over 4
371              
372             =item *
373              
374             Chris Andrews <chrisa@cpan.org>
375              
376             =item *
377              
378             Timothy Legge <timlegge@gmail.com>
379              
380             =back
381              
382             =head1 COPYRIGHT AND LICENSE
383              
384             This software is copyright (c) 2023 by Venda Ltd, see the CONTRIBUTORS file for others.
385              
386             This is free software; you can redistribute it and/or modify it under
387             the same terms as the Perl 5 programming language system itself.
388              
389             =cut