Skip to content

ステップ毎の実装ガイド

この章では、認証処理を実行して、序章でダウンロードした簡単なショッピングサイトのプロジェクトに3DS2フローを組み込む方法を説明します。この章の説明は、同じ機能を既存の決済処理に追加する場合のガイドとしても使用できます。

このチュートリアルを通して、Intellij IDEAを使用します。

完成したコードについてさらに詳しく学習するには、/finalディレクトリにあるGPaymentsのサンプルコードの、3DS2が組み込まれた最終的な加盟店決済ページにアクセスしてください。デモ用の3DSリクエスターのコードの概要は、最終的なデモコードの章を参照してください。

Info

本書の目的は、決済ページがJava以外の別のプログラミング言語で実装されていても、決済ページに3DSリクエスターを実装できるように、3DSリクエスターの基本的な流れを理解するのを助けることにあります。

処理 1: 認証の初期化

まず、認証を初期化する手順、すなわち/brw/init/{messageCategory}を呼び出す手順を説明します。

messageCategorypa(決済認証)またはnpa(非決済認証)のいずれかです。

Tip

各APIのエンドポイントは認証APIというドキュメント内のAPIに対応する参照先にリンクしており、APIの使用方法に関する詳細情報にアクセスできます。

GPaymentsのサンプル・コードには、/initialというディレクトリが含まれています。自分のIDEを使用してこのディレクトリを開きます(File > Open > Browse to initial folder > OKと操作します)。

open idebrowse ide

また、認証処理の実装に役立つ3ds-web-adapter.jsも用意されています。

/finalプロジェクトから最初のプロジェクトの/resources/static/jsディレクトリに3ds-web-adapter.jsコピーし貼り付けます。必要に応じjsディレクトリを作成します。このファイルの場所がよく分からない場合は、ディレクトリ構想を確認してください。

次に示す強調表示の行を追加し、3ds-web-adapter.jsファイルをcheckout.htmlページにインポートします。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
....
<script src="https://code.jquery.com/jquery-3.3.1.min.js"
        integrity="sha256-FgpCb/KJQlLNfOu91ta32o/NMZxltwRo8QtmkMRdAu8="
        crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.3/umd/popper.min.js"
        integrity="sha384-ZMP7rVo3mIykV+2+9J3UJ46jBk0WLaUAdn689aCwoqbBJiSnjAK/l8WvCWPIPm49"
        crossorigin="anonymous"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/js/bootstrap.min.js"
        integrity="sha384-ChfqqxuZUCnJSK3+MXmPNIyE6ZbWh2IMqE241rYiqJxyMiZ6OW/JmZQ5stwEULTy"
        crossorigin="anonymous"></script>
<script src="/js/3ds-web-adapter.js"></script>
.....

アダプターを使用できるようcheckout.htmlを編集します。checkout.htmlcheckout()関数を確認してください。次のコードのようになっているはずです。この関数は、JQueryを使用してカード会員入力フィールドの値をHTML文書から取得し、新しい変数transIdcardHolderInfopurchaseInfoを初期化します。
 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
//checkout.html 
function checkout() {
    var transId = $('#transId').val();

    // NOTE: Some attributes are set to default values for demo purpose
    var cardHolderInfo = {};

    if($('#billAddrLine1').val()) {
        cardHolderInfo.billAddrLine1 = $('#billAddrLine1').val();
    }

    ....

    if($('#cardExpiry').val()) {
        purchaseInfo.expiryDate = $('#expiryDate').val();
    }
    //remove cardholder information class, checkout button and show spinner effect
    $("#checkoutButton").remove();

    $("#cardholderInfoCard").remove();
    $("#checkoutCard").removeClass("d-none");

    //move to result screen 2 seconds after for demo purposes
    setTimeout(function () {
        window.location.href = "/auth/result"
    }, 2000);
}

checkout.htmlcheckout()に、認証処理の開始時にthreeDSAuth()メソッド(ステップ.1)を呼び出すための強調表示の行を追加します。
1
2
3
4
5
6
7
8
9
function checkout() {
    ...
    $("#cardholderInfoCard").remove();
    $("#checkoutCard").removeClass("d-none");

    //move to result screen 2 seconds after
    setTimeout(function () { window.location.href = "/auth/result"}, 2000);
    threeDSAuth(transId, cardHolderInfo, purchaseInfo);
}
次に示す強調表示の行をコメントアウトまたは削除してください。この行はデモの目的でのみここに挿入されているもので、結果画面に移動する前に2秒の遅延時間を確保します。
1
2
3
4
5
6
7
8
function checkout() {
    ...
    $("#cardholderInfoCard").remove();
    $("#checkoutCard").removeClass("d-none");

    //setTimeout(function () { window.location.href = "/auth/result"}, 2000);
    threeDSAuth(transId, cardHolderInfo, purchaseInfo);
}

Note

次に示すコードは、3ds-web-adapter.jsで定義されているthreeDSAuth()関数です。このコードから分かるように、threeDSAuth()はJSON形式でページから/auth/initにオブジェクトを送信し(行6)、たとえばthreeDSRequestorTransIDtransIdにより初期化されています。
 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
