Skip to content

Back-end implementation

In this section, we will show the implementation details for the back-end side of the merchant application using our sample code.

For the back-end, we need to implement a 3DS Requestor. The 3DS Requestor receives information from the 3ds-web-adapter and sends the requests to ActiveServer. It also receives the authentication results from ActiveServer and forwards the results to the 3ds-web-adapter.

The demo 3DS Requestor code provides the backend implementation with the following server side languages:

  • Java: The java version is implemented based on the Springboot framework. For details of Springboot, please check out https://spring.io/projects/spring-boot
  • C#: The C# version is implemented based on ASP.net.
  • PHP: The PHP version is implemented based on PHP 7.2 with cURL (Client URL Library).
  • Go: The Go version is implemented based on Go 1.12 with Go Module support. All dependencies are listed in go.mod file.

Why we need to implement a backend?

As defined by EMVCo's 3DSecure 2.0 specification, when the 3DS Server and the 3DS Requestor environment is separated, the communication between these two components must be mutual authenticated:

[Req 300] If the 3DS Requestor and 3DS Server are separate components, ensure that data transferred between the components is protected at a level that satisfies Payment System security requirements with mutual authentication of both servers.

Before starting the authentication process with ActiveServer, the 3DS Requestor needs to establish a mutual TLS connection with ActiveServer. Make sure you have the client certificate and configure it with the 3DS Requestor. If not, follow the Get client certificate and Configure 3DS Requestor details instructions found on the Introduction page.

Tip

The implementation of TLS configuration for the HTTP Client can be found as follows:

  • Java: The TLS configuration and client certificate loading can be found in class RestClientConfig.
  • C#: The TLS configuration and client certificate loading can be found in class RestClientHelper.cs.
  • PHP: The TLS configuration and client certificate loading can be found in file RestClientConfig.php.
  • Go: The TLS configuration and client certificate loading can be found in file https.go.

Next, we will describe the details of the back-end implementation based on the authentication processes and authentication sequence.

Process 1: Initialise the Authentication

To initialise authentication, the 3DS Requestor needs to:

  • Receive the Initialise authentication request from the 3DS-web-adapter (Step. 2).
  • Forward the request to ActiveServer (Step. 3).
  • Receive the response data from ActiveServer (Step. 4).
  • Return the response data to the 3DS-web-adapter (Step. 5).
 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
//AuthController.java
/**
 * Receives the initialise authentication request from the 3DS-web-adapter (Step 2)
 * Send data to ActiveServer to Initialise Authentication
 */
@PostMapping("/auth/init/{messageCategory}")
public Message initAuth(@RequestBody Message request,
    @PathVariable(value = "messageCategory") String messageCategory) {

  //Generate requestor trans ID
  String transId = UUID.randomUUID().toString();
  request.put("threeDSRequestorTransID", transId);
  //Fill the event call back url with requestor url + /3ds-notify
  String callBackUrl = config.getBaseUrl() + "/3ds-notify";
  request.put("eventCallbackUrl", callBackUrl);

  //ActiveServer url for Initialise Authentication
  String initAuthUrl = config.getAsAuthUrl() + "/api/v1/auth/brw/init/" + messageCategory;
  logger.info("initAuthRequest on url: {}, body: \n{}", initAuthUrl, request);

  //Send data to ActiveServer to Initialise authentication (Step 3)
  //Get the response data from ActiveServer (Step 4)
  Message response =
      restTemplate.postForObject(initAuthUrl, request, Message.class);
  logger.info("initAuthResponseBRW: \n{}", response);

  //Return data to 3ds-web-adapter (Step 5)
  return response;
}
 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
