File Coverage

blib/lib/Net/SAML2/SP.pm
Criterion Covered Total %
statement 133 145 91.7
branch 36 48 75.0
condition 6 10 60.0
subroutine 36 39 92.3
pod 14 14 100.0
total 225 256 87.8


line stmt bran cond sub pod time code
1 26     26   419854 use strict;
  26         74  
  26         993  
2 26     26   163 use warnings;
  26         83  
  26         2686  
3             package Net::SAML2::SP;
4             our $VERSION = '0.74'; # VERSION
5              
6 26     26   209 use Moose;
  26         70  
  26         262  
7              
8 26     26   185754 use Carp qw(croak);
  26         70  
  26         2005  
9 26     26   188 use Crypt::OpenSSL::X509;
  26         62  
  26         1056  
10 26     26   178 use Digest::MD5 ();
  26         694  
  26         744  
11 26     26   169 use List::Util qw(first none);
  26         55  
  26         2015  
12 26     26   180 use MooseX::Types::URI qw/ Uri /;
  26         66  
  26         375  
13 26     26   54847 use MooseX::Types::Common::String qw/ NonEmptySimpleStr /;
  26         195858  
  26         278  
14 26     26   87292 use Net::SAML2::Binding::POST;
  26         10478  
  26         1108  
15 26     26   15920 use Net::SAML2::Binding::Redirect;
  26         11159  
  26         1265  
16 26     26   16495 use Net::SAML2::Binding::SOAP;
  26         10910  
  26         1210  
17 26     26   1446 use Net::SAML2::Protocol::AuthnRequest;
  26         908  
  26         687  
18 26     26   15442 use Net::SAML2::Protocol::LogoutRequest;
  26         11546  
  26         1210  
19 26     26   273 use Net::SAML2::Util ();
  26         69  
  26         706  
20 26     26   149 use URN::OASIS::SAML2 qw(:bindings :urn);
  26         61  
  26         5283  
21 26     26   217 use XML::Generator;
  26         64  
  26         228  
