From 1ed7a84a171b1cddcd12dfd3ec07281583667670 Mon Sep 17 00:00:00 2001 From: Chris Andrews Date: Nov 22 2010 16:59:02 +0000 Subject: Switch objects to Moose Adds lots of testing, with some API changes. --- diff --git a/Makefile.PL b/Makefile.PL index d3c5a0c..f696adf 100755 --- a/Makefile.PL +++ b/Makefile.PL @@ -14,6 +14,7 @@ all_from 'lib/Net/SAML2.pm'; requires 'XML::Sig'; requires 'Crypt::OpenSSL::RSA'; requires 'Crypt::OpenSSL::X509'; +requires 'Crypt::OpenSSL::Random'; requires 'Crypt::OpenSSL::VerifyX509'; requires 'IO::Compress::RawDeflate'; requires 'IO::Uncompress::RawInflate'; @@ -23,12 +24,17 @@ requires 'MIME::Base64'; requires 'LWP::UserAgent'; requires 'XML::XPath'; requires 'DateTime::Format::XSD'; +requires 'Moose'; +requires 'MooseX::Types'; +requires 'MooseX::Types::URI'; author_requires 'Test::NoTabs'; author_requires 'Test::Pod' => '1.14'; author_requires 'Test::Pod::Coverage' => '1.04'; test_requires 'Test::More' => '0.88'; +test_requires 'Dancer' => '1.2000'; +test_requires 'WWW::Mechanize' => '1.66'; resources license => "http://dev.perl.org/licenses", diff --git a/lib/Net/SAML2.pm b/lib/Net/SAML2.pm index a5c5a95..42d19ce 100644 --- a/lib/Net/SAML2.pm +++ b/lib/Net/SAML2.pm @@ -28,7 +28,7 @@ Net::SAML2 - SAML bindings and protocol implementation url => $sso_url, ); - my $url = $redirect->sign_request($authnreq); + my $url = $redirect->sign($authnreq); # handle the POST back from the IdP, via the browser: @@ -38,7 +38,7 @@ Net::SAML2 - SAML bindings and protocol implementation ); if ($ret) { - my $assertion = Net::SAML2::Protocol::Assertion->new( + my $assertion = Net::SAML2::Protocol::Assertion->new_from_xml( xml => decode_base64($saml_response) ); diff --git a/lib/Net/SAML2/Binding/Artifact.pm b/lib/Net/SAML2/Binding/Artifact.pm deleted file mode 100644 index e3a2b32..0000000 --- a/lib/Net/SAML2/Binding/Artifact.pm +++ /dev/null @@ -1,112 +0,0 @@ -package Net::SAML2::Binding::Artifact; -use strict; -use warnings; - -=head1 NAME - -Net::SAML2::Binding::Artifact - SOAP Artifact binding for SAML2 - -=head1 SYNOPSIS - - my $resolver = Net::SAML2::Binding::Artifact->new( - url => $art_url, - key => 'sign-private.pem', - cert => 'sign-certonly.pem', - issuer => 'http://localhost:3000', - ); - - my $response = $resolver->resolve(params->{SAMLart}); - -=head1 METHODS - -=cut - -use XML::Sig; -use LWP::UserAgent; -use HTTP::Request::Common; - -=head2 new( ... ) - -Constructor. Returns an instance of the Artifact binding configured -for the given SP issuer and IdP resolver service url. - -Arguments: - - * ua - (optionally) a LWP::UserAgent-compatible UA - * url - the resolver service URL - * key - path to the signing key - * cert - path to the signing certificate - * issuer - the issuing SP's identity URI - -=cut - -sub new { - my ($class, %args) = @_; - my $self = bless {}, $class; - - $self->{ua} = $args{ua}; - $self->{url} = $args{url}; - $self->{key} = $args{key}; - $self->{cert} = $args{cert}; - $self->{issuer} = $args{issuer}; - - return $self; -} - -=head2 resolve($artifact) - -Resolve the given artifact, which should be an opaque SAML2 artifact id. - -Returns the Artifact, or dies if there was an error. - -=cut - -sub resolve { - my ($self, $artifact) = @_; - - my $saml_req = < - $self->{issuer} - $artifact - -XML - - my $sig = XML::Sig->new({ x509 => 1, key => $self->{key}, cert => $self->{cert} }); - my $signed_saml_req = $sig->sign($saml_req); - - my $ret = $sig->verify($signed_saml_req); - die "failed to sign" unless $ret; - - my $soap_req = <<"SOAP"; - - -$saml_req - - -SOAP - - my $soap_action = 'http://www.oasis-open.org/committees/security'; - - my $req = POST $self->{url}; - $req->header('SOAPAction' => $soap_action); - $req->header('Content-Type' => 'text/xml'); - $req->header('Content-Length' => length $soap_req); - $req->content($soap_req); - - my $ua = $self->{ua} || LWP::UserAgent->new; - my $res = $ua->request($req); - - my $sig_verify = XML::Sig->new({ x509 => 1 }); - $ret = $sig_verify->verify($res->content); - die "bad artifact response" unless $ret; - - return $res->content; -} - -1; diff --git a/lib/Net/SAML2/Binding/POST.pm b/lib/Net/SAML2/Binding/POST.pm index 00f8c3e..9544bd6 100644 --- a/lib/Net/SAML2/Binding/POST.pm +++ b/lib/Net/SAML2/Binding/POST.pm @@ -1,6 +1,6 @@ package Net::SAML2::Binding::POST; -use strict; -use warnings; +use Moose; +use MooseX::Types::Moose qw/ Str /; =head1 NAME @@ -29,14 +29,7 @@ No arguments. =cut -sub new { - my ($class, %args) = @_; - my $self = bless {}, $class; - - $self->{cacert} = $args{cacert}; - - return $self; -} +has 'cacert' => (isa => Str, is => 'ro', required => 1); =head2 handle_response($response) @@ -56,7 +49,7 @@ sub handle_response { # verify the signing certificate my $cert = $x->signer_cert; - my $ca = Crypt::OpenSSL::VerifyX509->new($self->{cacert}); + my $ca = Crypt::OpenSSL::VerifyX509->new($self->cacert); $ret = $ca->verify($cert); if ($ret) { diff --git a/lib/Net/SAML2/Binding/Redirect.pm b/lib/Net/SAML2/Binding/Redirect.pm index 4668160..caf7b2a 100644 --- a/lib/Net/SAML2/Binding/Redirect.pm +++ b/lib/Net/SAML2/Binding/Redirect.pm @@ -1,6 +1,7 @@ package Net::SAML2::Binding::Redirect; -use strict; -use warnings; +use Moose; +use MooseX::Types::Moose qw/ Str /; +use MooseX::Types::URI qw/ Uri /; =head1 NAME @@ -11,15 +12,19 @@ Net::SAML2::Binding::Redirect my $redirect = Net::SAML2::Binding::Redirect->new( key => 'sign-nopw-cert.pem', url => $sso_url, + param => 'SAMLRequest', ); - my $url = $redirect->sign_request($authnreq); + my $url = $redirect->sign($authnreq); # or - - my $ret = $post->handle_response( - $saml_response + + my $redirect = Net::SAML2::Binding::Redirect->new( + cert => $idp_cert, + param => 'SAMLResponse', ); + + my $ret = $redirect->verify($url); =head1 METHODS @@ -40,22 +45,19 @@ Constructor. Creates an instance of the Redirect binding. Arguments: - * key - the signing key + * key - the signing key (for creating Redirect URLs) + * cert - the IdP's signing cert (for verifying Redirect URLs) * url - the IdP's SSO service url for the Redirect binding + * param - the query param name to use (SAMLRequest, SAMLResponse) =cut -sub new { - my ($class, %args) = @_; - my $self = bless {}, $class; +has 'key' => (isa => Str, is => 'ro', required => 1); +has 'cert' => (isa => Str, is => 'ro', required => 1); +has 'url' => (isa => Uri, is => 'ro', required => 1, coerce => 1); +has 'param' => (isa => Str, is => 'ro', required => 1); - $self->{key} = $args{key}; - $self->{url} = $args{url}; - - return $self; -} - -=head2 sign_request($request, $relaystate) +=head2 sign($request, $relaystate) Signs the given request, and returns the URL to which the user's browser should be redirected. @@ -66,19 +68,19 @@ authentication process with the IdP. =cut -sub sign_request { +sub sign { my ($self, $request, $relaystate) = @_; my $output = ''; rawdeflate \$request => \$output; my $req = encode_base64($output, ''); - my $u = URI->new($self->{url}); - $u->query_param('SAMLRequest', $req); + my $u = URI->new($self->url); + $u->query_param($self->param, $req); $u->query_param('RelayState', $relaystate) if defined $relaystate; $u->query_param('SigAlg', 'http://www.w3.org/2000/09/xmldsig#rsa-sha1'); - my $key_string = read_file($self->{key}); + my $key_string = read_file($self->key); my $rsa_priv = Crypt::OpenSSL::RSA->new_private_key($key_string); my $to_sign = $u->query; @@ -89,24 +91,24 @@ sub sign_request { return $url; } -=head2 handle_request($url) +=head2 verify($url) Decode a Redirect binding URL. -Should also verify the signature on the response. +Verifies the signature on the response. =cut -sub handle_request { +sub verify { my ($self, $url) = @_; my $u = URI->new($url); - + # verify the response my $sigalg = $u->query_param('SigAlg'); die "can't verify '$sigalg' signatures" unless $sigalg eq 'http://www.w3.org/2000/09/xmldsig#rsa-sha1'; - my $cert = Crypt::OpenSSL::X509->new_from_file($self->{key}); + my $cert = Crypt::OpenSSL::X509->new_from_string($self->cert); my $rsa_pub = Crypt::OpenSSL::RSA->new_public_key($cert->pubkey); my $sig = decode_base64($u->query_param_delete('Signature')); @@ -114,7 +116,7 @@ sub handle_request { die "bad sig" unless $rsa_pub->verify($signed, $sig); # unpack the SAML request - my $deflated = decode_base64($u->query_param('SAMLRequest')); + my $deflated = decode_base64($u->query_param($self->param)); my $request = ''; rawinflate \$deflated => \$request; diff --git a/lib/Net/SAML2/Binding/SOAP.pm b/lib/Net/SAML2/Binding/SOAP.pm index 6566073..ab922ba 100644 --- a/lib/Net/SAML2/Binding/SOAP.pm +++ b/lib/Net/SAML2/Binding/SOAP.pm @@ -1,6 +1,7 @@ package Net::SAML2::Binding::SOAP; -use strict; -use warnings; +use Moose; +use MooseX::Types::Moose qw/ Str Object /; +use MooseX::Types::URI qw/ Uri /; =head1 NAME @@ -41,18 +42,13 @@ Arguments: =cut -sub new { - my ($class, %args) = @_; - my $self = bless {}, $class; +has 'ua' => (isa => Object, is => 'ro', required => 1, + default => sub { LWP::UserAgent->new }); - $self->{ua} = $args{ua}; - $self->{url} = $args{url}; - $self->{key} = $args{key}; - $self->{cert} = $args{cert}; - $self->{idp_cert} = $args{idp_cert}; - - return $self; -} +has 'url' => (isa => Uri, is => 'ro', required => 1, coerce => 1); +has 'key' => (isa => Str, is => 'ro', required => 1); +has 'cert' => (isa => Str, is => 'ro', required => 1); +has 'idp_cert' => (isa => Str, is => 'ro', required => 1); =head2 request($message) @@ -68,13 +64,13 @@ sub request { my $soap_action = 'http://www.oasis-open.org/committees/security'; - my $req = POST $self->{url}; + my $req = POST $self->url; $req->header('SOAPAction' => $soap_action); $req->header('Content-Type' => 'text/xml'); $req->header('Content-Length' => length $request); $req->content($request); - my $ua = $self->{ua} || LWP::UserAgent->new; + my $ua = $self->ua; my $res = $ua->request($req); return $self->handle_response($res->content); @@ -92,7 +88,7 @@ sub handle_response { my ($self, $response) = @_; # verify the response - my $sig_verify = XML::Sig->new({ x509 => 1, cert_text => $self->{idp_cert} }); + my $sig_verify = XML::Sig->new({ x509 => 1, cert_text => $self->idp_cert }); my $ret = $sig_verify->verify($response); die "bad SOAP response" unless $ret; @@ -123,7 +119,7 @@ sub handle_request { my $saml = $parser->findnodes_as_string('/soap-env:Envelope/soap-env:Body/*'); if (defined $saml) { - my $sig_verify = XML::Sig->new({ x509 => 1, cert_text => $self->{idp_cert} }); + my $sig_verify = XML::Sig->new({ x509 => 1, cert_text => $self->idp_cert }); my $ret = $sig_verify->verify($saml); return unless $ret; @@ -146,8 +142,8 @@ sub create_soap_envelope { # sign the message my $sig = XML::Sig->new({ x509 => 1, - key => $self->{key}, - cert => $self->{cert} + key => $self->key, + cert => $self->cert, }); my $signed_message = $sig->sign($message); diff --git a/lib/Net/SAML2/IdP.pm b/lib/Net/SAML2/IdP.pm index a454ac7..9f00486 100644 --- a/lib/Net/SAML2/IdP.pm +++ b/lib/Net/SAML2/IdP.pm @@ -1,6 +1,7 @@ package Net::SAML2::IdP; -use strict; -use warnings; +use Moose; +use MooseX::Types::Moose qw/ Str Object HashRef /; +use MooseX::Types::URI qw/ Uri /; =head1 NAME @@ -15,11 +16,28 @@ Net::SAML2::IdP - SAML Identity Provider object =cut +use Crypt::OpenSSL::VerifyX509; +use Crypt::OpenSSL::X509; use HTTP::Request::Common; use LWP::UserAgent; use XML::XPath; -=head2 new_from_url($url) +=head2 new + +Constructor + + * entityID + +=cut + +has 'entityid' => (isa => Str, is => 'ro', required => 1); +has 'cacert' => (isa => Str, is => 'ro', required => 1); +has 'sso_urls' => (isa => HashRef[Str], is => 'ro', required => 1); +has 'slo_urls' => (isa => HashRef[Str], is => 'ro', required => 1); +has 'art_urls' => (isa => HashRef[Str], is => 'ro', required => 1); +has 'certs' => (isa => HashRef[Str], is => 'ro', required => 1); + +=head2 new_from_url( url => $url, cacert => $cacert ) Create an IdP object by retrieving the metadata at the given URL. @@ -28,71 +46,79 @@ Dies if the metadata can't be retrieved. =cut sub new_from_url { - my ($class, $url) = @_; + my ($class, %args) = @_; - my $req = GET $url; + my $req = GET $args{url}; my $ua = LWP::UserAgent->new; my $res = $ua->request($req); die "no metadata" unless $res->is_success; my $xml = $res->content; - return $class->new($xml); + return $class->new_from_xml( xml => $xml, cacert => $args{cacert} ); } -=head2 new($xml) +=head2 new_from_xml( xml => $xml, cacert => $cacert ) Constructor. Create an IdP object using the provided metadata XML document. =cut -sub new { - my ($class, $xml) = @_; - my $self = bless {}, $class; +sub new_from_xml { + my ($class, %args) = @_; - my $xpath = XML::XPath->new( xml => $xml ); + my $xpath = XML::XPath->new( xml => $args{xml} ); $xpath->set_namespace('md', 'urn:oasis:names:tc:SAML:2.0:metadata'); $xpath->set_namespace('ds', 'http://www.w3.org/2000/09/xmldsig#'); - my ($desc) = $xpath->findnodes('//md:EntityDescriptor'); - if (defined $desc) { - $self->{entityID} = $desc->getAttribute('entityID'); - } - else { - die "can't find entityID in metadata"; - } + my $data; - my @ssos = $xpath->findnodes('//md:EntityDescriptor/md:IDPSSODescriptor/md:SingleSignOnService'); - for my $sso (@ssos) { + for my $sso ($xpath->findnodes('//md:EntityDescriptor/md:IDPSSODescriptor/md:SingleSignOnService')) { my $binding = $sso->getAttribute('Binding'); - $self->{SSO}->{$binding} = $sso->getAttribute('Location'); + $data->{SSO}->{$binding} = $sso->getAttribute('Location'); } - my @slos = $xpath->findnodes('//md:EntityDescriptor/md:IDPSSODescriptor/md:SingleLogoutService'); - for my $slo (@slos) { + for my $slo ($xpath->findnodes('//md:EntityDescriptor/md:IDPSSODescriptor/md:SingleLogoutService')) { my $binding = $slo->getAttribute('Binding'); - $self->{SLO}->{$binding} = $slo->getAttribute('Location'); + $data->{SLO}->{$binding} = $slo->getAttribute('Location'); } - my @arts = $xpath->findnodes('//md:EntityDescriptor/md:IDPSSODescriptor/md:ArtifactResolutionService'); - for my $art (@arts) { + for my $art ($xpath->findnodes('//md:EntityDescriptor/md:IDPSSODescriptor/md:ArtifactResolutionService')) { my $binding = $art->getAttribute('Binding'); - $self->{Art}->{$binding} = $art->getAttribute('Location'); + $data->{Art}->{$binding} = $art->getAttribute('Location'); } - # XXX this cert should get verified by our CA before we trust it - my @keys = $xpath->findnodes('//md:EntityDescriptor/md:IDPSSODescriptor/md:KeyDescriptor'); - for my $key (@keys) { + for my $key ($xpath->findnodes('//md:EntityDescriptor/md:IDPSSODescriptor/md:KeyDescriptor')) { my $use = $key->getAttribute('use'); my ($text) = $key->findvalue('ds:KeyInfo/ds:X509Data/ds:X509Certificate') =~ /^\s+(.+?)\s+$/s; - $self->{Cert}->{$use} = - sprintf("-----BEGIN CERTIFICATE-----\n%s\n-----END CERTIFICATE-----\n", $text); + $data->{Cert}->{$use} = sprintf("-----BEGIN CERTIFICATE-----\n%s\n-----END CERTIFICATE-----\n", $text); } + my $self = $class->new( + entityid => $xpath->findvalue('//md:EntityDescriptor/@entityID')->value, + sso_urls => $data->{SSO}, + slo_urls => $data->{SLO}, + art_urls => $data->{Art}, + certs => $data->{Cert}, + cacert => $args{cacert}, + ); + return $self; } +sub BUILD { + my ($self) = @_; + my $ca = Crypt::OpenSSL::VerifyX509->new($self->cacert); + + for my $use (keys %{ $self->certs }) { + my $cert = Crypt::OpenSSL::X509->new_from_string($self->certs->{$use}); + unless ($ca->verify($cert)) { + die "can't verify IdP '$use' cert"; + } + } +} + =head2 sso_url($binding) Returns the url for the SSO service using the given binding. Binding @@ -102,7 +128,7 @@ name should be the full URI. sub sso_url { my ($self, $binding) = @_; - return $self->{SSO}->{$binding}; + return $self->sso_urls->{$binding}; } =head2 slo_url($binding) @@ -114,7 +140,7 @@ binding. Binding name should be the full URI. sub slo_url { my ($self, $binding) = @_; - return $self->{SLO}->{$binding}; + return $self->slo_urls->{$binding}; } =head2 art_url($binding) @@ -126,7 +152,7 @@ binding. Binding name should be the full URI. sub art_url { my ($self, $binding) = @_; - return $self->{Art}->{$binding}; + return $self->art_urls->{$binding}; } =head2 cert($use) @@ -137,56 +163,7 @@ Returns the IdP's certificate for the given use (e.g. 'signing'). sub cert { my ($self, $use) = @_; - return $self->{Cert}->{$use}; -} - -=head2 entityID() - -Returns the IdP's entityID, for use as the Destination in requests. - -=cut - -sub entityID { - my ($self) = @_; - return $self->{entityID}; -} - -=head2 metadata - -Returns IdP metadata for this instance - -=cut - -sub metadata { - my ($self) = @_; - - return <<"METADATA"; - - - - - - - -$self->{cert} - - - - - - - - - urn:oasis:names:tc:SAML:2.0:nameid-format:persistent - urn:oasis:names:tc:SAML:2.0:nameid-format:transient - urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress - urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified - - - - - -METADATA + return $self->certs->{$use}; } 1; diff --git a/lib/Net/SAML2/Protocol/ArtifactResolve.pm b/lib/Net/SAML2/Protocol/ArtifactResolve.pm index 8d1ede6..7f51cd9 100644 --- a/lib/Net/SAML2/Protocol/ArtifactResolve.pm +++ b/lib/Net/SAML2/Protocol/ArtifactResolve.pm @@ -1,6 +1,10 @@ package Net::SAML2::Protocol::ArtifactResolve; -use strict; -use warnings; +use Moose; +use MooseX::Types::Moose qw/ Str /; +use MooseX::Types::URI qw/ Uri /; + +with 'Net::SAML2::Role::Templater', + 'Net::SAML2::Role::ProtocolMessage'; =head1 NAME @@ -27,22 +31,14 @@ Arguments: * issuer - the issuing SP's identity URI * artifact - the artifact to be resolved - * issueinstant - a DateTime for "now" * destination - the IdP's identity URI =cut -sub new { - my ($class, %args) = @_; - my $self = bless {}, $class; - - $self->{issuer} = $args{issuer}; - $self->{artifact} = $args{artifact}; - $self->{destination} = $args{destination}; - $self->{issueinstant} = $args{issueinstant}; +has 'artifact' => (isa => Str, is => 'ro', required => 1); +has 'issuer' => (isa => Uri, is => 'ro', required => 1, coerce => 1); +has 'destination' => (isa => Uri, is => 'ro', required => 1, coerce => 1); - return $self; -} =head2 as_xml @@ -53,24 +49,19 @@ Returns the ArtifactResolve request as XML. sub as_xml { my ($self) = @_; - my $issueinstant = DateTime::Format::XSD->format_datetime( - $self->{issueinstant} - ); - - my $xml = <<"EOXML"; - - $self->{issuer} - $self->{artifact} - + issuer ?> + artifact ?> + EOXML - return $xml; + return $self->template($template); } 1; diff --git a/lib/Net/SAML2/Protocol/Assertion.pm b/lib/Net/SAML2/Protocol/Assertion.pm index b0f0b3d..824a830 100644 --- a/lib/Net/SAML2/Protocol/Assertion.pm +++ b/lib/Net/SAML2/Protocol/Assertion.pm @@ -1,6 +1,9 @@ package Net::SAML2::Protocol::Assertion; -use strict; -use warnings; +use Moose; +use MooseX::Types::Moose qw/ Str HashRef ArrayRef /; + +with 'Net::SAML2::Role::Templater', + 'Net::SAML2::Role::ProtocolMessage'; =head1 NAME @@ -8,78 +11,50 @@ Net::SAML2::Protocol::Assertion - SAML2 assertion object =head1 SYNOPSIS - my $assertion = Net::SAML2::Protocol::Assertion->new( + my $assertion = Net::SAML2::Protocol::Assertion->new_from_xml( xml => decode_base64($SAMLResponse) ); +=cut + +has 'attributes' => (isa => HashRef[ArrayRef], is => 'ro', required => 1); +has 'session' => (isa => Str, is => 'ro', required => 1); +has 'nameid' => (isa => Str, is => 'ro', required => 1); + =head1 METHODS =cut -use XML::XPath; - -=head2 new( ... ) +=head2 new_from_xml( ... ) Constructor. Creates an instance of the Assertion object, parsing the given XML to find the attributes, session and nameid. =cut -sub new { +sub new_from_xml { my ($class, %args) = @_; - my $self = bless {}, $class; my $xpath = XML::XPath->new( xml => $args{xml} ); $xpath->set_namespace('saml', 'urn:oasis:names:tc:SAML:2.0:assertion'); - $self->{attributes} = {}; + my $attributes = {}; for my $node ($xpath->findnodes('//saml:Assertion/saml:AttributeStatement/saml:Attribute')) { my @values = $node->findnodes('saml:AttributeValue'); - $self->{attributes}->{$node->getAttribute('Name')} = [ + $attributes->{$node->getAttribute('Name')} = [ map { $_->string_value } @values ]; } - $self->{session} = $xpath->findvalue('//saml:AuthnStatement/@SessionIndex')->value; - $self->{nameid} = $xpath->findvalue('//saml:Subject/saml:NameID')->value; - + my $self = $class->new( + attributes => $attributes, + session => $xpath->findvalue('//saml:AuthnStatement/@SessionIndex')->value, + nameid => $xpath->findvalue('//saml:Subject/saml:NameID')->value, + ); + return $self; } -=head2 attributes() - -Returns a hash of SAML attributes found in the assertion. - -=cut - -sub attributes { - my ($self) = @_; - return $self->{attributes}; -} - -=head2 session() - -Returns the SAML session identifier, which may be used in a -LogoutRequest to terminate this session. - -=cut - -sub session { - my ($self) = @_; - return $self->{session}; -} - -=head2 nameid() - -Returns the nameid in the Assertion. - -=cut - -sub nameid { - my ($self) = @_; - return $self->{nameid}; -} - =head2 name Returns the CN attribute, if provided. @@ -88,7 +63,7 @@ Returns the CN attribute, if provided. sub name { my ($self) = @_; - return $self->{attributes}->{CN}->[0]; + return $self->attributes->{CN}->[0]; } 1; diff --git a/lib/Net/SAML2/Protocol/AuthnRequest.pm b/lib/Net/SAML2/Protocol/AuthnRequest.pm index 935d60a..11bb752 100644 --- a/lib/Net/SAML2/Protocol/AuthnRequest.pm +++ b/lib/Net/SAML2/Protocol/AuthnRequest.pm @@ -1,6 +1,10 @@ package Net::SAML2::Protocol::AuthnRequest; -use strict; -use warnings; +use Moose; +use MooseX::Types::Moose qw /Str /; +use MooseX::Types::URI qw/ Uri /; + +with 'Net::SAML2::Role::Templater', + 'Net::SAML2::Role::ProtocolMessage'; =head1 NAME @@ -18,30 +22,19 @@ Net::SAML2::Protocol::AuthnRequest - SAML2 AuthnRequest object =cut -use DateTime::Format::XSD; - =head2 new( ... ) Constructor. Creates an instance of the AuthnRequest object. Arguments: - * issueinstant - a DateTime for "now" * issuer - the SP's identity URI * destination - the IdP's identity URI =cut -sub new { - my ($class, %args) = @_; - my $self = bless {}, $class; - - $self->{issueinstant} = $args{issueinstant}; - $self->{issuer} = $args{issuer}; - $self->{destination} = $args{destination}; - - return $self; -} +has 'issuer' => (isa => Uri, is => 'ro', required => 1, coerce => 1); +has 'destination' => (isa => Uri, is => 'ro', required => 1, coerce => 1); =head2 as_xml() @@ -52,25 +45,20 @@ Returns the AuthnRequest as XML. sub as_xml { my ($self) = @_; - my $issueinstant = DateTime::Format::XSD->format_datetime( - $self->{issueinstant} - ); - - my $xml =<<"EOXML"; - + my $template =<<'EOXML'; - $self->{issuer} + issuer ?> EOXML - return $xml; + return $self->template($template); } 1; diff --git a/lib/Net/SAML2/Protocol/LogoutRequest.pm b/lib/Net/SAML2/Protocol/LogoutRequest.pm index bd19855..4c2d73a 100644 --- a/lib/Net/SAML2/Protocol/LogoutRequest.pm +++ b/lib/Net/SAML2/Protocol/LogoutRequest.pm @@ -1,6 +1,10 @@ package Net::SAML2::Protocol::LogoutRequest; -use strict; -use warnings; +use Moose; +use MooseX::Types::Moose qw/ Str /; +use MooseX::Types::URI qw/ Uri /; + +with 'Net::SAML2::Role::Templater', + 'Net::SAML2::Role::ProtocolMessage'; =head1 NAME @@ -30,17 +34,10 @@ Arguments: =cut -sub new { - my ($class, %args) = @_; - my $self = bless {}, $class; - - $self->{session} = $args{session}; - $self->{nameid} = $args{nameid}; - $self->{issuer} = $args{issuer}; - $self->{destination} = $args{destination}; - - return $self; -} +has 'session' => (isa => Str, is => 'ro', required => 1); +has 'nameid' => (isa => Str, is => 'ro', required => 1); +has 'issuer' => (isa => Uri, is => 'ro', required => 1, coerce => 1); +has 'destination' => (isa => Uri, is => 'ro', required => 1, coerce => 1); =head2 new_from_xml @@ -50,17 +47,18 @@ Create a LogoutRequest object from the given XML. sub new_from_xml { my ($class, %args) = @_; - my $self = bless {}, $class; my $xpath = XML::XPath->new( xml => $args{xml} ); $xpath->set_namespace('saml', 'urn:oasis:names:tc:SAML:2.0:assertion'); $xpath->set_namespace('samlp', 'urn:oasis:names:tc:SAML:2.0:protocol'); - $self->{id} = $xpath->findvalue('/samlp:LogoutRequest/@ID')->value; - $self->{session} = $xpath->findvalue('/samlp:LogoutRequest/samlp:SessionIndex')->value; - $self->{issuer} = $xpath->findvalue('/samlp:LogoutRequest/saml:Issuer')->value; - $self->{nameid} = $xpath->findvalue('/samlp:LogoutRequest/saml:NameID')->value; - $self->{destination} = $xpath->findvalue('/samlp:LogoutRequest/saml:NameID/@NameQualifier')->value; + my $self = $class->new( + id => $xpath->findvalue('/samlp:LogoutRequest/@ID')->value, + session => $xpath->findvalue('/samlp:LogoutRequest/samlp:SessionIndex')->value, + issuer => $xpath->findvalue('/samlp:LogoutRequest/saml:Issuer')->value, + nameid => $xpath->findvalue('/samlp:LogoutRequest/saml:NameID')->value, + destination => $xpath->findvalue('/samlp:LogoutRequest/saml:NameID/@NameQualifier')->value, + ); return $self; } @@ -74,74 +72,19 @@ Returns the LogoutRequest as XML. sub as_xml { my ($self) = @_; - my $xml =<<"EOXML"; + my $template = <<'EOXML'; - $self->{issuer} + issuer ?> $self->{nameid} - $self->{session} + NameQualifier="destination ?>" + SPNameQualifier="issuer ?>">nameid ?> + session ?> EOXML - return $xml; -} - -=head2 id - -Returns the ID of the parsed response. - -=cut - -sub id { - my ($self) = @_; - return $self->{id}; -} - -=head2 session - -Returns the Session attribute of the parsed response. - -=cut - -sub session { - my ($self) = @_; - return $self->{session}; -} - -=head2 nameid - -Returns the NameID attribute of the parsed response. - -=cut - -sub nameid { - my ($self) = @_; - return $self->{nameid}; -} - -=head2 issuer - -Returns the Issuer URI of the parsed response. - -=cut - -sub issuer { - my ($self) = @_; - return $self->{issuer}; -} - -=head2 destination - -Returns the Destination URI of the parsed response. - -=cut - -sub destination { - my ($self) = @_; - return $self->{destination}; + return $self->template($template); } -1; +__PACKAGE__->meta->make_immutable; diff --git a/lib/Net/SAML2/Protocol/LogoutResponse.pm b/lib/Net/SAML2/Protocol/LogoutResponse.pm index ace982e..61623f8 100644 --- a/lib/Net/SAML2/Protocol/LogoutResponse.pm +++ b/lib/Net/SAML2/Protocol/LogoutResponse.pm @@ -1,6 +1,10 @@ package Net::SAML2::Protocol::LogoutResponse; -use strict; -use warnings; +use Moose; +use MooseX::Types::Moose qw/ Str /; +use MooseX::Types::URI qw/ Uri /; + +with 'Net::SAML2::Role::Templater', + 'Net::SAML2::Role::ProtocolMessage'; =head1 NAME @@ -30,17 +34,10 @@ Arguments: =cut -sub new { - my ($class, %args) = @_; - my $self = bless {}, $class; - - $self->{issuer} = $args{issuer}; - $self->{destination} = $args{destination}; - $self->{status} = $args{status}; - $self->{response_to} = $args{response_to}; - - return $self; -} +has 'issuer' => (isa => Uri, is => 'ro', required => 1, coerce => 1); +has 'destination' => (isa => Uri, is => 'ro', required => 1, coerce => 1); +has 'status' => (isa => Str, is => 'ro', required => 1); +has 'response_to' => (isa => Str, is => 'ro', required => 1); =head2 new_from_xml @@ -50,18 +47,19 @@ Create a LogoutResponse object from the given XML. sub new_from_xml { my ($class, %args) = @_; - my $self = bless {}, $class; my $xpath = XML::XPath->new( xml => $args{xml} ); $xpath->set_namespace('saml', 'urn:oasis:names:tc:SAML:2.0:assertion'); $xpath->set_namespace('samlp', 'urn:oasis:names:tc:SAML:2.0:protocol'); - $self->{id} = $xpath->findvalue('/samlp:LogoutResponse/@ID')->value; - $self->{response_to} = $xpath->findvalue('/samlp:LogoutResponse/@InResponseTo')->value; - $self->{destination} = $xpath->findvalue('/samlp:LogoutResponse/@Destination')->value; - $self->{session} = $xpath->findvalue('/samlp:LogoutResponse/samlp:SessionIndex')->value; - $self->{issuer} = $xpath->findvalue('/samlp:LogoutResponse/saml:Issuer')->value; - $self->{status} = $xpath->findvalue('/samlp:LogoutResponse/samlp:Status/samlp:StatusCode/@Value')->value; + my $self = $class->new( + id => $xpath->findvalue('/samlp:LogoutResponse/@ID')->value, + response_to => $xpath->findvalue('/samlp:LogoutResponse/@InResponseTo')->value, + destination => $xpath->findvalue('/samlp:LogoutResponse/@Destination')->value, + session => $xpath->findvalue('/samlp:LogoutResponse/samlp:SessionIndex')->value, + issuer => $xpath->findvalue('/samlp:LogoutResponse/saml:Issuer')->value, + status => $xpath->findvalue('/samlp:LogoutResponse/samlp:Status/samlp:StatusCode/@Value')->value, + ); return $self; } @@ -75,87 +73,21 @@ Returns the LogoutResponse as XML. sub as_xml { my ($self) = @_; - my $xml =<<"EOXML"; + my $template =<<'EOXML'; - $self->{issuer} + IssueInstant="issue_instant ?>" + Destination="destination ?>" + InResponseTo="response_to ?>"> + issuer ?> - + EOXML - - return $xml; -} - -=head2 id - -Returns the ID of the parsed response. - -=cut - -sub id { - my ($self) = @_; - return $self->{id}; -} - -=head2 session - -Returns the Session attribute of the parsed response. - -=cut - -sub session { - my ($self) = @_; - return $self->{session}; -} - -=head2 response_to - -Returns the InResponseTo attribute of the parsed response. - -=cut -sub response_to { - my ($self) = @_; - return $self->{response_to}; -} - -=head2 issuer - -Returns the issuer URI of the parsed response. - -=cut - -sub issuer { - my ($self) = @_; - return $self->{issuer}; -} - -=head2 destination - -Returns the destination URI of the parsed response. - -=cut - -sub destination { - my ($self) = @_; - return $self->{destination}; -} - -=head2 status - -Returns the status URI of the parsed response. - -=cut - -sub status { - my ($self) = @_; - return $self->{status}; + return $self->template($template); } 1; diff --git a/lib/Net/SAML2/Role/ProtocolMessage.pm b/lib/Net/SAML2/Role/ProtocolMessage.pm new file mode 100644 index 0000000..ec4406e --- /dev/null +++ b/lib/Net/SAML2/Role/ProtocolMessage.pm @@ -0,0 +1,35 @@ +package Net::SAML2::Role::ProtocolMessage; +use Moose::Role; +use MooseX::Types::Moose qw/ Str /; +use DateTime::Format::XSD; +use Crypt::OpenSSL::Random; + +=head1 NAME + +Net::SAML2::Role::Templater - defaults for Protocol classes + +=head1 DESCRIPTION + +Provides default ID and timestamp arguments for Protocol classes. + +=cut + +has 'id' => (isa => Str, is => 'ro', required => 1); +has 'issue_instant' => (isa => Str, is => 'ro', required => 1); + +around 'BUILDARGS' => sub { + my $orig = shift; + my $class = shift; + my %args = @_; + + # random ID for this message + $args{id} ||= unpack 'H*', Crypt::OpenSSL::Random::random_pseudo_bytes(16); + + # IssueInstant in UTC + my $dt = DateTime->now( time_zone => 'UTC' ); + $args{issue_instant} ||= $dt->strftime('%FT%TZ'); + + return \%args; +}; + +1; diff --git a/lib/Net/SAML2/Role/Templater.pm b/lib/Net/SAML2/Role/Templater.pm new file mode 100644 index 0000000..f97878c --- /dev/null +++ b/lib/Net/SAML2/Role/Templater.pm @@ -0,0 +1,28 @@ +package Net::SAML2::Role::Templater; +use Moose::Role; +use Text::MicroTemplate qw/ build_mt /; + +=head1 NAME + +Net::SAML2::Role::Templater - simple templater routine for Protocol classes + +=head1 DESCRIPTION + +Template-processor role for Protocol classes. + +=head1 METHODS + +=head2 template($template) + +Evaluates the given template using $self as the context. + +=cut + +sub template { + my ($self, $template) = @_; + my $renderer = build_mt($template); + my $xml = $renderer->($self)->as_string; + return $xml; +} + +1; diff --git a/lib/Net/SAML2/SP.pm b/lib/Net/SAML2/SP.pm index b5fc994..3c1fe50 100644 --- a/lib/Net/SAML2/SP.pm +++ b/lib/Net/SAML2/SP.pm @@ -1,6 +1,9 @@ package Net::SAML2::SP; -use strict; -use warnings; +use Moose; +use MooseX::Types::Moose qw/ Str /; +use MooseX::Types::URI qw/ Uri /; + +with 'Net::SAML2::Role::Templater'; =head1 NAME @@ -37,23 +40,25 @@ Arguments: =cut -sub new { - my ($class, %args) = @_; - my $self = bless {}, $class; +has 'url' => (isa => Uri, is => 'ro', required => 1, coerce => 1); +has 'id' => (isa => Str, is => 'ro', required => 1); +has 'cert' => (isa => Str, is => 'ro', required => 1); +has 'cacert' => (isa => Str, is => 'ro', required => 1); - $self->{cacert_path} = $args{cacert}; - $self->{cert_path} = $args{cert}; - $self->{url} = $args{url}; - $self->{id} = $args{id}; +has 'org_name' => (isa => Str, is => 'ro', required => 1); +has 'org_display_name' => (isa => Str, is => 'ro', required => 1); +has 'org_contact' => (isa => Str, is => 'ro', required => 1); - $self->{org_name} = $args{org_name}; - $self->{org_display_name} = $args{org_display_name}; - $self->{org_contact} = $args{org_contact}; +has '_cert_text' => (isa => Str, is => 'rw', required => 0); - my $cert = Crypt::OpenSSL::X509->new_from_file($args{cert}); - $self->{cert} = $cert->as_string; - $self->{cert} =~ s/-----[^-]*-----//gm; +sub BUILD { + my ($self) = @_; + my $cert = Crypt::OpenSSL::X509->new_from_file($self->cert); + my $text = $cert->as_string; + $text =~ s/-----[^-]*-----//gm; + $self->_cert_text($text); + return $self; } @@ -69,7 +74,7 @@ sub authn_request { my $authnreq = Net::SAML2::Protocol::AuthnRequest->new( issueinstant => DateTime->now, - issuer => $self->{id}, + issuer => $self->id, destination => $destination, ); @@ -89,7 +94,7 @@ sub logout_request { my ($self, $destination, $nameid, $session) = @_; my $logout_req = Net::SAML2::Protocol::LogoutRequest->new( - issuer => $self->{id}, + issuer => $self->id, destination => $destination, nameid => $nameid, session => $session, @@ -112,7 +117,7 @@ sub logout_response { my ($self, $destination, $status, $response_to) = @_; my $logout_req = Net::SAML2::Protocol::LogoutResponse->new( - issuer => $self->{id}, + issuer => $self->id, destination => $destination, status => $status, response_to => $response_to, @@ -133,7 +138,7 @@ sub artifact_request { my ($self, $destination, $artifact) = @_; my $artifact_request = Net::SAML2::Protocol::ArtifactResolve->new( - issuer => $self->{id}, + issuer => $self->id, destination => $destination, artifact => $artifact, issueinstant => DateTime->now, @@ -142,19 +147,43 @@ sub artifact_request { return $artifact_request; } -=head2 redirect_binding +=head2 sso_redirect_binding($idp, $param) + +Returns a Redirect binding object for this SP, configured against the +given IDP for Single Sign On. $param specifies the name of the query +parameter involved - typically SAMLRequest. + +=cut + +sub sso_redirect_binding { + my ($self, $idp, $param) = @_; + + my $redirect = Net::SAML2::Binding::Redirect->new( + url => $idp->sso_url('urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect'), + cert => $idp->cert('signing'), + key => $self->cert, + param => $param, + ); + + return $redirect; +} + +=head2 slo_redirect_binding Returns a Redirect binding object for this SP, configured against the -given IDP. +given IDP for Single Log Out. $param specifies the name of the query +parameter involved - typically SAMLRequest or SAMLResponse. =cut -sub redirect_binding { - my ($self, $idp) = @_; +sub slo_redirect_binding { + my ($self, $idp, $param) = @_; my $redirect = Net::SAML2::Binding::Redirect->new( - key => $self->{cert_path}, - url => $idp, + url => $idp->slo_url('urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect'), + cert => $idp->cert('signing'), + key => $self->cert, + param => $param, ); return $redirect; @@ -174,8 +203,8 @@ sub soap_binding { my $soap = Net::SAML2::Binding::SOAP->new( ua => $ua, - key => $self->{cert_path}, - cert => $self->{cert_path}, + key => $self->cert, + cert => $self->cert, url => $idp_url, idp_cert => $idp_cert, ); @@ -193,7 +222,7 @@ sub post_binding { my ($self) = @_; my $post = Net::SAML2::Binding::POST->new( - cacert => $self->{cacert_path}, + cacert => $self->cacert, ); return $post; @@ -208,33 +237,34 @@ Returns the metadata XML document for this SP. sub metadata { my ($self) = @_; - return <<"METADATA"; - - - + my $template = <<'EOXML'; + + -$self->{cert} +cert ?> - - + + - $self->{org_name} - $self->{org_display_name} - $self->{url}/ + org_name ?> + org_display_name ?> + url ?>/ - $self->{org_display_name} - $self->{org_contact} + org_display_name ?> + org_contact ?> -METADATA +EOXML + + return $self->template($template); } 1; diff --git a/t/01-create-idp.t b/t/01-create-idp.t index d0b2230..abd06e5 100644 --- a/t/01-create-idp.t +++ b/t/01-create-idp.t @@ -9,17 +9,23 @@ my $xml = < -MIICQDCCAakCBEeNB0swDQYJKoZIhvcNAQEEBQAwZzELMAkGA1UEBhMCVVMxEzARBgNVBAgTCkNh -bGlmb3JuaWExFDASBgNVBAcTC1NhbnRhIENsYXJhMQwwCgYDVQQKEwNTdW4xEDAOBgNVBAsTB09w -ZW5TU08xDTALBgNVBAMTBHRlc3QwHhcNMDgwMTE1MTkxOTM5WhcNMTgwMTEyMTkxOTM5WjBnMQsw -CQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEUMBIGA1UEBxMLU2FudGEgQ2xhcmExDDAK -BgNVBAoTA1N1bjEQMA4GA1UECxMHT3BlblNTTzENMAsGA1UEAxMEdGVzdDCBnzANBgkqhkiG9w0B -AQEFAAOBjQAwgYkCgYEArSQc/U75GB2AtKhbGS5piiLkmJzqEsp64rDxbMJ+xDrye0EN/q1U5Of+ -RkDsaN/igkAvV1cuXEgTL6RlafFPcUX7QxDhZBhsYF9pbwtMzi4A4su9hnxIhURebGEmxKW9qJNY -Js0Vo5+IgjxuEWnjnnVgHTs1+mq5QYTA7E6ZyL8CAwEAATANBgkqhkiG9w0BAQQFAAOBgQB3Pw/U -QzPKTPTYi9upbFXlrAKMwtFf2OW4yvGWWvlcwcNSZJmTJ8ARvVYOMEVNbsT4OFcfu2/PeYoAdiDA -cGy/F2Zuj8XJJpuQRSE6PtQqBuDEHjjmOQJ0rV/r8mO1ZCtHRhpZ5zYRjhRC9eCbjx9VrFax0JDC -/FfwWigmrW0Y0Q== +MIIDFTCCAf2gAwIBAgIBATANBgkqhkiG9w0BAQUFADA3MQswCQYDVQQGEwJVUzEO +MAwGA1UECgwFbG9jYWwxCzAJBgNVBAsMAmN0MQswCQYDVQQDDAJDQTAeFw0xMDEw +MDYxMjM4MTRaFw0xMTEwMDYxMjM4MTRaMFcxCzAJBgNVBAYTAlVTMQ4wDAYDVQQK +DAVsb2NhbDELMAkGA1UECwwCY3QxDTALBgNVBAMMBHNhbWwxHDAaBgkqhkiG9w0B +CQEWDXNhbWxAY3QubG9jYWwwgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAMhu +pJZpvu1m6ys+IrWrm3pK+onwRAYCyrgQ0RyK2cHbVLFbjBqTjKnt+PiVbnZPZUTs +tkV9oijZGQvaMy9ingJursICUQzmOfYRDm4s9gFJJOHUGYnItRhp4uj3EoWWyX8I +6Mr+g3/vNgNFvD5S9L7Hk1mSw8SnPlblZAWlFUwXAgMBAAGjgY8wgYwwDAYDVR0T +AQH/BAIwADAxBglghkgBhvhCAQ0EJBYiUnVieS9PcGVuU1NMIEdlbmVyYXRlZCBD +ZXJ0aWZpY2F0ZTAdBgNVHQ4EFgQUGy/iPd7PVObrF+lK4+ZShcbStLYwCwYDVR0P +BAQDAgXgMB0GA1UdJQQWMBQGCCsGAQUFBwMCBggrBgEFBQcDBDANBgkqhkiG9w0B +AQUFAAOCAQEAYoYq3Rc6jC7f8DnKxDHntHxH91F5mfp8Y3j7ALcRG/mrzkMhvxU2 +O2qmh4aHzZBoY1EU9VjrVgyPJPAjFQVC+OjIE46Gavh5wobzYmVGeFLOa9NhPv50 +h3EOw1eCda3VwcvStWw1OhT8cpEGqgJJVAcjwcm4VBtWjodxRn3E4zBr/xxzR1HU +ISvnu1/xomsSS+aenG5toWmhoJIKFbfhQkpnBlgGD5+12Cxn2jHpgv15262ZZIJS +WPp/0bQqdAAUzkJZPpUGUN1sTXPJexYT6na7XvLd6mvO1g+WDk6aZnW/zcT3T9tL +Iavyic/p4gZtXckweq+VTn9CdZp6ZTQtVw== @@ -48,7 +54,7 @@ cGy/F2Zuj8XJJpuQRSE6PtQqBuDEHjjmOQJ0rV/r8mO1ZCtHRhpZ5zYRjhRC9eCbjx9VrFax0JDC XML -my $idp = Net::SAML2::IdP->new($xml); +my $idp = Net::SAML2::IdP->new_from_xml( xml => $xml, cacert => 't/cacert.pem' ); ok($idp); my $redirect_uri = 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect'; @@ -59,6 +65,6 @@ ok($idp->slo_url($redirect_uri)); ok($idp->art_url($soap_uri)); ok($idp->cert('signing')); -ok($idp->entityID eq 'http://sso.dev.venda.com/opensso'); +ok($idp->entityid eq 'http://sso.dev.venda.com/opensso'); done_testing; diff --git a/t/03-assertions.t b/t/03-assertions.t index 2dd1f8d..f474981 100644 --- a/t/03-assertions.t +++ b/t/03-assertions.t @@ -60,7 +60,7 @@ my $xml = < XML -my $assertion = Net::SAML2::Protocol::Assertion->new( +my $assertion = Net::SAML2::Protocol::Assertion->new_from_xml( xml => $xml ); ok($assertion); diff --git a/t/04-response.t b/t/04-response.t index 1b9dec4..1b53eba 100644 --- a/t/04-response.t +++ b/t/04-response.t @@ -77,7 +77,7 @@ ok(qr/verified/, $subject); #diag "subject: $subject\n"; my $assertion_xml = decode_base64($response); -my $assertion = Net::SAML2::Protocol::Assertion->new( +my $assertion = Net::SAML2::Protocol::Assertion->new_from_xml( xml => $xml, ); ok($assertion); diff --git a/t/05-soap-binding.t b/t/05-soap-binding.t index 72bc9e9..410d8cf 100644 --- a/t/05-soap-binding.t +++ b/t/05-soap-binding.t @@ -3,7 +3,6 @@ use strict; use warnings; use Net::SAML2; use MIME::Base64; -use Data::Dumper; use File::Slurp; use LWP::UserAgent; @@ -20,7 +19,7 @@ ok($sp); my $metadata = read_file('t/idp-metadata.xml'); ok($metadata); -my $idp = Net::SAML2::IdP->new($metadata); +my $idp = Net::SAML2::IdP->new_from_xml( xml => $metadata, cacert => 't/cacert.pem' ); ok($idp); my $slo_url = $idp->slo_url('urn:oasis:names:tc:SAML:2.0:bindings:SOAP'); ok($slo_url); @@ -31,7 +30,7 @@ my $nameid = 'user-to-log-out'; my $session = 'session-to-log-out'; my $request = $sp->logout_request( - $idp->entityID, $nameid, $session, + $idp->entityid, $nameid, $session, )->as_xml; ok($request); diff --git a/t/06-redirect-binding.t b/t/06-redirect-binding.t index d767a6b..0ac8e8a 100644 --- a/t/06-redirect-binding.t +++ b/t/06-redirect-binding.t @@ -20,24 +20,24 @@ ok($sp); my $metadata = read_file('t/idp-metadata.xml'); ok($metadata); -my $idp = Net::SAML2::IdP->new($metadata); +my $idp = Net::SAML2::IdP->new_from_xml( xml => $metadata, cacert => 't/cacert.pem' ); ok($idp); my $sso_url = $idp->sso_url('urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect'); ok($sso_url); -my $authnreq = $sp->authn_request($sso_url)->as_xml; +my $authnreq = $sp->authn_request($idp->entityid)->as_xml; ok($authnreq); -my $redirect = $sp->redirect_binding($sso_url); +my $redirect = $sp->sso_redirect_binding($idp, 'SAMLRequest'); ok($redirect); -my $location = $redirect->sign_request( +my $location = $redirect->sign( $authnreq, 'http://return/url', ); ok($location); -my ($request, $relaystate) = $redirect->handle_request($location); +my ($request, $relaystate) = $redirect->verify($location); ok($request); ok($relaystate); ok($relaystate eq 'http://return/url'); diff --git a/t/07-logout-request.t b/t/07-logout-request.t new file mode 100644 index 0000000..bc93701 --- /dev/null +++ b/t/07-logout-request.t @@ -0,0 +1,20 @@ +use Test::More; +use strict; +use warnings; +use Net::SAML2; + +my $lor = Net::SAML2::Protocol::LogoutRequest->new( + issuer => 'http://some/sp', + destination => 'http://some/idp', + nameid => 'name-to-log-out', + session => 'session-to-log-out', +); +ok($lor); +my $xml = $lor->as_xml; +ok($xml); +#diag($xml); + +ok(qr/ID=".+"/, $xml); +ok(qr/IssueInstant=".+"/, $xml); + +done_testing; diff --git a/t/08-logout-response.t b/t/08-logout-response.t new file mode 100644 index 0000000..da51783 --- /dev/null +++ b/t/08-logout-response.t @@ -0,0 +1,20 @@ +use Test::More; +use strict; +use warnings; +use Net::SAML2; + +my $lor = Net::SAML2::Protocol::LogoutResponse->new( + issuer => 'http://some/sp', + destination => 'http://some/idp', + status => 'urn:oasis:names:tc:SAML:2.0:status:Success', + response_to => 'randomID', +); +ok($lor); +my $xml = $lor->as_xml; +ok($xml); +#diag($xml); + +ok(qr/ID=".+"/, $xml); +ok(qr/IssueInstant=".+"/, $xml); + +done_testing; diff --git a/t/09-authn-request.t b/t/09-authn-request.t new file mode 100644 index 0000000..1b699f9 --- /dev/null +++ b/t/09-authn-request.t @@ -0,0 +1,18 @@ +use Test::More; +use strict; +use warnings; +use Net::SAML2; + +my $ar = Net::SAML2::Protocol::AuthnRequest->new( + issuer => 'http://some/sp', + destination => 'http://some/idp', +); +ok($ar); +my $xml = $ar->as_xml; +ok($xml); +#diag($xml); + +ok(qr/ID=".+"/, $xml); +ok(qr/IssueInstant=".+"/, $xml); + +done_testing; diff --git a/t/10-artifact-resolve.t b/t/10-artifact-resolve.t new file mode 100644 index 0000000..82f8c2b --- /dev/null +++ b/t/10-artifact-resolve.t @@ -0,0 +1,19 @@ +use Test::More; +use strict; +use warnings; +use Net::SAML2; + +my $ar = Net::SAML2::Protocol::ArtifactResolve->new( + issuer => 'http://some/sp', + destination => 'http://some/idp', + artifact => 'some-artifact', +); +ok($ar); +my $xml = $ar->as_xml; +ok($xml); +#diag($xml); + +ok(qr/ID=".+"/, $xml); +ok(qr/IssueInstant=".+"/, $xml); + +done_testing; diff --git a/t/author/podcoverage.t b/t/author/podcoverage.t index 3abc25e..7c5d01e 100755 --- a/t/author/podcoverage.t +++ b/t/author/podcoverage.t @@ -5,4 +5,4 @@ use Test::More; use Test::Pod::Coverage 1.04; -all_pod_coverage_ok(); +all_pod_coverage_ok( { also_private => ['BUILD'] } ); diff --git a/testapp/lib/Saml2Test.pm b/testapp/lib/Saml2Test.pm index f85fe19..7ca504a 100644 --- a/testapp/lib/Saml2Test.pm +++ b/testapp/lib/Saml2Test.pm @@ -23,24 +23,12 @@ get '/' => sub { }; get '/login' => sub { - my $idp = Net::SAML2::IdP->new_from_url(config->{idp}); - - my $sp = Net::SAML2::SP->new( - id => 'http://localhost:3000', - url => 'http://localhost:3000', - cert => 'sign-nopw-cert.pem', - key => 'sign-nopw-cert.pem', - ); - - my $sso_url = $idp->sso_url('urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect'); - my $authnreq = $sp->authn_request($idp->entityID)->as_xml; - - my $redirect = Net::SAML2::Binding::Redirect->new( - key => 'sign-nopw-cert.pem', - url => $sso_url, - ); - - my $url = $redirect->sign_request($authnreq); + my $idp = _idp(); + my $sp = _sp(); + my $authnreq = $sp->authn_request($idp->entityid)->as_xml; + + my $redirect = $sp->sso_redirect_binding($idp, 'SAMLRequest'); + my $url = $redirect->sign($authnreq); redirect $url, 302; return "Redirected\n"; @@ -51,41 +39,28 @@ get '/logout-local' => sub { }; get '/logout-redirect' => sub { - my $idp = Net::SAML2::IdP->new_from_url(config->{idp}); - my $slo_url = $idp->slo_url('urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect'); - - my $sp = Net::SAML2::SP->new( - id => 'http://localhost:3000', - url => 'http://localhost:3000', - cert => 'sign-nopw-cert.pem', - ); + my $idp = _idp(); + my $sp = _sp(); + my $logoutreq = $sp->logout_request( - $idp->entityID, params->{nameid}, params->{session} + $idp->entityid, params->{nameid}, params->{session} )->as_xml; - my $redirect = Net::SAML2::Binding::Redirect->new( - key => 'sign-nopw-cert.pem', - url => $slo_url, - ); - - my $url = $redirect->sign_request($logoutreq); + my $redirect = $sp->slo_redirect_binding($idp, 'SAMLRequest'); + my $url = $redirect->sign($logoutreq); redirect $url, 302; return "Redirected\n"; }; get '/logout-soap' => sub { - my $idp = Net::SAML2::IdP->new_from_url(config->{idp}); + my $idp = _idp(); my $slo_url = $idp->slo_url('urn:oasis:names:tc:SAML:2.0:bindings:SOAP'); my $idp_cert = $idp->cert('signing'); - - my $sp = Net::SAML2::SP->new( - id => 'http://localhost:3000', - url => 'http://localhost:3000', - cert => 'sign-nopw-cert.pem', - ); + + my $sp = _sp(); my $logoutreq = $sp->logout_request( - $idp->entityID, params->{nameid}, params->{session} + $idp->entityid, params->{nameid}, params->{session} )->as_xml; my $soap = Net::SAML2::Binding::SOAP->new( @@ -110,7 +85,7 @@ post '/consumer-post' => sub { ); if ($ret) { - my $assertion = Net::SAML2::Protocol::Assertion->new( + my $assertion = Net::SAML2::Protocol::Assertion->new_from_xml( xml => decode_base64(params->{SAMLResponse}) ); @@ -122,18 +97,14 @@ post '/consumer-post' => sub { }; get '/consumer-artifact' => sub { - my $idp = Net::SAML2::IdP->new_from_url(config->{idp}); + my $idp = _idp(); my $idp_cert = $idp->cert('signing'); my $art_url = $idp->art_url('urn:oasis:names:tc:SAML:2.0:bindings:SOAP'); my $artifact = params->{SAMLart}; - my $sp = Net::SAML2::SP->new( - id => 'http://localhost:3000', - url => 'http://localhost:3000', - cert => 'sign-nopw-cert.pem', - ); - my $request = $sp->artifact_request($idp->entityID, $artifact)->as_xml; + my $sp = _sp(); + my $request = $sp->artifact_request($idp->entityid, $artifact)->as_xml; my $soap = Net::SAML2::Binding::SOAP->new( url => $art_url, @@ -144,7 +115,7 @@ get '/consumer-artifact' => sub { my $response = $soap->request($request); if ($response) { - my $assertion = Net::SAML2::Protocol::Assertion->new( + my $assertion = Net::SAML2::Protocol::Assertion->new_from_xml( xml => $response ); @@ -156,22 +127,42 @@ get '/consumer-artifact' => sub { }; get '/sls-redirect-response' => sub { - my $post = Net::SAML2::Binding::Redirect->new; - my $ret = $post->handle_response( - params->{SAMLResponse} - ); + my $idp = _idp(); + my $idp_cert = $idp->cert('signing'); + + my $sp = _sp(); + my $redirect = $sp->slo_redirect_binding($idp, 'SAMLResponse'); + my ($response, $relaystate) = $redirect->verify(request->request_uri); - redirect '/', 302; + redirect $relaystate || '/', 302; return "Redirected\n"; }; get '/metadata.xml' => sub { - my $sp = Net::SAML2::SP->new( - id => 'http://localhost:3000', - url => 'http://localhost:3000', - cert => 'sign-nopw-cert.pem', - ); + my $sp = _sp(); return $sp->metadata; }; +sub _sp { + my $sp = Net::SAML2::SP->new( + id => 'http://localhost:3000', + url => 'http://localhost:3000', + cert => 'sign-nopw-cert.pem', + cacert => 'saml_cacert.pem', + + org_name => 'Saml2Test', + org_display_name => 'Saml2Test app for Net::SAML2', + org_contact => 'saml2test@example.com', + ); + return $sp; +} + +sub _idp { + my $idp = Net::SAML2::IdP->new_from_url( + url => config->{idp}, + cacert => 'saml_cacert.pem' + ); + return $idp; +} + true; diff --git a/testapp/t/001_base.t b/testapp/t/001_base.t index f92daf4..1451bf4 100644 --- a/testapp/t/001_base.t +++ b/testapp/t/001_base.t @@ -1,5 +1,7 @@ -use Test::More tests => 1; +use Test::More; use strict; use warnings; use_ok 'Saml2Test'; + +done_testing; diff --git a/testapp/t/002_index_route.t b/testapp/t/002_index_route.t index f65a1dc..c6092ea 100644 --- a/testapp/t/002_index_route.t +++ b/testapp/t/002_index_route.t @@ -1,4 +1,4 @@ -use Test::More tests => 3; +use Test::More; use strict; use warnings; @@ -10,3 +10,5 @@ route_exists [GET => '/'], 'a route handler is defined for /'; response_status_is ['GET' => '/'], 200, 'response status is 200 for /'; response_content_like [GET => '/'], qr/Log In/s, 'content looks OK for /'; + +done_testing; diff --git a/testapp/t/003_live_login.t b/testapp/t/003_live_login.t new file mode 100644 index 0000000..8bf5293 --- /dev/null +++ b/testapp/t/003_live_login.t @@ -0,0 +1,54 @@ +use Test::More; +use strict; +use warnings; + +use WWW::Mechanize; + +# the order is important +use Saml2Test; +use Dancer::Test; + +# interact with the IdP live +my $mech = WWW::Mechanize->new; + +## start a login +my $login = dancer_response(GET => '/login'); +ok($login); +ok($login->{status} == 302); +ok($login->{headers}->header('location')); + +## redirected to IdP login form + +$mech->get( $login->{headers}->header('location') ); +my $form = $mech->form_name('Login'); + +## submit login for with test credentials + +my $params = { + IDButton => 'Submit', + IDToken0 => '', + IDToken1 => 'demo', + IDToken2 => 'demodemo', +}; +for my $input ($form->inputs) { + $params->{$input->name} = $input->value; +} + +my $response = $mech->post($form->action, $params); +ok($mech->title eq 'Access rights validated'); + +## post the SAMLResponse form to the app + +$form = $mech->form_number(1); +ok($form); +ok(qr!/consumer-post!, $form->action); + +$params = { + SAMLResponse => $form->param('SAMLResponse'), +}; + +my $post = dancer_response(POST => '/consumer-post', { params => $params }); +is $post->{status}, 200; +ok(qr/User: /, $post->{content}); + +done_testing;