//AuthController.cs
/// <summary>
/// Receives the initialise authentication request from the 3DS-web-adapter (Step 2)
/// Send data to ActiveServer to Initialise Authentication
/// </summary>
/// <param name="request"></param>
/// <returns></returns>
[HttpPost, Route("auth/init/{messageCategory}")]
public Message initAuth([FromBody]Message request, string messageCategory)
{
    //Generate requestor trans ID
    string transId = Guid.NewGuid().ToString();
    request["threeDSRequestorTransID"] = transId;
    //Fill the event call back url with requestor url + /3ds-notify
    string callBackUrl = Config.BaseUrl + "/3ds-notify";
    request["eventCallbackUrl"] = callBackUrl;

    //ActiveServer url for Initialise Authentication
    string initAuthUrl = Config.AsAuthUrl + "/api/v1/auth/brw/init/" + messageCategory;
    logger.Info(string.Format("initAuthRequest on url: {0}, body: \n{1}", initAuthUrl, request));

    //Send data to ActiveServer to Initialise authentication (Step 3)
    //Get the response data from ActiveServer (Step 4)
    Message response = (Message)RestClientHelper.PostForObject(initAuthUrl, request, typeof(Message));
    logger.Info(string.Format("initAuthResponseBRW: \n{0}", response));

    //Return data to 3ds-web-adapter (Step 5)
    return response;
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?php
//AuthController.php
/**
 * Receives the initialise authentication request from the 3DS-web-adapter (Step 2)
 * Send data to ActiveServer to Initialise Authentication
 */
public function initAuth($messageCategory)
{
    $requestData = $this->_getJsonData();
    //Generate requestor trans ID
    $requestData->threeDSRequestorTransID = $this->_getUUId();
    //Fill the event call back url with requestor url + /3ds-notify
    $requestData->eventCallbackUrl = $this->config->getBaseUrl() . "/3ds-notify";

    //ActiveServer url for Initialise Authentication
    $initAuthUrl = "/api/v1/auth/brw/init/" . $messageCategory;
    //Send data to ActiveServer to Initialise authentication (Step 3)
    //Get the response data from ActiveServer (Step 4)
    $response = $this->restTemplate->post($initAuthUrl, $requestData);

    //Return data to 3ds-web-adapter (Step 5)
    $this->_returnJson($response->getBody());
}
 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
//main.go
func authController(r *gin.Engine, config *Config, httpClient *http.Client) {
    r.POST("/auth/init/:messageCategory", func(c *gin.Context) {

        var message map[string]interface{}

        err := c.ShouldBindJSON(&message)
        if err != nil {
            c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
            return
        }

        //adding requestorTransId
        message["threeDSRequestorTransID"] = uuid.New()
        //add callback event url
        message["eventCallbackUrl"] = config.GPayments.BaseUrl + "/3ds-notify"

        callASAPI(message, config.GPayments.AsAuthUrl+"/api/v1/auth/brw/init/"+c.Param("messageCategory"), c, httpClient)

    })
...
}

//call ActiveServer API, if message == nil, do GET otherwise POST
func callASAPI(message map[string]interface{}, url string, c *gin.Context, httpClient *http.Client) {

    var r *http.Response
    var err error

    if message == nil {
        r, err = httpClient.Get(url)
    } else {
        var data []byte
        data, err = json.Marshal(message)
        if err != nil {
            c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
            return
        }

        r, err = httpClient.Post(url, "application/json;charset=utf-8", bytes.NewBuffer(data))
    }

    defer r.Body.Close()

    if err != nil {
        c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
        return
    }

    response, err := ioutil.ReadAll(r.Body)

    if err != nil {
        c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
        return
    }

    contentType := r.Header.Get("Content-Type")

    c.Data(http.StatusOK, contentType, response)

}

Note

We set the eventCallbackUrl to http://localhost:8082/3ds-notify. This allows ActiveServer to make a notification when the browser information collection (Step. 7) is done.

The initAuth request will be sent to {ActiveServer auth url}/api/v1/auth/brw/init/{messageCategory}. To check the data structure of initAuth request, refer to the API document.

Process 2: Execute Authentication

To execute authentication, the 3DS Requestor needs to:

  • Handle the /3ds-notify message from ActiveServer called through the iframe after Step. 7.

  • Handle the Execute authentication request from the 3DS-web-adapter (Step. 9 and Step. 10).

  • Receive the authentication result and return it to the 3DS-web-adapter (Step. 12 and Step. 13).

When the browser information collection (Step. 7) is done, ActiveServer makes a notification to the eventCallbackUrl which we set to http://localhost:8082/3ds-notify. The 3DS Requestor handles this notification and passes the required parameters to the notify-3ds-events.html page.

 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;
  if ("3DSMethodFinished".equals(callbackType)) {

    callbackName = "_on3DSMethodFinished";

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

    callbackName = "_on3DSMethodSkipped";

  } else if ("AuthResultReady".equals(callbackType)) {
    callbackName = "_onAuthResult";
  } else {
    throw new IllegalArgumentException("invalid callback type");
  }

  model.addAttribute("transId", transId);
  model.addAttribute("callbackName", callbackName);
  model.addAttribute("callbackParam", param);

  return "notify_3ds_events";
}
 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
//MainController.cs
[HttpPost, ActionName("3ds-notify")]
public ActionResult NotifyResult()
{
    string transId = Request.Params["requestorTransId"];
    string callbackType = Request.Params["event"];
    string param = Request.Params["param"];

    String callbackName;
    if ("3DSMethodFinished".Equals(callbackType))
        callbackName = "_on3DSMethodFinished";
    else if ("3DSMethodSkipped".Equals(callbackType))
        callbackName = "_on3DSMethodSkipped";
    else if ("AuthResultReady".Equals(callbackType))
        callbackName = "_onAuthResult";
    else
        throw new ArgumentException("invalid callback type");

    dynamic model = new ExpandoObject();
    model.transId = transId;
    model.callbackName = callbackName;
    model.callbackParam = param;

    return View("notify_3ds_events", model);
}
 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
