3-D Secure

Prepare all the data required to perform the transaction as follows.
As you can see, in case of a card transaction there are three sets of information: sale, customer and card data.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
$card_params = array(
    'sale' => array(
        'amount'      => 100.00,
        'currency'    => 'EUR',
        'description' => 'Product #1'
    ),
    'customer' => array(
        'name'    => 'John Doe',
        'email'   => 'john@doe.com',
        'ip'      => '127.0.0.1',
        'address' => array (
            'street_house' => '1600 Pennsylvania Avenue Northwest',
            'city'         => 'Washington',
            'state'        => 'DC',
            'zip'          => '500',
            'country_code' => 'US',
        ),
    ),
    'card' => array(
        'token' => '12a34b45c67d89e00f1aa2bb3cc4dd5ee6ff12a34b45c67d89e00f1aa2bb3cc4',
     ),
     'back_url' => 'http://example.com/3dsecure', // 3d secure back redirect url
);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
card_params = {
    'sale' => {
        'amount'      => 19.99,
        'currency'    => 'EUR',
        'description' => 'Product #1',
    },
    'customer' => {
        'name'    => 'John Doe',
        'email'   => 'john@doe.com',
        'ip'      => '127.0.0.1',
        'address' => {
            'street_house' => '1600 Pennsylvania Avenue Northwest',
            'city'         => 'Washington',
            'state'        => 'DC',
            'zip'          => '500',
            'country_code' => 'US'
        }
    },
    'card' => {
        'token' => '12a34b45c67d89e00f1aa2bb3cc4dd5ee6ff12a34b45c67d89e00f1aa2bb3cc4'
    },
    'back_url' => 'http://example-url.com'
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
card_params = {
  'sale' : {
    'amount'      : 19.99,
    'currency'    : 'EUR',
    'description' : 'Product #1'
  },
  'customer' : {
    'name'    : 'John Doe',
    'email'   : 'john@doe.com',
    'ip'      : '127.0.0.1',
    'address' : {
      'street_house' : '1600 Pennsylvania Avenue Northwest',
      'city'         : 'Washington',
      'state'        : 'DC',
      'zip'          : '500',
      'country_code' : 'US'
    }
  },
  'card' : {
    'token' : '12a34b45c67d89e00f1aa2bb3cc4dd5ee6ff12a34b45c67d89e00f1aa2bb3cc4'
  },
  'back_url' : 'http://example.com/3dsecure'
}
1
2
3
4
Sale sale = new Sale(19.99, "EUR", "Product #1");
Address address = new Address("1600 Pennsylvania Avenue Northwest", "Washington", "DC", "500", "US");
Customer customer = new Customer("John Doe", "john@doe.com", "127.0.0.1", address);
Card card = new Card("4111111111111111", "03", "2017", "John Doe", "123")

Check enrollment

To check whether a card is enrolled in the 3-D Secure program, simply call the checkCard3DSecureByTokenByToken method. If the card is enrolled, you will also get an id_3dsecure_auth number.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
try {
    $status = $client->checkCard3DSecureByToken($card_params);
}
catch (Exception $e) {
    // Handle exception here, for example show an error page, stop action
}

if ($client->isSuccess()) {
    echo "Success, id_3dsecure_auth: {$status['id_3dsecure_auth']} \n";
} else {
    echo "Error ID: {$status['error']['id_error']}, \n".
         "Error number: {$status['error']['error_number']}, \n".
         "Error description: {$status['error']['error_description']}";
}

if (true == $status['is_card_enrolled'])
{
    // redirect to 3-D Secure provider
    header('Location: ' . $status['redirect_url']);
    die;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
begin
    status = client.check_card_3d_secure_by_token(card_params)
rescue PayLane::ClientError => e
    # handle exceptions here
end

if client.success?
    puts "Success, id_3dsecure_auth: #{status["id_3dsecure_auth"]}"
else
    puts "Error ID: #{status["error"]["id_error"]}, \n"\
         "Error number: #{status["error"]["error_number"]}, \n"\
         "Error description: #{status["error"]["error_description"]}"
end

if status['is_card_enrolled']
    # redirect to url in status['redirect_url']
    exit
end
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
try:
    status = client.check_card_3d_secure_by_token(card_params)
except Exception, e:
    # handle exceptions here

if client.is_success():
    print 'Success, id_3dsecure_auth: %s' % status['id_3dsecure_auth']
else:
    print 'Error (%s), %s - %s' % (status['error'].get('id_error'),
                                   status['error'].get('error_number'),
                                   status['error'].get('error_description'))

if status['is_card_enrolled']:
    # redirect to url in status['redirect_url']
    sys.exit()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
api.secure3DCheckCard(sale, customer, card, "http:// example.com/3dsecure", new Callback<Secure3DSaleResult>() {

@Override
public void onFinish(Secure3DSaleResult result) {

    if (result.isEnrolled()) {
        WebView webview =...;
        webview.loadUrl(result.getRedirectUrl());
    }
}

    @HandleException
    public void onProtocolError(ProtocolException e) {
        // invoke if not success
        // e.getCode() - error code
        // e.getMessage() - error message
    }

    @Override
    public void onError(Exception e) {
        // connection error etc.
    }
});
Ruby note:
There is no native function in Ruby to redirect to another website – you either have to use a mechanism provided by the used framework or write a function that will suit you best.
Python note:
There is no native function in Python to redirect to another website – you either have to use a mechanism provided by the used framework or write a function that will suit you best.

For Django, you can use:
1
2
from django.http import HttpResponseRedirect
HttpResponseRedirect(status['redirect_url'])
For Pylons, you can use:
1
2
from pylons.controllers.util import redirect
redirect(status['redirect_url'])

If the 3-D Secure request was performed successfully, you may now proceed with the actual transaction and redirect the customer to the 3-D Secure provider’s website (use the received redirect_url).

If the transaction was rejected and you received an error, process with a regular single card payment. You should also save the received error information in your system.

Transaction

After providing the required information, the customer will be redirected back to your website (back_url). You should now verify the returned information to avoid any fraud attempts and check the transaction’s status.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
$salt        = 'YOUR_HASH_SALT';
$status      = $_GET['status'];
$description = $_GET['description'];
$amount      = $_GET['amount'];
$currency    = $_GET['currency'];
$hash        = $_GET['hash'];

$id = '';
if ($status !== 'ERROR') // success, get id_3dsecure_auth
    $id = $_GET['id_3dsecure_auth'];
   
$calc_hash = sha1("{$salt}|{$status}|{$description}|{$amount}|{$currency}|{$id}");

// check hash salt
if ( $calc_hash !== $hash ) {
    die ("Error, wrong hash");
}

// check transaction status
if ($status === 'ERROR') {
    die("Error, 3-D auth transaction declined");
} else {
    // 3-D Secure authorization completed, perform sale
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
# Simple controller action code in Rails
# it's just an example - most of the logic should be moved to model

salt        = 'YOUR_HASH_SALT'
status      = params['status']
description = params['description']
amount      = params['amount']
currency    = params['currency']
hash        = params['hash']

id = ''
unless status == 'ERROR'
    id = params['id_3dsecure_auth']
else
# redirect to an index action to correct the payment + simple notice
# for Rails: redirect_to :index, :notice => "Error, 3-D auth transaction declined"
end

calc_hash = Digest::SHA1.hexdigest("#{salt}|#{status}|#{description}|#{amount}|#{currency}|#{id}")

unless calc_hash == hash
# redirect to an index action to correct the payment
# for Rails: redirect_to :index, :notice => "Wrong hash"
end


# check transaction status

if status == 'ERROR'
# redirect to an index action to correct the payment + simple notice
# for Rails: redirect_to :index, :notice => "Error, 3-D auth transaction declined"
else
# 3-D Secure authorization completed, perform sale
end
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
salt             = 'YOUR_HASH_SALT'
status           = get_request_param('status')
description      = get_request_param('description')
amount           = get_request_param('amount')
currency         = get_request_param('currency')
hash             = get_request_param('hash')
id_3dsecure_auth = None

# success, get id_3dsecure_auth
if status != 'ERROR':
    id_3dsecure_auth = get_request_param('id_3dsecure_auth')

calc_hash = hashlib.sha1(
    '|'.join([salt, status, description, amount, currency, id_3dsecure_auth])).hexdigest()

# check hash salt
if calc_hash != hash:
    sys.exit('Error, wrong hash')

# check transaction status
if status == 'ERROR':
    sys.exit('Error, 3-D auth transaction declined')
else:
    # '3-D Secure authorization completed, perform sale'
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
webview.setWebViewClient(new WebViewClient() {
    @Override
    public boolean shouldOverrideUrlLoading(WebView view, String url) {

        if (url.contains(redirectUrl)) {
            try {
                Map<String, String> map = getQueryMap(new URL(url).getQuery());
                String salt = "YOUR_HASH_SALT";
                String status = map.get("status");
                String description = map.get("description");
                String amount = map.get("amount");
                String currency = map.get("currency");
                String hash = map.get("hash");
                String id = map.get("id_3dsecure_auth");

                String calcHash = sha1(String.format("%1$s|%2$s|%3$s|%4$s|%5$s|%6$s", salt, status, description, amount, currency, id));

                //  check hash salt
                if (!calcHash.equals(hash)) {
                    // Error, wrong hash
                }

                if (status.equals("ERROR")) {
                    String errorDescription=map.get("error_description");
                    // Error, transaction declined

                } else {
                    String idSale=map.get("id_sale");
                    // Success, transaction completed
                }

            } catch (MalformedURLException e) {
                e.printStackTrace();
            } catch (NoSuchAlgorithmException e) {
                e.printStackTrace();
            } catch (UnsupportedEncodingException e) {
                e.printStackTrace();
            }
        } else {
            view.loadUrl(url);
        }
        return true;
    }
});

public static Map<String, String> getQueryMap(String query) {
    String[] params = query.split("&");
    Map<String, String> map = new HashMap<String, String>();
    for (String param : params) {
        String name = param.split("=")[0];
        String value = param.split("=")[1];
        map.put(name, value);
    }
    return map;
}

private static String convertToHex(byte[] data) {
    StringBuilder buf = new StringBuilder();
    for (byte b : data) {
        int halfbyte = (b >>> 4) & 0x0F;
        int two_halfs = 0;
        do {
            buf.append((0 <= halfbyte) && (halfbyte <= 9) ? (char) ('0' + halfbyte) : (char) ('a' + (halfbyte - 10)));
            halfbyte = b & 0x0F;
        } while (two_halfs++ < 1);
    }
    return buf.toString();
}

public static String sha1(String text) throws NoSuchAlgorithmException, UnsupportedEncodingException {
    MessageDigest md = MessageDigest.getInstance("SHA-1");
    md.update(text.getBytes("utf-8"), 0, text.length());
    byte[] sha1hash = md.digest();
    return convertToHex(sha1hash);
}
Python note:
The get_request_param function is supposed to collect data from GET params. Depending on your framework or any other toolkit, please use a proper function or write one that suits you best.

For Django, you can use:
1
param_from_get = request.GET.get('param_name')
For Pylons, you can use:
1
2
from pylons import request
param_from_get = request.GET.get('param_name')

If everything went fine, you may now perform the actual payment based on the 3-D Secure authorization.

You can check whether the transaction was performed successfully by calling the isSuccess method.
Retrieving the transaction ID number (or error details, if anything goes wrong) is also very simple and can be done as shown below.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
try {
    $status = $client->saleBy3DSecureAuthorization(array ('id_3dsecure_auth' => $id));
} catch (Exception $e) {
    // Handle exception here, for example show an error page, stop action
}

if ($client->isSuccess())
{
    echo "Success, id_sale: {$status['id_sale']} \n";
} else {
    echo "Error ID: {$status['error']['id_error']}, \n".
         "Error number: {$status['error']['error_number']}, \n".
         "Error description: {$status['error']['error_description']}");
}
1
2
3
4
5
6
7
8
9
10
11
12
13
begin
    status = client.sale_by_3d_secure_authorization({"id_3dsecure_auth" => id})
rescue PayLane::ClientError => e
    # handle exceptions here
end

if client.success?
    puts "Success, id_sale: #{status["id_sale"]}"
else
    puts "Error ID: #{status["error"]["id_error"]}, \n"\
         "Error number: #{status["error"]["error_number"]}, \n"\
         "Error description: #{status["error"]["error_description"]}"
end
1
2
3
4
5
6
7
8
9
10
11
12
try:
    status = client.sale_by_3d_secure_authorization(
        {'id_3dsecure_auth': id_3dsecure_auth})
except Exception, e:
    # handle exceptions here

if client.is_success():
    print 'Success, id_sale: %s' % status['id_sale']
else:
    print 'Error (%s), %s - %s' % (status['error'].get('id_error'),
                                   status['error'].get('error_number'),
                                   status['error'].get('error_description'))
1
2
3
4
5
6
7
8
9
Secure3DSaleResult result =...;
long id3dSecureAuth = result.getId3dSecureAuth();
api.secure3DAuthSale(id3dSecureAuth, new Callback<CardSaleResult>() {

    @Override
    public void onFinish(CardSaleResult result) {
        // success
    }
});

Card not enrolled

The situation presented above is an ideal flow. But it may happen that a card is not enrolled in the 3-D Secure program. Here’s how to deal with that.

After the 3-D authorization was performed, make sure that the card is enrolled and simply call the saleBy3DSecureAuthorization method like you would normally. It is important that you do this (instead of performing a regular sale) for security reasons – this way the whole 3-D Secure attempt will be recorded.

You can check whether the transaction was performed successfully by calling the isSuccess method.
Retrieving the transaction ID number (or error details, if anything goes wrong) is also very simple and can be done as shown below.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
$id_3dsecure_auth = $status['id_3dsecure_auth'];

if (true != $status['is_card_enrolled'])
{
    try {
        $status = $client->saleBy3DSecureAuthorization(array ('id_3dsecure_auth' => $id_3dsecure_auth));
    } catch (Exception $e) {
        // Handle exception here, for example show an error page, stop action
    }
}

if ($client->isSuccess()) {
    echo "Success, id_sale: {$status['id_sale']} \n";
} else {
    echo "Error ID: {$status['error']['id_error']}, \n".
         "Error number: {$status['error']['error_number']}, \n".
         "Error description: {$status['error']['error_description']}");
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
id_3dsecure_auth = status['id_3dsecure_auth']

unless status['is_card_enrolled']
    begin
        status = client.sale_by_3d_secure_authorization({"id_3dsecure_auth" => id_3dsecure_auth})
    rescue PayLane::ClientError => e
        # Handle exception here, for example show an error page, stop action
    end
end

if client.success?
    puts "Success, id_sale: #{status["id_sale"]}"
else
    puts "Error ID: #{status["error"]["id_error"]}, \n"\
         "Error number: #{status["error"]["error_number"]}, \n"\
         "Error description: #{status["error"]["error_description"]}"
end
1
2
3
4
5
6
7
8
9
10
11
12
13
if not is_card_enrolled:
    try:
        status = client.sale_by_3d_secure_authorization(
            {'id_3dsecure_auth': id_3dsecure_auth})
    except Exception, e:
        # Handle exception here, for example show an error page, stop action

if client.is_success():
    print 'Success, id_sale: %s' % status['id_sale']
else:
    print 'Error (%s), %s - %s' % (status['error'].get('id_error'),
                                   status['error'].get('error_number'),
                                   status['error'].get('error_description'))
1
2
3
4
5
6
7
8
9
10
Secure3DSaleResult result =...;
if (!result.isEnrolled()) {
    api.secure3DAuthSale(result.getId3dSecureAuth(), new Callback<CardSaleResult>() {

        @Override
        public void onFinish(CardSaleResult result) {
            // success
        }
    });
}