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   39126 use Moose;
  11         850193  
  11         80  
3              
4             our $VERSION = '0.72'; # TRIAL VERSION
5              
6 11     11   71886 use MooseX::Types::URI qw/ Uri /;
  11         236690  
  11         120  
7              
8             # ABSTRACT: SAML Identity Provider object
9              
10              
11 11     11   20802 use Crypt::OpenSSL::Verify;
  11         92239  
  11         274  
12 11     11   70 use Crypt::OpenSSL::X509;
  11         19  
  11         406  
13 11     11   2426 use HTTP::Request::Common;
  11         34229  
  11         785  
14 11     11   1438 use LWP::UserAgent;
  11         54539  
  11         298  
15 11     11   1680 use XML::LibXML;
  11         72762  
  11         76  
16 11     11   1499 use Try::Tiny;
  11         23  
  11         695  
17 11     11   1001 use Net::SAML2::XML::Util qw/ no_comments /;
  11         23  
  11         16730  
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 16186 my ($class, %args) = @_;
39              
40 5         99 my $req = GET $args{url};
41 5         7318 my $ua = $args{ua};
42 5 100       14 if (!$ua) {
43 4         21 $ua = LWP::UserAgent->new;
44 4 100       3145 if (defined $args{ssl_opts}) {
45 1         475 require LWP::Protocol::https;
46 1         61829 $ua->ssl_opts(%{ $args{ssl_opts} });
  1         10  
47             }
48             }
49              
50 5         47 my $res = $ua->request($req);
51 5 100       132 if (!$res->is_success) {
52 1         22 die(
53             sprintf(
54             "Error retrieving metadata: %s (%s)\n",
55             $res->message, $res->code
56             )
57             );
58             }
59              
60 4         115 my $xml = $res->decoded_content;
61              
62             return $class->new_from_xml(
63             xml => $xml,
64             cacert => $args{cacert},
65 4         62 );
66             }
67              
68              
69             sub new_from_xml {
70 19     19 1 10603 my($class, %args) = @_;
71              
72 19         95 my $dom = no_comments($args{xml});
73              
74 19         278 my $xpath = XML::LibXML::XPathContext->new($dom);
75 19         125 $xpath->registerNs('md', 'urn:oasis:names:tc:SAML:2.0:metadata');
76 19         66 $xpath->registerNs('ds', 'http://www.w3.org/2000/09/xmldsig#');
77              
78 19         32 my $data;
79              
80 19         35 my $basepath = '//md:EntityDescriptor/md:IDPSSODescriptor';
81              
82 19         141 for my $sso ($xpath->findnodes("$basepath/md:SingleSignOnService")) {
83 55         1295 my $binding = $sso->getAttribute('Binding');
84 55         505 $data->{SSO}->{$binding} = $sso->getAttribute('Location');
85             }
86              
87 19         190 for my $slo ($xpath->findnodes("$basepath/md:SingleLogoutService")) {
88 46         1702 my $binding = $slo->getAttribute('Binding');
89 46         369 $data->{SLO}->{$binding} = $slo->getAttribute('Location');
90             }
91              
92 19         337 for my $art ($xpath->findnodes("$basepath/md:ArtifactResolutionService")) {
93 15         850 my $binding = $art->getAttribute('Binding');
94 15         151 $data->{Art}->{$binding} = $art->getAttribute('Location');
95             }
96              
97 19         315 for my $format ($xpath->findnodes("$basepath/md:NameIDFormat")) {
98 95         1075 $format = $format->string_value;
99 95         203 $format =~ s/^\s+//g;
100 95         1134 $format =~ s/\s+$//g;
101              
102 95         465 my($short_format)
103             = $format =~ /urn:oasis:names:tc:SAML:(?:2.0|1.1):nameid-format:(.*)$/;
104              
105 95 50       203 if(defined $short_format) {
106 95         291 $data->{NameIDFormat}{$short_format} = $format;
107 95 100       227 $data->{DefaultFormat} = $short_format unless exists $data->{DefaultFormat};
108             }
109             }
110              
111 19         122 my %certs = ();
112 19         75 for my $key ($xpath->findnodes("$basepath/md:KeyDescriptor")) {
113 24         702 my $use = $key->getAttribute('use');
114 24         252 my $pem = $class->_get_pem_from_keynode($key);
115 24 100       80 if (!$use) {
116 1         6 push(@{$certs{signing}}, $pem);
  1         4  
117 1         2 push(@{$certs{encryption}}, $pem);
  1         3  
118             }
119             else {
120 23         54 push(@{$certs{$use}}, $pem);
  23         98  
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     55 : (),
      100        
138             );
139              
140             }
141              
142             sub _get_pem_from_keynode {
143 24     24   53 my $self = shift;
144 24         39 my $node = shift;
145              
146 24         146 $node->setNamespace('http://www.w3.org/2000/09/xmldsig#', 'ds');
147              
148 24         543 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         5303 $text =~ s/\n//g;
155              
156 24         59 my @lines;
157 24         215 while(length $text > 64) {
158 428         2131 push @lines, substr $text, 0, 64, '';
159             }
160 24         58 push @lines, $text;
161              
162 24         132 $text = join "\n", @lines;
163              
164 24         152 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 3719 my($self, $binding) = @_;
209 19         539 return $self->sso_urls->{$binding};
210             }
211              
212              
213             sub slo_url {
214 8     8 1 4301 my ($self, $binding) = @_;
215 8 50       229 return $self->slo_urls ? $self->slo_urls->{$binding} : undef;
216             }
217              
218              
219             sub art_url {
220 6     6 1 19 my ($self, $binding) = @_;
221 6 50       251 return $self->art_urls ? $self->art_urls->{$binding} : undef;
222             }
223              
224              
225             sub cert {
226 16     16 1 6838 my($self, $use) = @_;
227 16         392 return $self->certs->{$use};
228             }
229              
230              
231             sub binding {
232 20     20 1 5134 my($self, $name) = @_;
233              
234 20         70 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       56 if(exists $bindings->{$name}) {
241 20         94 return $bindings->{$name};
242             }
243              
244 0         0 return;
245             }
246              
247              
248             sub format {
249 12     12 1 38 my($self, $short_name) = @_;
250              
251 12 100 66     360 if(defined $short_name && exists $self->formats->{$short_name}) {
    100          
252 6         121 return $self->formats->{$short_name};
253             }
254             elsif($self->default_format) {
255 5         106 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.72
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