The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
package Authen::NZigovt::AuthenRequest;
BEGIN {
  $Authen::NZigovt::AuthenRequest::VERSION = '1.05';
}

use strict;
use warnings;

require XML::Generator;

use MIME::Base64               qw(encode_base64 decode_base64);
use URI::Escape                qw(uri_escape uri_unescape);
use IO::Uncompress::RawInflate qw(rawinflate $RawInflateError);
use IO::Compress::RawDeflate   qw(rawdeflate $RawDeflateError);

my $ns_saml  = [ saml  => 'urn:oasis:names:tc:SAML:2.0:assertion' ];
my $ns_samlp = [ samlp => 'urn:oasis:names:tc:SAML:2.0:protocol'  ];


sub new {
    my $class = shift;
    my $sp    = shift;

    my $self = bless {
        allow_create    => 'false',
        force_auth      => 'true',
        auth_strength   => 'low',
        @_,
    }, $class;
    return $self->_init($sp);
}


sub _init {
    my($self, $sp) = @_;

    $self->{request_id}      = $sp->generate_saml_id('AuthnRequest');
    $self->{entity_id}       = $sp->entity_id;
    $self->{destination_url} = $sp->idp->single_signon_location;
    $self->{request_time}    = $sp->now_as_iso();
    $self->{nameid_format}   = $sp->nameid_format();

    my $strength_class       = Authen::NZigovt->class_for('logon_strength');
    $self->{auth_strength}   = $strength_class->new($self->{auth_strength});

    my $xml = $self->_generate_authn_request_doc();
    $self->{query_string} = $sp->sign_query_string($self->_raw_query_string);

    return $self;
}


sub request_id      { shift->{request_id};          }
sub entity_id       { shift->{entity_id};           }
sub request_time    { shift->{request_time};        }
sub destination_url { shift->{destination_url};     }
sub saml_request    { shift->{saml_request};        }
sub relay_state     { shift->{relay_state};         }
sub allow_create    { shift->_bool('allow_create'); }
sub force_auth      { shift->_bool('force_auth');   }
sub auth_strength   { shift->{auth_strength};       }
sub _query_string   { shift->{query_string};        }
sub _nameid_format  { shift->{nameid_format};       }
sub _x              { shift->{x};                   }


sub _bool {
    my($self, $flag) = @_;
    my $value = shift->{$flag};
    return (defined($value) && lc($value) =~ /^(1|true)$/)
           ? 'true'
           : 'false';
}


sub as_url {
    my $self = shift;

    return $self->destination_url . '?' . $self->_query_string;
}


sub _raw_query_string {
    my $self = shift;

    my $qs = 'SAMLRequest=' . uri_escape( $self->encoded_saml_request() );

    if(my $rs = $self->relay_state) {
        $qs .= '&RelayState=' . uri_escape($rs);
    }

    return $qs;
}


sub _generate_authn_request_doc {
    my $self = shift;

    my $x = XML::Generator->new(#':pretty',
        namespace => [ @$ns_saml, @$ns_samlp ],
    );
    $self->{x} = $x;

    $self->{saml_request} = $x->AuthnRequest($ns_samlp,
        {
            Version                       => '2.0',
            ID                            => $self->request_id(),
            IssueInstant                  => $self->request_time(),
            Destination                   => $self->destination_url(),
            ForceAuthn                    => $self->force_auth(),
            AssertionConsumerServiceIndex => '0',
        },
        $self->_issuer(),
        $self->_nameid_policy(),
        $self->_authen_context(),
    ) . '';  # ensure result is stringified
}


sub _issuer {
    my $self = shift;
    my $x    = $self->_x;

    return $x->Issuer($ns_saml,
        $self->entity_id
    );
}


sub _nameid_policy {
    my $self = shift;
    my $x    = $self->_x;

    return $x->NameIDPolicy($ns_samlp,
        {
            Format      => $self->_nameid_format(),
            AllowCreate => $self->allow_create(),
        },
    );
}


sub _authen_context {
    my $self = shift;
    my $x    = $self->_x;

    my $strength = $self->auth_strength();
    return $x->RequestedAuthnContext($ns_samlp,
        $x->AuthnContextClassRef($ns_saml, $strength->urn()),
    );
}


