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   39486 use Moose;
  6         446787  
  6         51  
3 6     6   43941 use MooseX::Types::URI qw/ Uri /;
  6         112554  
  6         69  
4              
5             our $VERSION = '0.43';
6              
7             # ABSTRACT: Net::SAML2::IdP - SAML Identity Provider object
8              
9              
10 6     6   12498 use Crypt::OpenSSL::Verify;
  6         23542  
  6         197  
11 6     6   38 use Crypt::OpenSSL::X509;
  6         11  
  6         263  
12 6     6   540 use HTTP::Request::Common;
  6         12705  
  6         462  
13 6     6   1491 use LWP::UserAgent;
  6         20797  
  6         157  
14 6     6   664 use XML::LibXML;
  6         51960  
  6         42  
15 6     6   1577 use Net::SAML2::XML::Util qw/ no_comments /;
  6         16  
  6         9069  
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 7063 my($class, %args) = @_;
60              
61 7         47 my $dom = no_comments($args{xml});
62              
63 7         126 my $xpath = XML::LibXML::XPathContext->new($dom);
64 7         55 $xpath->registerNs('md', 'urn:oasis:names:tc:SAML:2.0:metadata');
65 7         30 $xpath->registerNs('ds', 'http://www.w3.org/2000/09/xmldsig#');
66              
67 7         12 my $data;
68              
69 7         42 for my $sso (
70             $xpath->findnodes(
71             '//md:EntityDescriptor/md:IDPSSODescriptor/md:SingleSignOnService')
72             )
73             {
74 21         658 my $binding = $sso->getAttribute('Binding');
75 21         376 $data->{SSO}->{$binding} = $sso->getAttribute('Location');
76             }
77              
78 7         91 for my $slo (
79             $xpath->findnodes(
80             '//md:EntityDescriptor/md:IDPSSODescriptor/md:SingleLogoutService')
81             )
82             {
83 21         977 my $binding = $slo->getAttribute('Binding');
84 21         203 $data->{SLO}->{$binding} = $slo->getAttribute('Location');
85             }
86              
87 7         77 for my $art (
88             $xpath->findnodes(
89             '//md:EntityDescriptor/md:IDPSSODescriptor/md:ArtifactResolutionService')
90             )
91             {
92 7         456 my $binding = $art->getAttribute('Binding');
93 7         87 $data->{Art}->{$binding} = $art->getAttribute('Location');
94             }
95              
96 7         88 for my $format (
97             $xpath->findnodes('//md:EntityDescriptor/md:IDPSSODescriptor/md:NameIDFormat'))
98             {
99 49         567 $format = $format->string_value;
100 49         118 $format =~ s/^\s+|\s+$//g;
101 49         1163 my($short_format)
102             = $format =~ /urn:oasis:names:tc:SAML:(?:2.0|1.1):nameid-format:(.*)$/;
103 49 50       125 if(defined $short_format) {
104 49         151 $data->{NameIDFormat}->{$short_format} = $format;
105 49 100       133 $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       39 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         32 for my $key (
117             $xpath->findnodes('//md:EntityDescriptor/md:IDPSSODescriptor/md:KeyDescriptor'))
118             {
119 7   50     298 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         125 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         2412 $text =~ s/\n//g;
130              
131 7         22 my @lines;
132 7         86 while(length $text > 64) {
133 142         928 push @lines, substr $text, 0, 64, '';
134             }
135 7         20 push @lines, $text;
136              
137 7         56 $text = join "\n", @lines;
138              
139             # form a PEM certificate
140 7         92 $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     28 );
      50        
157              
158 7         246 return $self;
159             }
160              
161              
162             sub BUILD {
163 7     7 1 37 my($self) = @_;
164              
165 7 50       249 if ($self->cacert) {
166 7         179 my $ca = Crypt::OpenSSL::Verify->new($self->cacert, { strict_certs => 0, });
167              
168 7         50 for my $use (keys %{$self->certs}) {
  7         297  
169 7         175 my $cert = Crypt::OpenSSL::X509->new_from_string($self->certs->{$use});
170             ## BUGBUG this is failing for valid things ...
171 7         27 eval { $ca->verify($cert) };
  7         940  
172 7 50       2665 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 4731 my($self, $binding) = @_;
182 8         293 return $self->sso_urls->{$binding};
183             }
184              
185              
186             sub slo_url {
187 4     4 1 13 my ($self, $binding) = @_;
188 4 50       142 return $self->slo_urls ? $self->slo_urls->{$binding} : undef;
189             }
190              
191              
192             sub art_url {
193 3     3 1 14 my ($self, $binding) = @_;
194 3 50       104 return $self->art_urls ? $self->art_urls->{$binding} : undef;
195             }
196              
197              
198             sub cert {
199 6     6 1 1196 my($self, $use) = @_;
200 6         192 return $self->certs->{$use};
201             }
202              
203              
204             sub binding {
205 9     9 1 3466 my($self, $name) = @_;
206              
207 9         36 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       31 if(exists $bindings->{$name}) {
213 9         42 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     227 if(defined $short_name && exists $self->formats->{$short_name}) {
    50          
224 4         95 return $self->formats->{$short_name};
225             }
226             elsif($self->default_format) {
227 2         49 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.43
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