#################### main pod documentation begin ################### =head1 NAME Zymonic::Condition::PayerAuthentication - Zymonic Workflow Condition module =head1 SYNOPSIS Conditions on PayerAuthentication. =head1 DESCRIPTION Will check card number (retrieved via Zymonic::Decryptor) for payer authentication. Condition will pass when card is not enrolled, or when PARes has been sucessfully received after authentication. Condition will fail when card is enrolled, setting ACS URL and PAReq fields allowing the user to perform the authentication and try again with PARes. =head1 USAGE The following ClassOption Schema should be used: EncryptedCardNumber is the field to look in to get the encrypted card number to pass to the decryptor. ACS_URL, PAReq and Issuer_Cert are fields that will be set with the values a user should user to do the authentication. Will only be set if card is enrolled. PARes is the field to look in to get the user's sent PARes value. Device_Category should be the field which determines if user is using a mobile device. If user is using a mobile device, also need to pass back PAReq and Issuer_Cert. ECI, PARes_id and PARes_auth_code are fields that will be set after a successful PARes has been passed back by the user. Status is a field which will be updates with the current status of the authentication. Format is: "Y" the cardholder completed authentication correctly. "A" the attempted authentication was recorded. "U" a system error prevented authentication from completing. "N" the cardholder did not complete authentication "" the digital certificate was invalid. "Q" card not enrolled. (original P, but that is used for pending) "E" awaiting authentication. Format may be defined differently by the PayerAuthentication module being used. Can set in zz_system_options a value with name PayerAuthenticationModule and this will be used instead of this value. SkipPayerAuthentication, if Y, will skip payer auth. FailureUSerMessage is the error message that should be displayed to the user when payer authentication fails. If not set will default to: "Payer Authentication failed. Please try again or use a different payment method." =head1 BUGS NONE =head1 SUPPORT As in the license, Zymonic is provided without warranty or support unless purchased separately, however... If you email zymonic-support@zednax.com your issue will be noted and may receive a response. For security issues, please contact zymonic-security@zednax.com and someone will respond within 8 working hours. =head1 AUTHOR Alex Masidlover et al. CPAN ID: MODAUTHOR Zednax Limited alex.masidlover@zednax.com http://www.zednax.com =head1 COPYRIGHT This program is free software licensed under the... Alfresco Public License 1.0 The full text of the license can be found in the LICENSE file included with this module. Other licenses may be acceptable if including parts of Zymonic in larger projects, please contact Zednax for details. =head1 SEE ALSO perl(1). =cut #################### main pod documentation end ################### package Zymonic::Condition::PayerAuthentication; use strict; BEGIN { use Exporter (); use vars qw($VERSION @ISA @EXPORT @EXPORT_OK %EXPORT_TAGS); $VERSION = 'D1-r7507'; @ISA = qw(Exporter); #Give a hoot don't pollute, do not export more than needed by default @EXPORT = qw(); @EXPORT_OK = qw(); %EXPORT_TAGS = (); } use base "Zymonic::Condition"; use Zymonic::Decryptor::Client; use Zymonic::Table; use Zymonic::Utils qw(get_array debug deep_replace_card_numbers clean); use Data::Dumper; #################### subroutine header begin #################### =head2 satisfied Usage : $condition->satisfied; Purpose : Evaluates whether the condition is satsified. Returns : 'PASS' or 'FAIL'. Argument : nothing Throws : Zymonic::Exception::Condition Comment : Passes when not enrolled or when sucessful PARes. Fails otherwise, i.e. card has yet to be authentications. See Also : =cut #################### subroutine header end #################### sub satisfied { my $self = shift; my $precheck = shift || ''; # TODO: is this correct? to always pass on precheck if ($precheck) { return 'PASS'; } my $skip_payer_auth = $self->skip_payer_authentication(); if ( $skip_payer_auth && $skip_payer_auth eq 'Y' ) { return 'PASS'; } my $payer_authentication = $self->payer_authentication(); my $encrypted_card_number = $self->encrypted_card_number(); my $pares = $self->pares(); my $transaction_id = $self->transaction_id(); my $device_category = $self->device_category(); my $pareq; my $issuer_cert; if ( $device_category == 1 ) { $pareq = $self->pareq(); $issuer_cert = $self->issuer_cert(); } my $request = { messagetype => 'PayerAuthentication', payer_authentication_type => $payer_authentication, card_number => 'ENCRYPTED' . $encrypted_card_number . 'ENCRYPTED', ( $pares ? ( pares => $pares ) : () ), ( $issuer_cert ? ( issuer_cert => $issuer_cert ) : () ), ( $device_category ? ( device_category => $device_category ) : () ), ( $issuer_cert ? ( issuer_cert => $issuer_cert ) : () ), %{ $self->transaction_details($transaction_id) }, %{ $self->payment_data_details($encrypted_card_number) }, # send http headers http_accept => clean( $ENV{HTTP_ACCEPT}, ':/*;=.+,' ) || '', http_user_agent => clean( $ENV{HTTP_USER_AGENT}, ':/*.;_()-' ) || '', }; debug( 'Payer Authentication request: ' . Dumper( deep_replace_card_numbers($request) ) ); # call the decryptor to process card authentication my $response = $self->decryptor_client()->call_decryptor($request); debug( 'Payer Authentication response: ' . Dumper( deep_replace_card_numbers($response) ) ); Zymonic::Exception::Condition->throw( error => 'No response received from decryptor', zname => $self->{zname}, catchable => 'false' ) unless ref($response) && keys %{$response} > 0; # set the status field from the response $self->set_field_value( 'Status', $response->{status} ) if $response->{status}; # check for user error flag, flag on self and debug any error messages if ( $response->{user_error} ) { debug("Payer Authentication error: $response->{error}") if $response->{error}; $self->{user_error} = $self->static_or_field_value( 'FailureUserMessage', $self->{other_fields}, 'optional' ) || 'Payer Authentication failed. Please try again or use a different payment method.'; # record the error on session my $session = $self->ancestor('Zymonic::Session'); if ($session) { my $process_parent = $self->ancestor('Zymonic::Process'); $session->record_error( $process_parent->{zname} . "." . $self->{zname}, $self->{user_error} ) if $process_parent; } return 'FAIL'; } elsif ( $response->{error} ) { # any errors without user_error flag should through system errors Zymonic::Exception::Condition->throw( error => 'Error from decryptor: ' . $response->{error}, zname => $self->{zname}, catchable => 'false' ); } elsif ( $response->{pares} ) { # if pares, set pares fields and pass condition # check for pares results if ( defined $response->{ECI} && $response->{pares_id} && $response->{pares_auth_code} ) { $self->set_field_value( 'ECI', $response->{ECI} ); $self->set_field_value( 'PARes_auth_code', $response->{pares_auth_code} ); $self->set_field_value( 'PARes_id', $response->{pares_id} ); return 'PASS'; } else { # fail if no pares_id or auth_cose return 'FAIL'; } } elsif ( defined $response->{enrolled} ) { # if enrolled, set fields and fail (ie.e. wait for pares before passing) if ( $response->{enrolled} ) { $self->set_field_value( 'ACS_URL', $response->{acs_url} ); $self->set_field_value( 'PAReq', $response->{pareq} ); $self->set_field_value( 'Issuer_Cert', $response->{issuer_cert} ); return 'FAIL'; } else { # not enrolled, check for ECI and PASS $self->set_field_value( 'ECI', $response->{ECI} ) if defined $response->{ECI}; return 'PASS'; } } elsif ( $response->{allow_continue} && $response->{allow_continue} eq 'true' ) { # an error occurred but user should be allowed to continue with payment debug("Payer Authentication message: $response->{message}") if $response->{message}; return 'PASS'; } else { Zymonic::Exception::Condition->throw( error => 'Payer Authentication response should contain either pares or enrolled', zname => $self->{zname}, catchable => 'false' ); } } ################# subroutine header begin #################### =head2 payer_authentication Usage : $self->payer_authentication Purpose : Gets the payer_authentication value from the class options. Returns : a scalar payer_authentication value Argument : nothing Throws : nothing Comment : See Also : =cut #################### subroutine header end #################### sub payer_authentication { my $self = shift; # check for system option override my $pa = $self->{config}->sys_opt('PayerAuthenticationModule'); return ( $pa ? $pa : $self->static_or_field_value( 'PayerAuthentication', $self->{other_fields} ) ); } ################# subroutine header begin #################### =head2 encrypted_card_number Usage : $self->encrypted_card_number Purpose : Gets the encrypted_card_number value from the class options. Returns : a scalar encrypted_card_number value Argument : nothing Throws : nothing Comment : See Also : =cut #################### subroutine header end #################### sub encrypted_card_number { my $self = shift; return $self->{encrypted_card_number_field}->value if $self->{encrypted_card_number_field}; # other fields already set prior to calling my $ecn_ref = $self->{other_fields}->{ $self->{xmldef}->{ClassOptions}->{EncryptedCardNumber}->{Field}->{ZName}->{content} }; Zymonic::Exception::Condition->throw( error => "Unable to find EncryptedCardNumber field: " . $self->{xmldef}->{ClassOptions}->{EncryptedCardNumber}->{Field}->{ZName}->{content}, zname => $self->{zname}, catchable => 'false' ) unless $ecn_ref; $self->{encrypted_card_number_field} = $ecn_ref->{parent}->get_object($ecn_ref); return $self->{encrypted_card_number_field}->value; } ################# subroutine header begin #################### =head2 pares Usage : $self->pares Purpose : Gets the pares value from the class options. Returns : a scalar pares value Argument : nothing Throws : nothing Comment : See Also : =cut #################### subroutine header end #################### sub pares { my $self = shift; return $self->{pares_field}->value if $self->{pares_field}; my $p_ref = $self->{other_fields}->{ $self->{xmldef}->{ClassOptions}->{PARes}->{Field}->{ZName}->{content} }; return '' unless $p_ref; $self->{pares_field} = $p_ref->{parent}->get_object($p_ref); return $self->{pares_field}->value || ''; } #################### subroutine header begin #################### =head2 decyptor_client Usage : $self->decryptor_client; Purpose : Returns a handle to a decryptor client Returns : Zymonic::Decryptor::Client object Argument : nothing Throws : nothing Comment : See Also : =cut #################### subroutine header end #################### sub decryptor_client { my $self = shift; $self->{decryptor_client} = Zymonic::Decryptor::Client->new( parent => $self, config => $self->{config}, db => $self->{DB} ) unless defined( $self->{decryptor_client} ); return $self->{decryptor_client}; } #################### subroutine header begin #################### =head2 set_field_value Usage : $self->set_status($field, $value); Purpose : If $field is set in ClassOptions, will set field to incoming value. Returns : nothing Argument : The value to set Throws : Zymonic::Exception::Field Comment : See Also : =cut #################### subroutine header end #################### sub set_field_value { my $self = shift; my $field = shift; my $value = shift; return unless $field && ref( $self->{xmldef}->{ClassOptions}->{$field} ) && ref( $self->{xmldef}->{ClassOptions}->{$field}->{Field} ) && ref( $self->{xmldef}->{ClassOptions}->{$field}->{Field}->{ZName} ) && $self->{xmldef}->{ClassOptions}->{$field}->{Field}->{ZName}->{content} && defined $value; # lookup the field if not already done so unless ( $self->{"ZZvf_$field"} ) { my $lookup_zname = $self->{xmldef}->{ClassOptions}->{$field}->{Field}->{ZName}->{content}; my $field_def = $self->{other_fields}->{$lookup_zname}; my $found_field = $field_def->{parent}->get_object($field_def) if $field_def; Zymonic::Exception::Field->throw( error => "Unable to find specified $field Field: $lookup_zname", zname => $self->{zname}, catchable => 'false' ) unless $found_field; $self->{"ZZvf_$field"} = $found_field; } # set field with encrypted pan $self->{"ZZvf_$field"}->value($value); } ################# subroutine header begin #################### =head2 skip_payer_authentication Usage : $self->skip_payer_authentication Purpose : Gets the skip_payer_authentication value from the class options. Returns : a scalar skip_payer_authentication value Argument : nothing Throws : nothing Comment : See Also : =cut #################### subroutine header end #################### sub skip_payer_authentication { my $self = shift; return $self->static_or_field_value( 'SkipPayerAuthentication', $self->{other_fields}, 'optional' ); } #################### subroutine header begin #################### =head2 transaction_details Usage : $self->transaction_details($id) Purpose : Returns the settings from form for the given transaction id. Returns : A hashref of settings for the specified transaction. Argument : A transaction id Throws : Zymonic::Exception::Condition Comment : See Also : =cut #################### subroutine header end #################### sub transaction_details { my $self = shift; my $id = shift; Zymonic::Exception::Condition->throw( zname => $self->{zname}, error => 'Need Transaction ID to lookup transaction', catchable => 'true' ) unless $id; # check in cache return $self->{transaction_details}->{$id} if $self->{transaction_details}->{$id}; # transaction not in db yet, so get form values (from other_fields) # map into simple hash and add to cache $self->{transaction_details}->{$id} = { map { $_->value( $_->raw_value, $_->{fromdb} ) unless defined $_->value; $_->{field} => $_->value } map { $_->{parent}->get_object($_) } values %{ $self->{other_fields} } }; # get MID details my $mid_table = Zymonic::Table->new( parent => $self, zname => 'pte_mids', config => $self->{config}, auth => $self->{auth}, DB => $self->{DB}, ); $mid_table->get_all_records( { where_clause => $mid_table->{tableasname} . '.mid = ?', where_params => [ $self->{transaction_details}->{$id}->{mid} ] } ); # set them on transaction map { $_->value( $_->raw_value, $_->{fromdb} ) unless defined $_->value; $self->{transaction_details}->{$id}->{ $_->{field} } = $_->value unless defined $self->{transaction_details}->{$id}->{ $_->{field} } } map { $mid_table->get_object($_) } values( %{ $mid_table->{records}->[0]->{fields} } ); # if currency not set on transaction get it from MID unless ( $self->{transaction_details}->{$id}->{currency} ) { my $crf_ref = $mid_table->{records}->[0]->{fields}->{pte_mids_currency}; my $crf_field = $mid_table->get_object($crf_ref) if $crf_ref; if ($crf_field) { $crf_field->value( $crf_field->raw_value, $crf_field->{fromdb} ) unless defined $crf_field->value; $self->{transaction_details}->{$id}->{currency} = $crf_field->value; } } # set transaction_id $self->{transaction_details}->{$id}->{transaction_id} = $id; return $self->{transaction_details}->{$id}; } #################### subroutine header begin #################### =head2 payment_data_details Usage : $self->payment_data_details($token) Purpose : Returns the settings from DB for the given payment_data token. Returns : A hashref of settings for the specified payment_data. Argument : A payment_data token Throws : Zymonic::Exception::Condition Comment : See Also : =cut #################### subroutine header end #################### sub payment_data_details { my $self = shift; my $token = shift; Zymonic::Exception::Condition->throw( zname => $self->{zname}, error => 'Need token to lookup Payment Data', catchable => 'true' ) unless $token; # check in cache return $self->{payment_data_details}->{$token} if $self->{payment_data_details}->{$token}; # load payment data table my $payment_data_table = Zymonic::Table->new( parent => $self, zname => 'pte_cards', config => $self->{config}, auth => $self->{auth}, DB => $self->{DB}, filter => { where => 'token = ?', params => [$token] } ); $payment_data_table->get_all_records(); # map into simple hash and add to cache $self->{payment_data_details}->{$token} = { map { $_->value( $_->raw_value, $_->{fromdb} ) unless defined $_->value; $_->{field} => $_->value } map { $payment_data_table->get_object($_) } values( %{ $payment_data_table->{records}->[0]->{fields} } ) }; # set payment_data_id $self->{payment_data_details}->{$token}->{payment_data_id} = $self->{payment_data_details}->{$token}->{id}; # remove card_number field as it is not in the DB delete $self->{payment_data_details}->{$token}->{card_number}; return $self->{payment_data_details}->{$token}; } ################# subroutine header begin #################### =head2 pareq Usage : $self->pareq Purpose : Gets the pareq value from the class options. Returns : a scalar pareq value Argument : nothing Throws : nothing Comment : See Also : =cut #################### subroutine header end #################### sub pareq { my $self = shift; return $self->{pareq_field}->value if $self->{pareq_field}; my $p_ref = $self->{other_fields}->{ $self->{xmldef}->{ClassOptions}->{PAReq}->{Field}->{ZName}->{content} }; return '' unless $p_ref; $self->{pareq_field} = $p_ref->{parent}->get_object($p_ref); return $self->{pareq_field}->value || ''; } ################# subroutine header begin #################### =head2 issuer_cert Usage : $self->issuer_cert Purpose : Gets the issuer_cert value from the class options. Returns : a scalar issuer_cert value Argument : nothing Throws : nothing Comment : See Also : =cut #################### subroutine header end #################### sub issuer_cert { my $self = shift; return $self->{issuer_cert_field}->value if $self->{issuer_cert_field}; my $p_ref = $self->{other_fields}->{ $self->{xmldef}->{ClassOptions}->{issuer_cert}->{Field}->{ZName}->{content} }; return '' unless $p_ref; $self->{issuer_cert_field} = $p_ref->{parent}->get_object($p_ref); return $self->{issuer_cert_field}->value || ''; } ################# subroutine header begin #################### =head2 device_category Usage : $self->device_category Purpose : Gets the device_category value from the class options. Returns : a scalar device_category value Argument : nothing Throws : nothing Comment : See Also : =cut #################### subroutine header end #################### sub device_category { my $self = shift; return $self->{device_category_field}->value if $self->{device_category_field}; my $p_ref = $self->{other_fields}->{ $self->{xmldef}->{ClassOptions}->{device_category}->{Field}->{ZName}->{content} }; return '' unless $p_ref; $self->{device_category_field} = $p_ref->{parent}->get_object($p_ref); return $self->{device_category_field}->value || ''; } #################### subroutine header begin #################### =head2 manual Usage : $self->manual($manual) Purpose : Outputs an XML manual for this object Returns : A hashref to be converted to XML using XML::Simple. Argument : A Zymonic::Manual object $schemas - a hashref containing input/output schemas, if present then schema fragments will be added to the manual Throws : nothing Comment : See Also : =cut #################### subroutine header end #################### sub manual { my $self = shift; my $manual = shift; my $schemas = shift; # build any SUPER manual stuff $self->SUPER::manual( $manual, $schemas ); # add potential error if ( my $parent_process = $self->ancestor('Zymonic::Process') ) { $manual->add_error( { code => { content => $parent_process->{zname} . "." . $self->{zname}, }, description => { content => $self->static_or_field_value( 'FailureUserMessage', $self->{other_fields}, 'optional' ) || 'Payer Authentication failed. Please try again or use a different payment method.' }, } ); } } 1;