sub encoded_saml_request {
    my($self) = @_;

    my $xml    = $self->saml_request();
    my $data   = '';
    my $status = rawdeflate \$xml => \$data, Append => 0
        or die "Can't compress request data: $RawDeflateError\n";

    $data = encode_base64($data);
    $data =~ s{[\r\n]}{}g;

    return $data;
}


sub dump_request {
    my $class = shift;

    # Get data from commandline or slurp from standard input

    my $data = @_
               ? shift
               : do { local($/) = undef; <STDIN>; };

    my $xml  = $data =~ m{^https?:}
               ? $class->_request_from_uri($data)
               : $class->_request_from_uri_param($data);

    print $xml, "\n";
}


sub _request_from_uri {
    my($class, $uri) = @_;

    my($data) = $uri =~ m{\bSAMLRequest=(.*?)(?:&|$)}
        or die "Can't find 'SAMLRequest' parameter in query string";
    return  $class->_request_from_uri_param($data);
}


sub _request_from_uri_param {
    my($class, $data) = @_;

    $data = uri_unescape($data);
    $data = decode_base64($data);

    my($xml, $status);
    $status = rawinflate \$data => \$xml
        or die "Can't decompress request data: $RawInflateError\n";

    return $xml;
}

1;

__END__

=head1 NAME

Authen::NZigovt::AuthenRequest - Generate a SAML2 AuthenRequest message

=head1 DESCRIPTION

This package is used by the L<Authen::NZigovt::ServiceProvider> to generate a
SAML2 AuthnRequest message and send it to the NZ igovt logon service Identity
provider using the HTTP-Redirect binding.

=head1 METHODS

=head2 new

Constructor.  Should not be called directly.  Instead, call the C<new_request>
method on the service provider object.

The following named parameters are recognised:

  allow_create     boolean       (default: false)
  force_auth       boolean       (default: true)
  relay_state      short string  (default: none)
  auth_strength    see below     (default: 'low')

=head2 request_id

Accessor for the generated unique ID for this request.

=head2 entity_id

Accessor for the entity ID of the Service Provider which generated the request.

=head2 request_time

Accessor for the request creation time formatted as an ISO date/time string.

=head2 destination_url

Accessor for the URL of the Identity Provider's single signon service, to which
this request will be sent.

=head2 saml_request

Accessor for the XML document containing the SAML2 AuthenRequest.

=head2 relay_state

Accessor for the C<relay_state> parameter optionally passed to the constructor.
If not provided, no relay state will be passed to the Identity Provider.

=head2 allow_create

Accessor for the C<allow_create> parameter optionally passed to the constructor.
If not provided, this parameter will default to 'false'.

=head2 force_auth

Accessor for the C<force_auth> parameter optionally passed to the constructor.
If not provided, this parameter will default to 'true'.

=head2 auth_strength

Accessor for the C<auth_strength> parameter optionally passed to the
constructor.  If a value is provided, it will be passed to the constructor for
L<Authen::NZigovt::LogonStrength>.  If not provided, this parameter will
default to the URN for low strength logons.

=head2 as_url

Accessor for the URL to be used in the redirect.  The URL will be constructed
from the URL of the Identity Provider's single signon service and a query
string containing the SAML2 AuthnRequest message an optional relay state
parameter and a digital signature.

=head2 encoded_saml_request

Accessor for the XML SAML AuthnRequest message after deflate compression and
MIME Base64 encoding have been applied.

=head2 dump_request

This method is used by the C<< nzigovt dump-req >> command to decode and
decompress the SAMLRequest parameter from a generated URL.  It is provided as a
diagnostic aid.


=head1 SEE ALSO

See L<Authen::NZigovt> for documentation index.


=head1 LICENSE AND COPYRIGHT

Copyright (c) 2010-2011 the New Zealand Electoral Enrolment Centre

Written by Grant McLean E<lt>grant@catalyst.net.nzE<gt>

This program is free software; you can redistribute it and/or modify it
under the terms of either: the GNU General Public License as published
by the Free Software Foundation; or the Artistic License.

See http://dev.perl.org/licenses/ for more information.

=cut