<?php
//MainController.php
public function notifyResult()
{
    $requestorTransId = $_POST["requestorTransId"];
    $callbackType = $_POST["event"];
    $param = (isset($_POST["param"]) && !empty($_POST["param"])) ? $_POST["param"] : "";

    if ($callbackType === "3DSMethodFinished") {

        $callbackName = "_on3DSMethodFinished";

    } else if ($callbackType === "3DSMethodSkipped") {

        $callbackName = "_on3DSMethodSkipped";

    } else if ($callbackType === "AuthResultReady") {
        $callbackName = "_onAuthResult";

    } else if ($callbackType === "InitAuthTimedOut") {

        $callbackName = "_onInitAuthTimedOut";
    } else {
        echo "invalid callback type";
        exit;
    }

    $this->model["transId"] = $requestorTransId;
    $this->model["callbackName"] = $callbackName;
    $this->model["callbackParam"] = $param;

    $this->_render("notify_3ds_events");
}
 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
//main.go
//Main controller
func mainController(r *gin.Engine, config *Config) {
...
    r.POST("/3ds-notify", func(c *gin.Context) {

        transId := c.PostForm("requestorTransId")
        if transId == "" {
            c.JSON(http.StatusBadRequest, gin.H{"error": "invalid parameter"})
        }

        event := c.PostForm("event")
        if event == "" {
            c.JSON(http.StatusBadRequest, gin.H{"error": "invalid parameter"})
        }

        param := c.Param("param")

        var callbackName string

        switch event {
        case "3DSMethodFinished":

            callbackName = "_on3DSMethodFinished"

        case "3DSMethodSkipped":

            callbackName = "_on3DSMethodSkipped"

        case "AuthResultReady":

            callbackName = "_onAuthResult"

        case "InitAuthTimedOut":

            callbackName = "_onInitAuthTimedOut"

        default:

            c.JSON(http.StatusBadRequest, gin.H{"error": "invalid callback type"})
            return

        }

        renderPage(gin.H{
            "transId":       transId,
            "callbackName":  callbackName,
            "callbackParam": param,
        }, notify3DSEventsTpl, c)
    })

}

Note

This handler is called by ActiveServer, parameters requestorTransId, event and an optional param will be provided by ActiveServer. The event can be 3DSMethodFinished, 3DSMethodSkipped, InitAuthTimedOut or AuthResultReady. For descriptions of each event please refer to our API document for the field eventCallbackUrl. The handler method then sets the proper attribute in the page context and returns to the notify_3ds_events.html page. The page then renders the content with Mustache templating engine.

To check the front-end implementation of notify_3ds_events.html, refer here.

Then when the 3DS client is finished browser information collecting, it will call the /auth endpoint to start the authentication. The 3DS Requestor handles the Execute authentication requests (Step. 9, Step. 10, Step. 12, and Step. 13).

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
//AuthController.java
/**
 * Receives the Execute authentication request from the 3DS-web-adapter (Step 9)
 * Send data to ActiveServer to Execute Authentication
 */