//3ds-web-adapter.js
function threeDSAuth(transId, cardHolderInfo, purchaseInfo) {

    var postData = {...}

    ....

    console.log('init authentication');
    $.ajax({
        url: '/auth/init',
        type: 'POST',
        contentType: "application/json",
        data: JSON.stringify(postData),
        success: function (data) {
            console.log('init auth returns:', data);
            $('<iframe id="threeds-container" width="0" height="0" 
            style="visibility: hidden;" 
            src="' + data.threeDSServerCallbackUrl + '"></iframe>')
                .appendTo('.challengeContainer');
        },
        error: function () {
            alert('error');
        },
        dataType: 'json'

    });

}
ユーザーの必要に応じthreeDSAuth()を変更したり、cardholderInfopurchaseInfotransIdでなくユーザー独自のオブジェクトを転送できます。送信できるデータ要素の型は、最終的なコードの/dto/activeserverディレクトリの中のInitAuthRequestBRWに示されているほか、APIドキュメントにも示されています。

3ds-web-adapter.js/auth/initへのPOSTリクエストを行いますので(ステップ.2)、このリクエストを処理するための新しいコントローラークラスが必要です。

クライアントサイドからのリクエストを処理するためには、/auth/initリクエストを処理するコントローラーを作成する必要があります。/java/sample_requestorディレクトリ内に新しいコントローラークラスAuthController.java作成してください。さらに、/auth/initリクエストを処理し認証APIエンドポイント/api/v1/{messageCategory}を呼び出すinitAuthメソッドをコピーし貼り付けます。

 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
public class AuthController {
  @PostMapping("/auth/init")
  public InitAuthResponseBRW initAuth(@RequestBody InitAuthRequestBRW request) {
    String initBrwUrl = THREE_DS_SERVER_URL + "/api/v1/auth/brw/init/{messageCategory}";
    // 認証の初期化 by making  POST request to /brw/init/{messageCategory} (ステップ. 3)
    RequestEntity<InitAuthRequestBRW> req =
        new RequestEntity<>(request, HttpMethod.POST, URI.create(initBrwUrl));

    try {
      ResponseEntity<InitAuthResponseBRW> resp =
          restTemplate.exchange(req, InitAuthResponseBRW.class);

      InitAuthResponseBRW initRespBody = resp.getBody();
      logger.info("initAuthResponseBRW {}", initRespBody);

      // set InitAuthResponseBRW for future use
      transactionInfo.setInitAuthResponseBRW(initRespBody);
      return initRespBody;

    } catch (HttpClientErrorException | HttpServerErrorException ex) {

      logger.error("initAuthReq failed, {}, {}", ex.getStatusCode(), ex.getResponseBodyAsString());

      throw ex;
    }
  }
}

決済認証を初期化したい場合は{messageCategory}paに、非決済認証を初期化したい場合は{messageCategory}npa置換します。この例ではpaを使用しています。

1
2
3
4
5
6
7
//AuthController.java
String initBrwUrl = THREE_DS_SERVER_URL + "/api/v1/auth/brw/init/pa";
// 認証の初期化 by making  POST request to /brw/init/{messageCategory} (ステップ. 3)
InitAuthResponseBRW initAuthResponseBRW = restTemplate.postForObject(initBrwUrl, 
    request, InitAuthResponseBRW.class);

....

文字列THREE_DS_SERVER_URLを、デフォルト設定で与えられるURLに置換します。THREE_DS_SERVER_URLは、Settings > 3D Secure 2の中にあるAUTH_API_URLです。詳細は、ここを参照してください。

1
2
3
//AuthController.java
private final String THREE_DS_SERVER_URL = "https://api.as.testlab.3dsecure.cloud:7443";
....

文字列THREE_DS_REQUESTOR_URLは、デフォルト設定で与えられるURLに置換するか、ホスト対象の3DSリクエスターのURLに更新します。

1
2
3
//AuthController.java
private final String THREE_DS_REQUESTOR_URL = "http://localhost:8082";
....

activeserverディレクトリを/java/sample_requestor/dtoディレクトリにコピーし貼り付けます。このディレクトリには、API呼び出しを行うのに必要なすべてのクラスが含まれています。認証を初期化するために最も重要なのは、以下のクラスです。

次に、AuthController.javaの中にあるfillInitAuthRequestBRWメソッドについて説明します。このメソッドではInitAuthRequestBRWにデモ用のデフォルトデータを挿入していますが、実際の状況では、データベースからのデータ、あるいはcheckout.htmlから送信されたカード会員情報をロードするように、このメソッドの部分を入れ替えることができます。

 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
//AuthController.java
 /**
 * This method is to fill in the InitAuthRequestBRW with demo data, you need to fill the information from your database
 * @param initAuthRequestBRW
 * @param eventCallBackUrl
 */
private void fillInitAuthRequestBRW(InitAuthRequestBRW initAuthRequestBRW, String eventCallBackUrl) {

    initAuthRequestBRW.setAcctID("personal account");

    // Fill AcctInfo with default data.
    AcctInfo acctInfo = new AcctInfo();
    acctInfo.setChAccAgeInd("03");
    acctInfo.setChAccChange("20160712");
    acctInfo.setChAccChangeInd("04");
    acctInfo.setChAccDate("20140328");
    acctInfo.setChAccPwChange("20170328");
    acctInfo.setChAccPwChangeInd("02");
    acctInfo.setNbPurchaseAccount("11");
    acctInfo.setPaymentAccAge("20160917");
    acctInfo.setPaymentAccInd("02");
    acctInfo.setProvisionAttemptsDay("3");
    acctInfo.setShipAddressUsage("20160714");
    acctInfo.setShipAddressUsageInd("02");
    acctInfo.setShipNameIndicator("02");
    acctInfo.setSuspiciousAccActivity("02");
    acctInfo.setTxnActivityDay("1");
    acctInfo.setTxnActivityYear("21");
    initAuthRequestBRW.setAcctInfo(acctInfo);

    initAuthRequestBRW.setAcctType("03");
    initAuthRequestBRW.setAuthenticationInd("01");//01 = Payment transaction

    // fills ThreeDSRequestorAuthenticationInfo
    ThreeDSRequestorAuthenticationInfo threeDSRequestorAuthenticationInfo = new ThreeDSRequestorAuthenticationInfo();
    threeDSRequestorAuthenticationInfo.setThreeDSReqAuthData("login GP");
    threeDSRequestorAuthenticationInfo.setThreeDSReqAuthMethod("02");
    threeDSRequestorAuthenticationInfo.setThreeDSReqAuthTimestamp("201711071307");
    initAuthRequestBRW.setAuthenticationInfo(threeDSRequestorAuthenticationInfo);

    // fills MerchantRiskIndicator, optional but strongly recommended for the accuracy of risk based authentication
    MerchantRiskIndicator merchantRiskIndicator = new MerchantRiskIndicator();
    merchantRiskIndicator.setDeliveryEmailAddress("test@123.com");
    merchantRiskIndicator.setDeliveryTimeframe("02");
    merchantRiskIndicator.setGiftCardAmount("123");
    merchantRiskIndicator.setGiftCardCount("02");
    merchantRiskIndicator.setGiftCardCurr("840");
    merchantRiskIndicator.setPreOrderDate("20170519");
    merchantRiskIndicator.setPreOrderPurchaseInd("02");
    merchantRiskIndicator.setReorderItemsInd("01");
    merchantRiskIndicator.setShipIndicator("01");

    initAuthRequestBRW.setMerchantRiskIndicator(merchantRiskIndicator);

    /**
     * Options for threeDSRequestorChallengeInd - Indicates whether a challenge is requested for this transaction.
     * Values accepted:
     *  01 = No preference
     *  02 = No challenge requested
     *  03 = Challenge requested: 3DSリクエスター Preference
     *  04 = Challenge requested: Mandate
     *  05–79 = Reserved for EMVCo future use (values invalid until defined by EMVCo)
     *  80-99 = Reserved for DS use
     */
    initAuthRequestBRW.setChallengeInd("01");
    initAuthRequestBRW.setEventCallbackUrl(eventCallBackUrl); //Set this to your url
    initAuthRequestBRW.setMerchantId("123456789012345");
    initAuthRequestBRW.setPurchaseDate(LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss")));
    initAuthRequestBRW.setPurchaseInstalData("24");
    initAuthRequestBRW.setRecurringExpiry("20180131");
    initAuthRequestBRW.setRecurringFrequency("6");
    initAuthRequestBRW.setTransType("03");
}

MerchantTransaction.java、および対応するgettersetterメソッドに、新しい変数initAuthResponseBRW追加します。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
//MerchantTransaction.java
import com.gpayments.requestor.requestor.dto.activeserver.InitAuthResponseBRW;

public class MerchantTransaction {

    private InitAuthResponseBRW initAuthResponseBRW;

    ...

    //getter 
    public InitAuthResponseBRW getInitAuthResponseBRW() {
        return initAuthResponseBRW;
    }

    //setter
    public void setInitAuthResponseBRW(InitAuthResponseBRW initAuthResponseBRW) {
        this.initAuthResponseBRW = initAuthResponseBRW;
    }

ActiveServerへのAPI呼び出しを行うには、RESTTemplateにクライアント証明書を添付する必要があります。

GPaymentsから与えられたクライアント証明書(.p12ファイル)を/resources/certsディレクトリにコピーし貼り付けます。ディレクトリがない場合は/certsディレクトリを作成します。証明書ファイルをまだ受け取っていない場合は、お問合わせください。ActiveServerが社内ソフトウェアである場合は、管理者UIからクライアント証明書をダウンロードできます。クライアント証明書のダウンロードに関する詳細は、加盟店のセキュリティーを参照してください。

最終コードに含まれているcacerts.jksトラストストアファイルを/resources/certsディレクトリにコピーし貼り付けます。証明書が必要なのは、相互SSL認証を可能にするために3DSリクエスターと3DSサーバーの相互認証が必要だからです。

finalパッケージに含まれている設定クラスRestClientConfig.java/java/sample_requestor/configディレクトリにコピーし貼り付けます。次のクラスは、クライアント証明書をHttpClientにロードする方法の例です。このクラスは、GPaymentsから与えられたトラストストアを使用してユーザーがActiveServerからダウンロードしたキーストアファイルをロードします。

 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
//RestClientConfig.java
@Configuration
public class RestClientConfig {

  private static final String KEYSTORE_PASSWORD = "123456";
  private static final String CA_CERTS_FILE_NAME = "certs/cacerts.jks";
  private static final String CLIENT_CERTS_FILE_NAME = "certs/client_certificate.p12";

  @Bean
  public RestTemplate restTemplate()
      throws IOException, UnrecoverableKeyException, CertificateException, NoSuchAlgorithmException,
      KeyStoreException, KeyManagementException {
    SSLContext sslContext =
        SSLContextBuilder.create()
            .loadKeyMaterial(
                new ClassPathResource(CLIENT_CERTS_FILE_NAME).getURL(),
                KEYSTORE_PASSWORD.toCharArray(),
                KEYSTORE_PASSWORD.toCharArray())
            .loadTrustMaterial(
                new ClassPathResource(CA_CERTS_FILE_NAME).getURL(), KEYSTORE_PASSWORD.toCharArray())
            .build();

    CloseableHttpClient client =
        HttpClients.custom()
            .setSSLContext(sslContext)
            .build();
    HttpComponentsClientHttpRequestFactory httpRequestFactory =
        new HttpComponentsClientHttpRequestFactory(client);

    return new RestTemplate(httpRequestFactory);
  }
}
KEYSTORE_PASSWORDを、デフォルト設定で与えられるパスワードに置換します。ActiveServerが社内ソフトウェアである場合、このパスワードは、管理者ダッシュボードの加盟店ページからクライアント証明書をダウンロードするときに指定したパスワードです。クライアント証明書のダウンロードに関する詳細は、加盟店のセキュリティーを参照してください。

1
2
//RestClientConfig.java
private String KEYSTORE_PASSWORD = "123456";

Info

これは、クライアント証明書を使用してRESTFul APIリクエストを行う、Javaに固有な方法です。別の言語を使用して、3DSリクエスターが3DSサーバーと相互認証するためのサーバーサイドコードを実装する場合も、同様な手順に従う必要があります。

次のような依存関係をpom.xml追加し、SSLContextBuilderクラスに必要なApache HttpClientを取得します。
1
2
3
4
5
6
7
8
<dependencies>
    ...
    <dependency>
        <groupId>org.apache.httpcomponents</groupId>
        <artifactId>httpclient</artifactId>
        <version>4.5.6</version>
    </dependency>   
</dependencies>
必要なすべてのクラスがインポートされ、エラーがないことを確認してください。

もう一度アプリを実行してhttp://localhost:8082からアクセスし、決済処理を実行します。/api/v1/auth/brw/init/{messageCategory}に対応する正しい応答が得られ、次のようにログのthreeDSServerCallbackUrlが有効になっているはずです。

1
2
3
4
{
    "threeDSServerCallbackUrl":"https://admin.as.testlab.3dsecure.cloud/api/v1/auth/brw/callback?transId=cbc559c0-96bd-4078-be3c-ea0cc80686f0&t=ZIIgV2LqMak5ONQ1w3l3akfpZlMPftFlOE5Garat6lHiJ3T2Kq2vULuLhIQ7l5UKzpnNpztx",
    "threeDSServerTransID":"ade87add-b50c-42da-bbea-cdcbd85dcbf3"
}

3ds-notifyの部分で決済処理が停止しますが、これは、この部分がまだ実装されていないためであり、この部分については次の章で対応します。

Tip

変更内容がプロパゲートされるためには、プロジェクトを再実行する必要があります。CTRL+Cを使用して現在実行中のアプリを終了し、コマンドラインに次のように入力してプロジェクトを再実行します。

1
mvn spring-boot:run

3DSサーバーはコールバックURLを使用して、ACSは3DSメソッドを使用して、リスクに基づく認証に必要なブラウザー情報を収集します。

Note

/auth/initからの応答が正常に得られると(ステップ.5)3ds-web-adapter.jsは非表示のiframeを決済ページに埋め込み(ステップ.6)、ACSと3DSサーバーはthreeDSServerCallbackUrlを使用してブラウザー情報を収集できるようになります(ステップ.7)

1
2
3
4
5
//3ds-web-adapter.js
success: function(data) {
    $('<iframe id="threeds-container" width="0" height="0" style="visibility: hidden;" src="'+ data.threeDSServerCallbackUrl+'"></iframe>')
    .appendTo('.challengeContainer');
},

Info

シーケンス図のステップ.1ステップ.7は、この処理が終了すると実行されます。

処理 2: 認証の実行

前のステップで、3ds-web-adapter.jsは、コールバックsrcthreeDSServerCallbackUrlにセットされたiframeを挿入しています。このため、3DSサーバーはブラウザーへのコールバックを行って(ステップ.7)、必要なブラウザー情報を収集できます。

また、eventCallBackUrlTHREE_DS_REQUESTOR_URL/3ds-notifyにセットして/brw/init/{messageCategory}を呼び出しています(line.2)。これにより、3DSサーバーは、ブラウザー情報の収集が完了したときに通知を行えます。

1
2
3
//AuthController.java
fillInitAuthRequestBRW(request, THREE_DS_REQUESTOR_URL + "/3ds-notify");
.....

したがって、このAPI呼び出しを受信するためのハンドラーメソッドを作成しておく必要があります。

以下のコードをMainController.java追加します。 このハンドラーメソッドは、パラメーターtransIdcallbackType、およびオプションのparamを取り込みます。callbackTypeは、3DSMethodFinishedまたは3DSMethodSkippedのいずれかです。ハンドラーメソッドは文字列notify_3ds_eventsを返します。Springbootに慣れていない方向けに説明すると、これは、/resources/templatesディレクトリから名前の一致するHTMLページを返すことを意味します。

 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
// MainController.java
@PostMapping("/3ds-notify")
public String notifyResult(
        @RequestParam("requestorTransId") String transId,
        @RequestParam("event") String callbackType,
        @RequestParam(name = "param", required = false) String param,
        Model model) {

    String callbackName;
    // check the callbackType and initialise callbackName
    if ("3DSMethodFinished".equals(callbackType)) {

        callbackName = "_on3DSMethodFinished";

    } else if ("3DSMethodSkipped".equals(callbackType)) {

        callbackName = "_on3DSMethodSkipped";

    } else {
        throw new IllegalArgumentException("invalid callback type");
    }

    //Pass on the object to the page
    model.addAttribute("transId", transId);
    model.addAttribute("callbackName", callbackName);
    model.addAttribute("callbackParam", param);

    return "notify_3ds_events";
}

/3ds-notifyへの呼び出しにより返されたページであるcallbackFnを呼び出すためのnotify_3ds_events.htmlという新しいHTMLファイルを、/resources/templatesディレクトリ内に作成します。
 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
<!--notify_3ds_events.html-->
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8"/>
    <title>3DSecure 2.0 Authentication</title>
</head>
<body>

<form>
    <input type="hidden" id="notifyCallback" name="notifyCallback" data-th-value="${callbackName}"/>
    <input type="hidden" id="transId" name="transId" data-th-value="${transId}"/>
    <input type="hidden" id="param" name="param" data-th-value="${callbackParam}"/>
</form>

<script src="https://code.jquery.com/jquery-3.3.1.min.js"
        integrity="sha256-FgpCb/KJQlLNfOu91ta32o/NMZxltwRo8QtmkMRdAu8="
        crossorigin="anonymous"></script>
<script>

//notify parent checkout page to proceed with rest procedures.

var callbackFn = parent[$('#notifyCallback').val()];

//callbackFn is defined in 3ds-notify handler method
if (typeof callbackFn === 'function') {
    callbackFn($('#transId').val(),$('#param').val());
}

</script>

</body>
</html>
3DSサーバーから与えられるcallbackNameによって、3ds-web-adapter.jsで呼び出されるメソッドが異なることが分かります(ステップ.8)。 各メソッドについて以下に説明します。

  • _onThreeDSMethodFinished - 3DSメソッドが完了したことを通知し、ブラウザー情報の収集が完了したことを示す_doAuth()を呼び出します。
  • _onThreeDSMethodSkipped - 3DSメソッドをスキップしたことを通知し、ACSによるブラウザー情報の収集をスキップしたことを示す_doAuth()を呼び出します。

3ds-web-adapter_doAuth()は、/authへのPOSTリクエストを行い認証を実行します(ステップ.9)

このリクエストを処理する新しいハンドラーメソッドをAuthController.java追加します。 このメソッドは、threeDSRequestorTransIDthreeDSServerTransIDを使用して/brwへのPOST APIリクエストを行うはずです(ステップ.10)。続いて3DSサーバーは、AReqを作成、送信し、AResを受信するとこれを処理して3DS処理を開始します(ステップ.11)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
//AuthController.java
@PostMapping("/auth")
public AuthResponseBRW auth(@RequestParam("id") String transId) {

    MerchantTransaction transaction = transMgr.findTransaction(transId);

    //create authentication request.
    AuthRequestBRW authRequest = new AuthRequestBRW();
    authRequest.setThreeDSRequestorTransID(transaction.getId());
    authRequest.setThreeDSServerTransID(transaction.getInitAuthResponseBRW().getThreeDSServerTransID());

    String brwUrl = THREE_DS_SERVER_URL + "/api/v1/auth/brw";
    AuthResponseBRW response = restTemplate.postForObject(brwUrl, authRequest, AuthResponseBRW.class);

    logger.info("authResponseBRW {}", response);

    return response;
}

アプリを再実行し、デフォルト値を使用して決済処理を実行すると、/api/v1/auth/brwに対応する正しい応答が得られ(ステップ.12)、transStatusがYにセットされているはずです。

1
2
3
4
5
6
{
    "authenticationValue":"YXV0aCB2YWx1ZSB4eHh4eHh4eHg=",
    "eci":"06",
    "threeDSServerTransID":"52056514-7504-40c7-886f-2a0452d8edbd",
    "transStatus":"Y"
}

チャレンジシナリオをテストしたい場合は、ここのセクションを参照ください

/brwへのAPI呼び出しに対する応答では、challengeUrlがセットされ、transStatusCにセットされます。

1
2
3
4
5
6
7
{
    "acsChallengeMandated":"Y",
    "authenticationType":"01",
    "challengeUrl":"https://admin.as.testlab.3dsecure.cloud/api/v1/auth/brw/challenge/init?txid=78cd5068-96ea-48c4-b013-e6843fa8b2e4",
    "threeDSServerTransID":"c8a32fb7-9556-4242-896b-562ee8ca25df",
    "transStatus":"C",
}

チャレンジウィンドウで要求されるワンタイムパスコードは123456です。エラーが発生しますが、これはcallbackTypeAuthResultReadyの処理機能がまだ実装されていないためであり、この部分は次の章で実装します。

Note

transStatusCの場合、3ds-web-adapter.jsstartChallenge(url)を呼び出し、srcchallengeUrlにセットされたiframeをチャレンジウィンドウに挿入します(ステップ.14(C))
1
2
3
4
5
6
7
8
9
function startChallenge(url) {
//remove the spinner
$(".spinner").remove();

//create the iframe
$('<iframe src="'+url+'" class="h-100 w-100 border-0" id="challengeWindow" name="challengeWindow"></iframe>')
.appendTo('.challengeContainer');

}

iframeクラスがh-100およびw-100にセットされたことが分かります。これらはそれぞれ、height: 100%!importantおよびwidth: 100%!importantというcss型を実装するためのブートストラップ・デフォルト・クラスです。これが必要な理由は、ACSから与えられた内容に応じてiframeのサイズを調整する必要があるためです。チャレンジ画面は次のスクリーンショットのようになるはずです。

result screen

Info

実際の状況でACSが実行するのは、購入金額の確認だけで済むような簡単な処理ではなく、取得したカード会員情報に応じたリスクに基づく複雑な認証処理になります。同様に、認証メソッドはイシュアーにより決定され実行されます。

処理 3: 認証結果の取得

前のステップで説明したように、ここでカード会員に表示できるように認証結果の更新を要求する必要があります。フリクションレス・フローの場合も同様に更新を要求する必要があります(次の警告の項を参照)。

認証結果を別に取得しなければいけない理由

フリクションレス・フローの場合、利用可能な認証結果がステップ.12ですでに取得できているのに、なぜもう一度結果を要求する必要があるのか、不思議に思われるかもしれません。

これが必要なのは、ステップ.13で認証結果がページに転送されるためです。3ds-web-adapterが結果を3DSリクエスターに送り返すことも可能ですが、データが安全でないと見なされます。サーバー側では常に、元の情報源である3DSサーバーから戻される結果をサーバー自身の機能により取得できなければなりません。このステップで結果の受信を要求する必要があるのは、このためです。

チャレンジ・フローの場合は、3DSメソッドのステータスの通知の場合と同様に、認証結果は3ds-notifyのエンドポイントを介して3DSサーバーから通知されます。

MainController.javaで"AuthResultReady" callbackTypeを処理できるよう、次に示す強調表示の新しいelse ifステートメントを追加します。

1
2
3
4
5
6
7
8
//MainController.java
if ("3DSMethodFinished".equals(callbackType)) {
    callbackName = "_on3DSMethodFinished";
} else if ("3DSMethodSkipped".equals(callbackType)) {
    callbackName = "_on3DSMethodSkipped";
} else if ("AuthResultReady".equals(callbackType)) {
    callbackName = "_onAuthResult";
}

notify-3ds-eventsから_onAuthResultReadyが呼び出され、ウィンドウを/auth/result?txid=+transIdにリダイレクトします。 認証結果を取得したい場合は、/brw/resultを呼び出して、ハンドラー/auth/resultの中で結果の受信を要求できます。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
@GetMapping("/auth/result")
public String result(
        @RequestParam("txid") String transId,
        Model model) {

    MerchantTransaction transaction = transMgr.findTransaction(transId);
    String resultUrl = AuthController.THREE_DS_SERVER_URL + "/api/v1/auth/brw/result?threeDSServerTransID=" +
                    transaction.getInitAuthResponseBRW().getThreeDSServerTransID();
    AuthResponseBRW response = restTemplate.getForObject(resultUrl, AuthResponseBRW.class);
    model.addAttribute("result", response);
    return "result";
}
result.htmlの中の強調表示になっているコードをDelete or comment outします。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
<div class="col-sm-8">
    <div class="card">
        <div class="card-header">Transaction successful</div>
        <div class="card-body">
            <!--<h4 class="card-title text-success">Thank you for your purchase!</h4>-->
            <!--<p class="card-text">Want to continue shopping? Press "Back to Home".</p>-->

            <a href="/" class="btn btn-primary btn-block">Back to Home</a>

        </div>
    </div>
</div>

認証結果とその他の説明情報が表示されるよう、result.htmlの代わりに、次に示す強調表示のコードを追加します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
 <div class="col-sm-8">
     <div class="card">
         <div class="card-header">Transaction successful</div>
         <div class="card-body">
             <h4 class="card-title text-success">3D secure authentication was successful</h4>
             <p class="card-text">You may move onto authorization using the result below 
                  or you can press "Back To Home" to restart authentication process.</p>

             <dl class="row">
                 <dt class="col-sm-3">Trans Status</dt>
                 <dd class="col-sm-9" data-th-text="${result.transStatus}">Y</dd>
                 <dt class="col-sm-3">ECI</dt>
                 <dd class="col-sm-9" data-th-text="${result.eci}">eci value</dd>
                 <dt class="col-sm-3">CAVV</dt>
                 <dd class="col-sm-9" data-th-text="${result.authenticationValue}">cavv value</dd>
                 <dt class="col-sm-3">Other Info</dt>
                 <dd class="col-sm-9"></dd>
             </dl>

             <a href="/" class="btn btn-primary">Back To Home</a>
         </div>
     </div>
 </div>

Success

お疲れ様でした。ActiveServerが組み込まれた加盟店決済ページの作成作業は、これで完了です。 アプリをもう一度実行してみてください。取引ステータス、ECI、CAVVが含まれた結果ページが正しく表示されるはずです。

新しい結果画面は、次のスクリーンショットのようになります。 result screen

Tip

通常、この処理の後は、取引を完了するための、取引ステータス、ECI、CAVVを使用した信用認証の実行に進みます。

最終的なデモコード

最終的なデモ用コードには、/finalディレクトリにあるGPaymentsのサンプル・コードからアクセスできます。このコードは、追加設定を行わなくてもデフォルト設定のままでActiveServerを使用して動作しますので、操作は簡単で、このガイドに従ってアプリを実行するだけです。

コード解説

3DSリクエスターのデモ用コードには、以下のようにいくつかのエンドポイントとコンポーネントが実装されています。

  • /auth/init - このエンドポイントは3ds-web-adapter.jsから呼び出されて認証初期化リクエストを受信し、3DS2認証処理を開始します。このエンドポイントは、3DSリクエスター内でActiveServerの認証初期化APIである/api/v1/auth/brw/init/{messageCategory}を呼び出します。

  • /auth - このエンドポイントは3ds-web-adapter.jsから呼び出されて認証リクエストを受信し、ブラウザー情報の収集後に3DS2認証処理を実行し、オプションのその他の3DSメソッドを実行します。このエンドポイントは、3DSリクエスター内でActiveServerの認証APIである/api/v1/auth/brwを呼び出します。

  • /3ds-notify - 3DSメソッドの実行ステータス、認証結果、チャレンジ結果についてActiveServerがiframeを通して3DSリクエスターに通知できるようにします。

  • 3ds-web-adapter.js - 3DSリクエスター処理のスキャフォールディング用に作成された簡単なJavaScriptライブラリ。

  • com.gpayments.requestor.dto.activeserverパッケージ内のクラス - ActiveServer API DTO(データ転送オブジェクト)。

加盟店のwebサイトに組み込まれた3DSリクエスターのデモ用コードは、加盟店サイトが決済ページに組み込むための3ds-web-adapterを提供します。加盟店webサイトは、3ds-web-adapter.jsthreeDSAuth()を呼び出してカード会員情報をセットし、認証処理を開始します。また、加盟店webサイトは、3DSリクエスターが関連するコールバックURLと実行すべき3DSメソッドをセットできるようにするためのiframeも提供する必要があります。

認証処理が終わると、3DSリクエスターからイベントコールバックURL /3ds-notifyを使用して認証結果が送信されます。この時点で、決済代行会社または加盟店は認証処理に進むことができます。

ライトボックス・チャレンジ画面

チュートリアルではチャレンジ画面がインラインになっていましたが、webサイトのデザインによってはポップアップ内にチャレンジウィンドウを表示した方が良いかも知れません。 UI自体はACSに用意されていますので、この機能の実装について心配する必要はありません。

checkout.html開き、チャレンジウィンドウを定義しているコード部分を見つけてください。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
 <!--checkout.html-->
<div class="card d-none" id="checkoutCard">
    <div class="challengeContainer border">
        <div class="spinner row h-100 justify-content-center align-items-center">
            <div class="col">
                <div class="sk-fading-circle">
                    <div class="sk-circle1 sk-circle"></div>
                    <div class="sk-circle2 sk-circle"></div>
                    <div class="sk-circle3 sk-circle"></div>
                    <div class="sk-circle4 sk-circle"></div>
                    <div class="sk-circle5 sk-circle"></div>
                    <div class="sk-circle6 sk-circle"></div>
                    <div class="sk-circle7 sk-circle"></div>
                    <div class="sk-circle8 sk-circle"></div>
                    <div class="sk-circle9 sk-circle"></div>
                    <div class="sk-circle10 sk-circle"></div>
                    <div class="sk-circle11 sk-circle"></div>
                    <div class="sk-circle12 sk-circle"></div>
                </div>    
                <div class="text-center"><img class="w-25" src="images/visa-logo.png"/></div>
            </div>
        </div>
    </div>
</div>
行2と3をコメントアウトして、インライン・チャレンジ・カードを無効化します。
 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
 <!--checkout.html-->
<!--<div class="card d-none" id="checkoutCard">-->
    <!--<div class="challengeContainer border">-->
        <div class="spinner row h-100 justify-content-center align-items-center">
            <div class="col">
                <div class="sk-fading-circle">
                    <div class="sk-circle1 sk-circle"></div>
                    <div class="sk-circle2 sk-circle"></div>
                    <div class="sk-circle3 sk-circle"></div>
                    <div class="sk-circle4 sk-circle"></div>
                    <div class="sk-circle5 sk-circle"></div>
                    <div class="sk-circle6 sk-circle"></div>
                    <div class="sk-circle7 sk-circle"></div>
                    <div class="sk-circle8 sk-circle"></div>
                    <div class="sk-circle9 sk-circle"></div>
                    <div class="sk-circle10 sk-circle"></div>
                    <div class="sk-circle11 sk-circle"></div>
                    <div class="sk-circle12 sk-circle"></div>
                </div>    
                <div class="text-center"><img class="w-25" src="images/visa-logo.png"/></div>
            </div>
        </div>
    </div>
</div>
<!--Cardholder information -->
<div class="card" id="cardholderInfoCard">
4, 5, 6, 7, 8, 30 と31を追加し、モーダル内にチャレンジウィンドウを作成します。
 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
<!--checkout.html-->
<!--<div class="card d-none" id="checkoutCard">-->
<!--<div class="challengeContainer border">-->
<div class="modal fade" id="authBox" data-backdrop="static" data-keyboard="false" tabindex="-1"
             role="dialog" aria-labelledby="exampleModalLabel" aria-hidden="true">
    <div class="modal-dialog h-100 d-flex flex-column justify-content-center my-0" role="document">
        <div class="modal-content">
            <div class="modal-body challengeContainer">
                <div class="spinner row h-100 justify-content-center align-items-center">
                    <div class="col">
                        <div class="sk-fading-circle">
                            <div class="sk-circle1 sk-circle"></div>
                            <div class="sk-circle2 sk-circle"></div>
                            <div class="sk-circle3 sk-circle"></div>
                            <div class="sk-circle4 sk-circle"></div>
                            <div class="sk-circle5 sk-circle"></div>
                            <div class="sk-circle6 sk-circle"></div>
                            <div class="sk-circle7 sk-circle"></div>
                            <div class="sk-circle8 sk-circle"></div>
                            <div class="sk-circle9 sk-circle"></div>
                            <div class="sk-circle10 sk-circle"></div>
                            <div class="sk-circle11 sk-circle"></div>
                            <div class="sk-circle12 sk-circle"></div>
                        </div>    
                        <div class="text-center"><img class="w-25" src="images/visa-logo.png"/></div>
                    </div>
                </div>
            </div>
        </div>
    </div>
</div>
<!--Cardholder information -->
<div class="card" id="cardholderInfoCard">
次に、checkout.htmlの最後まで下にスクロールしてcheckout()関数の中の次のコードを探します。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
//checkout.html
function checkout() {
    ....


    //remove cardholder information class, checkout button and show spinner effect
    $("#checkoutButton").remove();    

    $("#cardholderInfoCard").remove();
    $("#checkoutCard").removeClass("d-none");

    threeDSAuth(transId, cardHolderInfo, purchaseInfo);
}
カード会員情報クラスを削除しカード内にスピナーを表示するため、行8と9コメントアウトします。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
//checkout.html
function checkout() {
    ....

    //remove cardholder information class, checkout button and show spinner effect
    $("#checkoutButton").remove();

    //$("#cardholderInfoCard").remove();
    //$("#checkoutCard").removeClass("d-none");

    threeDSAuth(transId, cardHolderInfo, purchaseInfo);
}

これに代わるポップアップ・チャレンジウィンドウ用のモーダルボックスを表示するため、行11を追加します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
//checkout.html
function checkout() {
    ....

    //remove cardholder information class, checkout button and show spinner effect
    $("#checkoutButton").remove();

    //$("#cardholderInfoCard").remove();
    //$("#checkoutCard").removeClass("d-none");

    $('#authBox').modal();

    threeDSAuth(transId, cardHolderInfo, purchaseInfo);
}
コードを再度実行すると、チャレンジ画面がポップアップウィンドウに変わったことが分かります。

challenge light box

デフォルト設定

最終的なデモ用コードでは、以下の値はデフォルト値に設定されています。この部分は、段階を追ったチュートリアルの実行中に参照できます。

Note

  • THREE_DS_SERVER_URLは、Settings > 3D Secure 2の中にあるAUTH_API_URLです。詳細は、ここを参照してください。
  • KEYSTORE_PASSWORDは、ActiveServer Administrationから証明書ファイル(.p12ファイル)をダウンロードするときに設定したパスワードです。証明書ファイルのダウンロードについては、ここを参照してください。

お問い合わせ

本書の内容についての質問は、弊社担当部門がお受けします。techsupport@gpayments.co.jpまでEメールにてお問い合わせください。