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