@PostMapping("/auth")
public Message auth(@RequestBody Message request) {

  //ActiveServer url for Execute Authentication
  String authUrl = config.getAsAuthUrl() + "/api/v1/auth/brw";
  logger.info("requesting BRW Auth API {}, body: \n{}", authUrl, request);

  //Send data to ActiveServer to Execute Authentication (Step 10)
  //Get the response data from ActiveServer (Step 12)
  Message response =
      restTemplate.postForObject(authUrl, request, Message.class);
  logger.info("authResponseBRW: \n{}", response);

  //Return data to 3ds-web-adapter (Step 13)
  return response;
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
//AuthController.cs
/// <summary>
/// Receives the Execute authentication request from the 3DS-web-adapter(Step 9) Send data to
/// ActiveServer to Execute Authentication
/// </summary>
/// <param name="trans"></param>
/// <returns></returns>
[HttpPost, Route("auth")]
public Message auth([FromBody]Message request)
{
    //ActiveServer url for Execute Authentication
    String authUrl = Config.AsAuthUrl + "/api/v1/auth/brw";
    logger.Info(string.Format("requesting BRW Auth API {0}, body: \n{1}", authUrl, request));

    //Send data to ActiveServer to Execute Authentication (Step 10)
    //Get the response data from ActiveServer (Step 12)
    Message response = (Message)RestClientHelper.PostForObject(authUrl, request, typeof(Message));
    logger.Info(string.Format("authResponseBRW: \n{0}", response));

    //Return data to 3ds-web-adapter (Step 13)
    return response;
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
<?php
//AuthController.php
/**
 * Receives the Execute authentication request from the 3DS-web-adapter (Step 9) Send data to
 * ActiveServer to Execute Authentication
 */
public function auth()
{
    $requestData = $this->_getJsonData();

    //ActiveServer url for Execute Authentication
    $authUrl = "/api/v1/auth/brw";

    //Send data to ActiveServer to Execute Authentication (Step 10)
    //Get the response data from ActiveServer (Step 12)
    $response = $this->restTemplate->post($authUrl, $requestData);

    //Return data to 3ds-web-adapter (Step 13)
    $this->_returnJson($response->getBody()->getContents());
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
//AuthController routers
func authController(r *gin.Engine, config *Config, httpClient *http.Client) {
...
    r.POST("/auth", func(c *gin.Context) {

        var message map[string]interface{}

        err := c.ShouldBindJSON(&message)
        if err != nil {
            c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
            return
        }

        callASAPI(message, config.GPayments.AsAuthUrl+"/api/v1/auth/brw", c, httpClient)

    })
...
}

Note

The 3DS Requestor returns a response to the front-end. The returned message contains a transStatus with value Y or C, which will trigger either frictionless flow or challenge flow. To check how the front-end handles the transStatus, refer here. To check the structure of the response data, refer to the API document.

Process 3: Get Authentication Result

To get the authentication result, the 3DS Requestor needs to:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
//AuthController.java
/**
 * Receives the Request for authentication result request (Step 15(F) and Step 20(C))
 * Send data to ActiveServer to Retrieve Authentication Results
 */
@GetMapping("/auth/result")
public Message result(@RequestParam("txid") String serverTransId) {

//ActiveServer url for Retrieve Results
String resultUrl = config.getAsAuthUrl() +
    "/api/v1/auth/brw/result?threeDSServerTransID=" +
    serverTransId;

//Get authentication result from ActiveServer (Step 16(F) and Step 21(C))
Message response = restTemplate.getForObject(resultUrl, Message.class);
logger.info("authResponse: \n{}", response);

//Show authentication results on result.html (Step 17(F) and Step 22(C))
return response;
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
/// <summary>
/// Receives the Request for authentication result request (Step 15(F) and Step 20(C))
/// Send data to ActiveServer to Retrieve Authentication Results
/// </summary>
/// <param name="txid"></param>
/// <returns></returns>
[HttpGet, Route("auth/result")]
public Message authResult(String txid)
{

    string serverTransId = txid;
    //ActiveServer url for Retrieve Results
    string resultUrl = Config.AsAuthUrl + "/api/v1/auth/brw/result?threeDSServerTransID=" + serverTransId;

    //Get authentication result from ActiveServer (Step 16(F) and Step 21(C))
    Message result = (Message)RestClientHelper.GetForObject(resultUrl, typeof(Message));

    //Show authentication results on result.html (Step 17(F) and Step 22(C))
    return result;
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
<?php
//AuthController.php
/**
 * Receives the Request for authentication result request (Step 15(F) and Step 20(C))
 * Send data to ActiveServer to Retrieve Authentication Results
 */
public function result()
{
    $serverTransId = $_GET["txid"];
    //ActiveServer url for Retrieve Results
    $resultUrl = "/api/v1/auth/brw/result?threeDSServerTransID=" . $serverTransId;

    //Get authentication result from ActiveServer (Step 16(F) and Step 21(C))
    $response = $this->restTemplate->get($resultUrl);

    //Show authentication results on result.html (Step 17(F) and Step 22(C))
    $this->_returnJson($response->getBody()->getContents());
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
//AuthController routers
func authController(r *gin.Engine, config *Config, httpClient *http.Client) {
...
    r.GET("/auth/result", func(c *gin.Context) {

        transId := c.Query("txid")
        if transId == "" {
            c.JSON(http.StatusBadRequest, gin.H{"error": "invalid transId"})
            return

        }

        callASAPI(nil, config.GPayments.AsAuthUrl+"/api/v1/auth/brw/result?threeDSServerTransID="+transId, c, httpClient)

    })
...
}

Success

This covers the backend implementation. After authentication is completed the checkout process can move on to performing authorisation using the Transaction Status, ECI and CAVV to complete the transaction.

Whats next?

Select the next button to go through all the features of our Sample Code for the GPayments 3DS Requestor.