22              
23             # ABSTRACT: SAML Service Provider object
24              
25              
26              
27              
28             has 'url' => (isa => Uri, is => 'ro', required => 1, coerce => 1);
29             has 'id' => (isa => 'Str', is => 'ro', required => 1);
30             has 'cert' => (isa => 'Str', is => 'ro', required => 1, predicate => 'has_cert');
31             has 'key' => (isa => 'Str', is => 'ro', required => 1);
32             has 'cacert' => (isa => 'Str', is => 'rw', required => 0, predicate => 'has_cacert');
33              
34             has 'encryption_key' => (isa => 'Str', is => 'ro', required => 0, predicate => 'has_encryption_key');
35             has 'error_url' => (isa => Uri, is => 'ro', required => 1, coerce => 1);
36             has 'org_name' => (isa => 'Str', is => 'ro', required => 1);
37             has 'org_display_name' => (isa => 'Str', is => 'ro', required => 1);
38             has 'org_contact' => (isa => 'Str', is => 'ro', required => 1);
39             has 'org_url' => (isa => 'Str', is => 'ro', required => 0);
40              
41             # These are no longer in use, but are not removed by the off change that
42             # someone that extended us or added a role to us with these params.
43             has 'slo_url_soap' => (isa => 'Str', is => 'ro', required => 0);
44             has 'slo_url_post' => (isa => 'Str', is => 'ro', required => 0);
45             has 'slo_url_redirect' => (isa => 'Str', is => 'ro', required => 0);
46             has 'acs_url_post' => (isa => 'Str', is => 'ro', required => 0);
47             has 'acs_url_artifact' => (isa => 'Str', is => 'ro', required => 0);
48              
49             has '_cert_text' => (isa => 'Str', is => 'ro', init_arg => undef, builder => '_build_cert_text', lazy => 1);
50              
51             has '_encryption_key_text' => (isa => 'Str', is => 'ro', init_arg => undef, builder => '_build_encryption_key_text', lazy => 1);
52             has 'authnreq_signed' => (isa => 'Bool', is => 'ro', required => 0, default => 1);
53             has 'want_assertions_signed' => (isa => 'Bool', is => 'ro', required => 0, default => 1);
54              
55             has 'sign_metadata' => (isa => 'Bool', is => 'ro', required => 0, default => 1);
56              
57             has assertion_consumer_service => (is => 'ro', isa => 'ArrayRef', required => 1);
58             has single_logout_service => (is => 'ro', isa => 'ArrayRef', required => 1);
59              
60             around BUILDARGS => sub {
61             my $orig = shift;
62             my $self = shift;
63              
64             my %args = @_;
65              
66             if (!$args{single_logout_service}) {
67             #warn "Deprecation warning, please upgrade your code to use ..";
68             my @slo;
69             if (my $slo = $args{slo_url_soap}) {
70             push(
71             @slo,
72             {
73             Binding => BINDING_SOAP,
74             Location => $args{url} . $slo,
75             }
76             );
77             }
78             if (my $slo = $args{slo_url_redirect}) {
79             push(
80             @slo,
81             {
82             Binding => BINDING_HTTP_REDIRECT,
83             Location => $args{url} . $slo,
84             }
85             );
86             }
87             if (my $slo = $args{slo_url_post}) {
88             push(
89             @slo,
90             {
91             Binding => BINDING_HTTP_POST,
92             Location => $args{url} . $slo,
93             }
94             );
95             }
96             $args{single_logout_service} = \@slo;
97             }
98              
99             if (!$args{assertion_consumer_service}) {
100             #warn "Deprecation warning, please upgrade your code to use ..";
101             my @acs;
102             if (my $acs = delete $args{acs_url_post}) {
103             push(
104             @acs,
105             {
106             Binding => BINDING_HTTP_POST,
107             Location => $args{url} . $acs,
108             isDefault => 'true',
109             }
110             );
111             }
112             if (my $acs = $args{acs_url_artifact}) {
113             push(
114             @acs,
115             {
116             Binding => BINDING_HTTP_ARTIFACT,
117             Location => $args{url} . $acs,
118             isDefault => 'false',
119             }
120             );
121             }
122              
123             $args{assertion_consumer_service} = \@acs;
124             }
125             if (!@{$args{assertion_consumer_service}}) {
126             croak("You don't have any Assertion Consumer Services configured!");
127             }
128              
129             my $acs_index = 1;
130             if (none { $_->{index} } @{$args{assertion_consumer_service}}) {
131             foreach (@{$args{assertion_consumer_service}}) {
132             $_->{index} = $acs_index;
133             ++$acs_index;
134             }
135             }
136              
137             return $self->$orig(%args);
138             };
139              
140             sub _build_encryption_key_text {
141 2     2   5 my ($self) = @_;
142              
143 2 100       50 return '' unless $self->has_encryption_key;
144 1         25 my $cert = Crypt::OpenSSL::X509->new_from_file($self->encryption_key);
145 1         23 my $text = $cert->as_string;
146 1         46 $text =~ s/-----[^-]*-----//gm;
147 1         41 return $text;
148             }
149              
150             sub _build_cert_text {
151 7     7   16 my ($self) = @_;
152              
153 7 50       159 return '' unless $self->has_cert;
154 7         135 my $cert = Crypt::OpenSSL::X509->new_from_file($self->cert);
155 7         209 my $text = $cert->as_string;
156 7         70 $text =~ s/-----[^-]*-----//gm;
157 7         260 return $text;
158             }
159              
160              
161             sub authn_request {
162 4     4 1 16 my $self = shift;
163 4         9 my $destination = shift;
164 4         11 my $nameid_format = shift;
165 4         11 my (%params) = @_;
166              
167 4   100     40 return Net::SAML2::Protocol::AuthnRequest->new(
168             issueinstant => DateTime->now,
169             issuer => $self->id,
170             destination => $destination,
171             nameid_format => $nameid_format || '',
172             %params,
173             );
174              
175             }
176              
177              
178             sub logout_request {
179 3     3 1 32 my ($self, $destination, $nameid, $nameid_format, $session, $params) = @_;
180              
181             my $logout_req = Net::SAML2::Protocol::LogoutRequest->new(
182             issuer => $self->id,
183             destination => $destination,
184             nameid => $nameid,
185             session => $session,
186             NonEmptySimpleStr->check($nameid_format)
187             ? (nameid_format => $nameid_format)
188             : (),
189             (defined $params->{sp_name_qualifier})
190             ? (affiliation_group_id => $params->{sp_name_qualifier})
191             : (),
192             (defined $params->{name_qualifier})
193             ? (name_qualifier => $params->{name_qualifier})
194             : (),
195             (defined $params->{include_name_qualifier})
196             ? ( include_name_qualifier => $params->{include_name_qualifier} )
197 3 50       120 : ( include_name_qualifier => 1),
    100          
    100          
    50          
198             );
199 3         22 return $logout_req;
200             }
201              
202              
203             sub logout_response {
204 0     0 1 0 my ($self, $destination, $status, $response_to) = @_;
205              
206 0         0 my $status_uri = Net::SAML2::Protocol::LogoutResponse->status_uri($status);
207 0         0 my $logout_req = Net::SAML2::Protocol::LogoutResponse->new(
208             issuer => $self->id,
209             destination => $destination,
210             status => $status_uri,
211             response_to => $response_to,
212             );
213              
214 0         0 return $logout_req;
215             }
216              
217              
218             sub artifact_request {
219 0     0 1 0 my ($self, $destination, $artifact) = @_;
220              
221 0         0 my $artifact_request = Net::SAML2::Protocol::ArtifactResolve->new(
222             issuer => $self->id,
223             destination => $destination,
224             artifact => $artifact,
225             issueinstant => DateTime->now,
226             );
227              
228 0         0 return $artifact_request;
229             }
230              
231              
232             sub sp_post_binding {
233 3     3 1 14 my ($self, $idp, $param) = @_;
234              
235 3 50       9 unless ($idp) {
236 0         0 croak("Unable to create a post binding without an IDP");
237             }
238              
239 3   50     9 $param //= 'SAMLRequest';
240              
241 3 50       13 my $post = Net::SAML2::Binding::POST->new(
242             url => $idp->sso_url('urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST'),
243             cert => ($self->cert,),
244             $self->authnreq_signed ? (
245             key => $self->key,
246             ) : (
247             insecure => 1,
248             ),
249             param => $param,
250             );
251              
252 3         41 return $post;
253             }
254              
255              
256             sub sso_redirect_binding {
257 2     2 1 584 my ($self, $idp, $param) = @_;
258              
259 2 50       8 unless ($idp) {
260 0         0 croak("Unable to create a redirect binding without an IDP");
261             }
262              
263 2 50       8 $param = 'SAMLRequest' unless $param;
264              
265 2 100       13 my $redirect = Net::SAML2::Binding::Redirect->new(
266             url => $idp->sso_url('urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect'),
267             cert => $idp->cert('signing'),
268             $self->authnreq_signed ? (
269             key => $self->key,
270             ) : (
271             insecure => 1,
272             ),
273             param => $param,
274             );
275              
276 2         34 return $redirect;
277             }
278              
279              
280             sub slo_redirect_binding {
281 0     0 1 0 my ($self, $idp, $param) = @_;
282              
283 0         0 my $redirect = Net::SAML2::Binding::Redirect->new(
284             url => $idp->slo_url('urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect'),
285             cert => $idp->cert('signing'),
286             key => $self->key,
287             param => $param,
288             );
289 0         0 return $redirect;
290             }
291              
292              
293             sub soap_binding {
294 1     1 1 5 my ($self, $ua, $idp_url, $idp_cert) = @_;
295              
296 1 50       29 return Net::SAML2::Binding::SOAP->new(
297             ua => $ua,
298             key => $self->key,
299             cert => $self->cert,
300             url => $idp_url,
301             idp_cert => $idp_cert,
302             $self->has_cacert ? (cacert => $self->cacert) : (),
303             );
304             }
305              
306              
307             sub post_binding {
308 2     2 1 18 my ($self) = @_;
309              
310 2 50       66 return Net::SAML2::Binding::POST->new(
311             $self->has_cacert ? (cacert => $self->cacert) : ()
312             );
313             }
314              
315              
316             sub generate_sp_desciptor_id {
317 7     7 1 10 my $self = shift;
318 7         18 return Net::SAML2::Util::generate_id();
319             }
320              
321              
322             my $md = ['md' => URN_METADATA];
323             my $ds = ['ds' => URN_SIGNATURE];
324              
325             sub generate_metadata {
326 7     7 1 8 my $self = shift;
327              
328 7         43 my $x = XML::Generator->new(conformance => 'loose', xml => { version => "1.0", encoding => 'UTF-8' });
329              
330 7         729 my $error_uri = $self->error_url;
331 7 100       28 if (!$error_uri->scheme) {
332 6         234 $error_uri = $self->url . $self->error_url;
333             }
334              
335 7 100       248 return $x->xml( $x->EntityDescriptor(
    50          
336             $md,
337             {
338             entityID => $self->id,
339             ID => $self->generate_sp_desciptor_id(),
340             },
341             $x->SPSSODescriptor(
342             $md,
343             {
344             AuthnRequestsSigned => $self->authnreq_signed,
345             WantAssertionsSigned => $self->want_assertions_signed,
346             errorURL => $error_uri,
347             protocolSupportEnumeration => URN_PROTOCOL,
348             },
349              
350             $self->_generate_key_descriptors($x, 'signing'),
351              
352             $self->has_encryption_key ? $self->_generate_key_descriptors($x, 'encryption') : (),
353              
354             $self->_generate_single_logout_service($x),
355              
356             $self->_generate_assertion_consumer_service($x),
357              
358             ),
359             $x->Organization(
360             $md,
361             $x->OrganizationName(
362             $md, { 'xml:lang' => 'en' }, $self->org_name,
363             ),
364             $x->OrganizationDisplayName(
365             $md, { 'xml:lang' => 'en' },
366             $self->org_display_name,
367             ),
368             $x->OrganizationURL(
369             $md,
370             { 'xml:lang' => 'en' },
371             defined($self->org_url) ? $self->org_url : $self->url
372             )
373             ),
374             $x->ContactPerson(
375             $md,
376             { contactType => 'other' },
377             $x->Company($md, $self->org_display_name,),
378             $x->EmailAddress($md, $self->org_contact,),
379             )
380             ));
381             }
382              
383             sub _generate_key_descriptors {
384 8     8   13 my $self = shift;
385 8         8 my $x = shift;
386 8         11 my $use = shift;
387              
388             return
389 8 50 66     157 if !$self->authnreq_signed
      33        
390             && !$self->want_assertions_signed
391             && !$self->sign_metadata;
392              
393 8 100       180 my $key = $use eq 'signing' ? $self->_cert_text : $self->_encryption_key_text;
394              
395 8         60 return $x->KeyDescriptor(
396             $md,
397             { use => $use },
398             $x->KeyInfo(
399             $ds,
400             $x->X509Data($ds, $x->X509Certificate($ds, $key)),
401             $x->KeyName($ds, $self->key_name($use)),
402             ),
403             );
404             }
405              
406              
407             sub key_name {
408 12     12 1 18770 my $self = shift;
409 12         14 my $use = shift;
410 12 100       317 my $key = $use eq 'signing' ? $self->_cert_text : $self->_encryption_key_text;
411 12 100       26 return unless $key;
412 11         97 return Digest::MD5::md5_hex($key);
413             }
414              
415             sub _generate_single_logout_service {
416 7     7   274 my $self = shift;
417 7         8 my $x = shift;
418 7         11 return map { $x->SingleLogoutService($md, $_) } @{ $self->single_logout_service };
  3         84  
  7         154  
419             }
420              
421             sub _generate_assertion_consumer_service {
422 7     7   138 my $self = shift;
423 7         8 my $x = shift;
424 7         8 return map { $x->AssertionConsumerService($md, $_) } @{ $self->assertion_consumer_service };
  14         560  
  7         158  
425             }
426              
427              
428              
429             sub metadata {
430 7     7 1 35 my $self = shift;
431              
432 7         13 my $metadata = $self->generate_metadata();
433 7 100       3200 return $metadata unless $self->sign_metadata;
434              
435 26     26   55653 use Net::SAML2::XML::Sig;
  26         85  
  26         375  
436 6         117 my $signer = Net::SAML2::XML::Sig->new(
437             {
438             key => $self->key,
439             cert => $self->cert,
440             sig_hash => 'sha256',
441             digest_hash => 'sha256',
442             x509 => 1,
443             ns => { md => 'urn:oasis:names:tc:SAML:2.0:metadata' },
444             id_attr => '/md:EntityDescriptor[@ID]',
445             }
446             );
447 6         2368 return $signer->sign($metadata);
448             }
449              
450              
451             sub get_default_assertion_service {
452 3     3 1 7219 my $self = shift;
453 4 50   4   18 my $default = first { $_->{isDefault} eq 1 || $_->{isDefault} eq 'true' }
454 3         11 grep { defined $_->{isDefault} } @{ $self->assertion_consumer_service };
  6         28  
  3         104  
455 3 100       12 return $default if $default;
456              
457 2     4   7 $default = first { ! defined $_->{isDefault} } @{ $self->assertion_consumer_service };
  4         6  
  2         47  
458 2 100       10 return $default if $default;
459              
460 1         23 return $self->assertion_consumer_service->[0];
461             }
462              
463             __PACKAGE__->meta->make_immutable;
464              
465             __END__
466              
467             =pod
468              
469             =encoding UTF-8
470              
471             =head1 NAME
472              
473             Net::SAML2::SP - SAML Service Provider object
474              
475             =head1 VERSION
476              
477             version 0.74
478              
479             =head1 SYNOPSIS
480              
481             my $sp = Net::SAML2::SP->new(
482             id => 'http://localhost:3000',
483             url => 'http://localhost:3000',
484             cert => 'sign-nopw-cert.pem',
485             key => 'sign-nopw-key.pem',
486             );
487              
488             =head1 METHODS
489              
490             =head2 new( ... )
491              
492             Constructor. Create an SP object.
493              
494             Arguments:
495              
496             =over
497              
498             =item B<url>
499              
500             Base for all SP service URLs
501              
502             =item B<error_url>
503              
504             The error URI. Can be relative to the base URI or a regular URI
505              
506             =item B<id>
507              
508             SP's identity URI.
509              
510             =item B<cert>
511              
512             Path to the signing certificate
513              
514             =item B<key>
515              
516             Path to the private key for the signing certificate
517              
518             =item B<encryption_key>
519              
520             Path to the public key that the IdP should use for encryption. This
521             is used when generating the metadata.
522              
523             =item B<cacert>
524              
525             Path to the CA certificate for verification
526              
527             =item B<org_name>
528              
529             SP organisation name
530              
531             =item B<org_display_name>
532              
533             SP organisation display name
534              
535             =item B<org_contact>
536              
537             SP contact email address
538              
539             =item B<org_url>
540              
541             SP organization url. This is optional and url will be used as in
542             previous versions if this is not provided.
543              
544             =item B<authnreq_signed>
545              
546             Specifies in the metadata whether the SP signs the AuthnRequest
547             Optional (0 or 1) defaults to 1 (TRUE) if not specified.
548              
549             =item B<want_assertions_signed>
550              
551             Specifies in the metadata whether the SP wants the Assertion from
552             the IdP to be signed
553             Optional (0 or 1) defaults to 1 (TRUE) if not specified.
554              
555             =item B<sign_metadata>
556              
557             Sign the metadata, defaults to 1 (TRUE) if not specified.
558              
559             =item B<single_logout_service>
560              
561             The following option replaces the previous C<slo_url_post>, C<slo_url_soap> and
562             C<slo_url_redirect> constructor parameters. The former options are mapped to
563             this new structure.
564              
565             This expects an array of hash refs where you define one or more Single Logout
566             Services
567              
568             [
569             {
570             Binding => BINDING_HTTP_POST,
571             Location => https://foo.example.com/your-post-endpoint,
572             },
573             {
574             Binding => BINDING_HTTP_ARTIFACT,
575             Location => https://foo.example.com/your-artifact-endpoint,
576             }
577             ]
578              
579             =item B<assertion_consumer_service>
580              
581             The following option replaces the previous C<acs_url_post> and
582             C<acs_url_artifact> constructor parameters. The former options are mapped to
583             this new structure.
584              
585             This expects an array of hash refs where you define one or more Assertion
586             Consumer Services.
587              
588             [
589             # Order decides the index if not supplied, else we assume you have an index
590             {
591             Binding => BINDING_HTTP_POST,
592             Location => https://foo.example.com/your-post-endpoint,
593             isDefault => 'false',
594             # optionally
595             index => 1,
596             },
597             {
598             Binding => BINDING_HTTP_ARTIFACT,
599             Location => https://foo.example.com/your-artifact-endpoint,
600             isDefault => 'true',
601             index => 2,
602             }
603             ]
604              
605             =back
606              
607             =head2 authn_request( $destination, $nameid_format, %params )
608              
609             Returns an AuthnRequest object created by this SP, intended for the
610             given destination, which should be the identity URI of the IdP.
611              
612             %params is a hash containing parameters valid for
613             Net::SAML2::Protocol::AuthnRequest. For example:
614              
615             =over
616              
617             my %params = (
618             force_authn => 1,
619             is_passive => 1,
620             )
621              
622             my $authnreq = authn_request(
623             'https://keycloak.local:8443/realms/Foswiki/protocol/saml',
624             'urn:oasis:names:tc:SAML:2.0:nameid-format:persistent',
625             %params
626             );
627              
628             =back
629              
630             =head2 logout_request( $destination, $nameid, $nameid_format, $session, $params )
631              
632             Returns a LogoutRequest object created by this SP, intended for the
633             given destination, which should be the identity URI of the IdP.
634              
635             Also requires the nameid (+format) and session to be logged out.
636              
637             =over
638              
639             $params is a HASH reference for parameters to Net::SAML2::Protocol::LogoutRequest
640              
641             $params = (
642             # name qualifier parameters from Assertion NameId
643             name_qualifier => "https://idp.shibboleth.local/idp/shibboleth"
644             sp_name_qualifier => "https://netsaml2-testapp.local"
645             );
646              
647             =back
648              
649             =head2 logout_response( $destination, $status, $response_to )
650              
651             Returns a LogoutResponse object created by this SP, intended for the
652             given destination, which should be the identity URI of the IdP.
653              
654             Also requires the status and the ID of the corresponding
655             LogoutRequest.
656              
657             =head2 artifact_request( $destination, $artifact )
658              
659             Returns an ArtifactResolve request object created by this SP, intended
660             for the given destination, which should be the identity URI of the
661             IdP.
662              
663             =head2 sp_post_binding ( $idp, $param )
664              
665             Returns a POST binding object for this SP, configured against the
666             given IDP for Single Sign On. $param specifies the name of the query
667             parameter involved - typically C<SAMLRequest>.
668              
669             =head2 sso_redirect_binding( $idp, $param )
670              
671             Returns a Redirect binding object for this SP, configured against the
672             given IDP for Single Sign On. $param specifies the name of the query
673             parameter involved - typically C<SAMLRequest>.
674              
675             =head2 slo_redirect_binding( $idp, $param )
676              
677             Returns a Redirect binding object for this SP, configured against the
678             given IDP for Single Log Out. $param specifies the name of the query
679             parameter involved - typically C<SAMLRequest> or C<SAMLResponse>.
680              
681             =head2 soap_binding( $ua, $idp_url, $idp_cert )
682              
683             Returns a SOAP binding object for this SP, with a destination of the
684             given URL and signing certificate.
685              
686             XXX UA
687              
688             =head2 post_binding( )
689              
690             Returns a POST binding object for this SP.
691              
692             =head2 generate_sp_desciptor_id ( )
693              
694             Returns the Net::SAML2 unique ID from Net::SAML2::Util::generate_id.
695              
696             =head2 generate_metadata( )
697              
698             Generate the metadata XML document for this SP.
699              
700             =head2 key_name($type)
701              
702             Get the key name for either the C<signing> or C<encryption> key
703              
704             =head2 metadata( )
705              
706             Returns the metadata XML document for this SP.
707              
708             =head2 get_default_assertion_service
709              
710             Return the assertion service which is the default
711              
712             =head1 AUTHORS
713              
714             =over 4
715              
716             =item *
717              
718             Chris Andrews <chrisa@cpan.org>
719              
720             =item *
721              
722             Timothy Legge <timlegge@gmail.com>
723              
724             =back
725              
726             =head1 COPYRIGHT AND LICENSE
727              
728             This software is copyright (c) 2023 by Venda Ltd, see the CONTRIBUTORS file for others.
729              
730             This is free software; you can redistribute it and/or modify it under
731             the same terms as the Perl 5 programming language system itself.
732              
733             =cut