File Coverage

blib/lib/Net/SAML2/SP.pm
Criterion Covered Total %
statement 126 138 91.3
branch 32 44 72.7
condition 6 10 60.0
subroutine 35 38 92.1
pod 14 14 100.0
total 213 244 87.3


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