Back-end implementation (v2)
In this section, we will show the implementation details for the back-end side of the merchant application using our sample code based on the Auth API version 2.
Important
GPayments strongly recommends all new implementations of ActiveServer to integrate with version 2 of the Auth API from the beginning, as version 1 will be deprecated in a future release. Refer to the API migration from v1 to v2 guide for a summary of the changes and migration process from v1 to v2. To check the existing API v1 implementation, refer here.
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, 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.java
. - 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 30 31 32 33 34 35 36 37 38 39 40 | //AuthControllerV2.java @PostMapping("/v2/auth/init") @ResponseBody public Message initAuth( @RequestParam(value = "trans-type", required = false) String transType, @RequestBody Message request, HttpSession session) { return authServiceV2.initAuth(transType, request, session); } //AuthServiceV2.java public Message initAuth(String transType, Message request, HttpSession session) { //Generate requestor trans ID String transId = UUID.randomUUID().toString(); request.put(THREE_DS_REQUESTOR_TRANS_ID, transId); //Fill the event call back url with requestor url + /3ds-notify String callBackUrl = config.getBaseUrl() + "/3ds-notify"; request.put("eventCallbackUrl", callBackUrl); //Send data to ActiveServer to Initialise authentication (Step 3) //Get the response data from ActiveServer (Step 4) Message response = sendInitAuthRequest(transType, request, session); return response; } private Message sendInitAuthRequest(String transType, Message request, HttpSession session) { ... //ActiveServer url for Initialise Authentication String initAuthUrl = config.getAsAuthUrl() + "/api/v2/auth/brw/init"; //Add parameter trans-type=prod in the initAuthUrl to use prod DS, otherwise use TestLabs DS //For example, in this demo, the initAuthUrl for transactions with prod DS is https://api.as.testlab.3dsecure.cloud:7443/api/v2/auth/brw/init?trans-type=prod //For more details, refer to: https://docs.activeserver.cloud if ("prod".equals(transType)) { initAuthUrl = initAuthUrl + "?trans-type=prod"; } Message response = sendRequest(initAuthUrl, request, HttpMethod.POST); ... 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 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 | //AuthV2Controller.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("v2/auth/init")] public Message initAuth([FromBody]Message request, [FromUri(Name = "trans-type")] string transType = null) { return authServiceV2.initAuth(transType, request); } //AuthServiceV2.cs public Message initAuth(string transType, Message request) { //Generate requestor trans ID string transId = Guid.NewGuid().ToString(); request[THREE_DS_REQUESTOR_TRANS_ID] = transId; //Fill the event call back url with requestor url + /3ds-notify string callBackUrl = Config.BaseUrl + "/3ds-notify"; request["eventCallbackUrl"] = callBackUrl; //Send data to ActiveServer to Initialise authentication (Step 3) //Get the response data from ActiveServer (Step 4) Message response = sendInitAuthRequest(transType, request); logger.Info(string.Format("initAuthResponseBRW: \n{0}", response)); return response; } /** * Send data to ActiveServer to Initialise authentication and get the response data from * ActiveServer. * * @param transType: transType=prod to use prod DS, otherwise use TestLabs DS * @param request: init auth request * @return init auth response */ private Message sendInitAuthRequest(String transType, Message request) { setSessionAttribute(INIT_AUTH_REQUEST, request); //ActiveServer url for Initialise Authentication string initAuthUrl = Config.AsAuthUrl + "/api/v2/auth/brw/init"; //Add parameter trans-type=prod in the initAuthUrl to use prod DS, otherwise use testlab DS //For example, in this demo, the initAuthUrl for transactions with prod DS is https://api.as.testlab.3dsecure.cloud:7443/api/v2/auth/brw/init?trans-type=prod //For more details, refer to: https://docs.activeserver.cloud if ("prod".Equals(transType)) initAuthUrl = initAuthUrl + "?trans-type=prod"; logger.Info(string.Format("initAuthRequest on url: {0}, body: \n{1}", initAuthUrl, request)); Message response = (Message)RestClientHelper.PostForObject(initAuthUrl, request, typeof(Message)); if (response != null) setSessionAttribute(INIT_AUTH_RESPONSE, response); else throw new ArgumentException("Invalid init auth response"); 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 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 | <?php //AuthControllerV2.php public function initAuth() { $requestData = Utils::_getJsonData(); //Generate requestor trans ID $requestData->threeDSRequestorTransID = Utils::_getUUId(); //Fill the event call back url with requestor url + /3ds-notify $requestData->eventCallbackUrl = $this->config->getBaseUrl() . "/3ds-notify"; $transType = $_GET["trans-type"]; $responseBody = $this->authService->sendInitAuthRequest($transType, $requestData); //Return data to 3ds-web-adapter (Step 5) Utils::_returnJson($responseBody); } //AuthServiceV2.php public function sendInitAuthRequest($transType, $requestData) { $_SESSION[SessionKeys::INIT_AUTH_REQUEST] = $requestData; //ActiveServer url for Initialise Authentication //Add parameter trans-type=prod in the initAuthUrl to use prod DS, otherwise use testlab DS //For example, in this demo, the initAuthUrl for transactions with prod DS is https://api.as.testlab.3dsecure.cloud:7443/api/v2/auth/brw/init?trans-type=prod //For more details, refer to: https://docs.activeserver.cloud $initAuthUrl = "/api/v2/auth/brw/init"; if (!empty($transType) && $transType == "prod") { $initAuthUrl = $initAuthUrl . "?trans-type=prod"; } //Send data to ActiveServer to Initialise authentication (Step 3) //Get the response data from ActiveServer (Step 4) $response = $this->restTemplate->post($initAuthUrl, $requestData); $responseBody = json_decode($response->getBody()); if ($responseBody != null) { $_SESSION[SessionKeys::INIT_AUTH_RESPONSE] = $responseBody; return $response->getBody(); } else { echo "invalid init auth response"; exit; } } |
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 | //api-v2.go //AuthControllerV2 routers v2 func authControllerV2(r *gin.Engine, config *Config, httpClient *http.Client, fp *mustache.FileProvider) { r.POST("/v2/auth/init", func(c *gin.Context) { var message Message 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" //Add parameter trans-type=prod in the initAuthUrl to use prod DS, otherwise use testlab DS //For example, in this demo, the initAuthUrl for transactions with prod DS is https://api.as.testlab.3dsecure.cloud:7443/api/v2/auth/brw/init?trans-type=prod //For more details, refer to: https://docs.activeserver.cloud callASAPI(asSession{ message: message, url: appendTransTypeIfNecessary("/api/v2/auth/brw/init", c), context: c, httpClient: httpClient, config: config, rHandler: func(resp []byte, contentType string, context *gin.Context) error { msg, err := parseMap(resp) if err != nil { return err } log.Printf("init auth response: %v\n", msg) //store the response in current session. session := sessions.Default(c) session.Set(InitAuthRequest, message) session.Set(InitAuthResponse, msg) err = session.Save() if err != nil { return err } //now return the data context.Data(http.StatusOK, contentType, resp) return nil }}) }) ... } |
Note
We set the eventCallbackUrl
to {baseUrl}/3ds-notify
. This allows ActiveServer to make a notification when the browser information collection (Step. 7) is done. The baseUrl
is set here.
The initAuth
request will be sent to {ActiveServer auth url}/api/v2/auth/brw/init
. To check the data structure of initAuth
request, refer to the API document.
Important: By default, the URL above will send the authentication request to GPayments TestLabs for testing purposes. When moving into production, to send the API request to the card scheme directory server, the trans-type query parameter must be appended to this API URL. See API description for further information on usage.
We save the initAuth
response message to session for future use. To check the data structure of initAuth
response, refer to the API document.
HTTP header for Master Auth API client certificate¶
If you are using a Master Auth API client certificate to authenticate a Business Admin user on behalf of a merchant, the back-end needs to add a HTTP Header in the HTTP Request with a field of AS-Merchant-Token
, which should be set to the merchantToken from the merchants profile.
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 | //Note: the sendRequest() function will send the request to ActiveServer. //If this is groupAuth, the request should include a HTTP Header with the field of AS-Merchant-Token. private Message sendRequest(String url, Message request, HttpMethod method) { HttpEntity<Message> req; HttpHeaders headers = null; if (config.isGroupAuth()) { //the certificate is for groupAuth, work out the header. headers = new HttpHeaders(); headers.add("AS-Merchant-Token", config.getMerchantToken()); } switch (method) { case POST: req = new HttpEntity<>(request, headers); return restTemplate.postForObject(url, req, Message.class); case GET: if (headers == null) { return restTemplate.getForObject(url, Message.class); } else { req = new HttpEntity<>(headers); return restTemplate.exchange(url, HttpMethod.GET, req, Message.class).getBody(); } default: return null; } } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | //RestClientHelper.cs public static object PostForObject(string url, object request, Type responseType) { HttpWebRequest req = (HttpWebRequest)HttpWebRequest.Create(url); req.Method = "POST"; req.ClientCertificates.Add(GetClientCertificate()); //the certificate is for groupAuth, work out the header if (Config.GroupAuth) req.Headers.Add("AS-Merchant-Token", Config.MerchantToken); req.ContentType = "application/json;charset=utf-8"; string strRequest = JsonConvert.SerializeObject(request, Formatting.Indented, new JsonSerializerSettings() { NullValueHandling = NullValueHandling.Ignore }); byte[] postData = System.Text.Encoding.UTF8.GetBytes(strRequest); req.ContentLength = postData.Length; using (Stream streamOut = req.GetRequestStream()) streamOut.Write(postData, 0, postData.Length); string result = null; using (StreamReader streamIn = new StreamReader(req.GetResponse().GetResponseStream())) result = streamIn.ReadToEnd(); return JsonConvert.DeserializeObject(result, responseType); } |
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 | <?php //Note: the post() function will send the request to ActiveServer. //If this is groupAuth, the request should include a HTTP Header with the field of AS-Merchant-Token. //RestClientConfig.php public function __construct(Config $config) { ... if ($config->isGroupAuth()) { $this->headers = array('AS-Merchant-Token' => $config->getMerchantToken()); } } function post($request_uri, $post_data) { $response = $this->client->request( "POST", $request_uri, ['json' => $post_data, 'headers' => $this->headers]); return $response; } function get($request_uri) { $response = $this->client->request("GET", $request_uri, ['headers' => $this->headers]); 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 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 | //main.go //call ActiveServer API, if message == nil, do GET otherwise POST, return response if any func callASAPI(session asSession) { var r *http.Request var err error //generate the url, if the url starts with http, use it as is otherwise prefix with the base URL var url string if strings.HasPrefix(strings.ToLower(session.url), "http") { url = session.url } else { url = session.config.GPayments.AsAuthUrl + session.url } if session.message == nil { log.Println("Send GET request to 3DS Server, url: " + url) r, err = http.NewRequest("GET", url, nil) if err != nil { session.context.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } } else { var data []byte data, err = json.Marshal(session.message) if err != nil { session.context.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } log.Printf("Send POST request to 3DS Server, url: %s, body: %v\n", url, session.message) r, err = http.NewRequest("POST", url, bytes.NewBuffer(data)) if err != nil { session.context.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } r.Header.Set("Content-Type", "application/json;charset=utf-8") } //if this is groupAuth if session.config.GPayments.GroupAuth { r.Header.Set("AS-Merchant-Token", session.config.GPayments.MerchantToken) } response, err := session.httpClient.Do(r) if err != nil { session.context.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } defer response.Body.Close() contentType := response.Header.Get("Content-Type") responseBody, err := ioutil.ReadAll(response.Body) if err != nil { session.context.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } log.Printf("Received response with content type: %s, content: %s\n", contentType, string(responseBody)) if session.rHandler != nil { //process the response by the responseHandler. the handle returns the data as well. if err = session.rHandler(responseBody, contentType, session.context); err != nil { session.context.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } } else { //if no response handler provided, return the data by default. session.context.Data(http.StatusOK, contentType, responseBody) } } |
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 30 31 32 33 34 35 | //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 if ("InitAuthTimedOut".equals(callbackType)) { callbackName = "_onInitAuthTimedOut"; } 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 26 27 | //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 if ("InitAuthTimedOut".Equals(callbackType)) callbackName = "_onInitAuthTimedOut"; 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 53 | //main.go func mainController(r *gin.Engine, config *Config, httpClient *http.Client) { ... r.POST("/3ds-notify", func(c *gin.Context) { transId := c.PostForm("requestorTransId") if transId == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid parameter"}) return } event := c.PostForm("event") if event == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid parameter"}) return } param := c.PostForm("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 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 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 | //AuthControllerV2.java /** * Receives the Execute authentication request from the 3DS-web-adapter (Step 9) Send data to * ActiveServer to Execute Authentication */ @PostMapping("/v2/auth") @ResponseBody public Message auth(@RequestBody Message request, HttpSession session) { return authServiceV2.auth(request, session); } //AuthServiceV2.java public Message auth(Message request, HttpSession session) { verifySessionTransId((String) request.get(THREE_DS_REQUESTOR_TRANS_ID), session); //Send data to ActiveServer to Execute Authentication (Step 10) //Get the response data from ActiveServer (Step 12) Message response = sendAuthRequest(request, session); logger.info("authResponseBRW: \n{}", response); return response; } private Message sendAuthRequest(Message request, HttpSession session) { setSessionAttribute(AUTH_REQUEST, request, session); //get authUrl from session storage Message initAuthResponse = getSessionAttribute(INIT_AUTH_RESPONSE, session); String authUrl = (String) initAuthResponse.get("authUrl"); logger.info("requesting BRW Auth API {}, body: \n{}", authUrl, request); Message response = sendRequest(authUrl, request, HttpMethod.POST); if (response != null) { setSessionAttribute(AUTH_RESPONSE, response, session); } else { throw new IllegalArgumentException("Invalid auth response"); } 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 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 | //AuthV2Controller.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("v2/auth")] public Message auth([FromBody]Message request) { return authServiceV2.auth(request); } //AuthServiceV2.cs public Message auth(Message request) { verifySessionTransId((String)request[THREE_DS_REQUESTOR_TRANS_ID]); //Send data to ActiveServer to Execute Authentication (Step 10) //Get the response data from ActiveServer (Step 12) Message response = sendAuthRequest(request); logger.Info(string.Format("authResponseBRW: \n{0}", response)); return response; } private Message sendAuthRequest(Message request) { setSessionAttribute(AUTH_REQUEST, request); //get authUrl from session storage Message initAuthResponse = getSessionAttribute(INIT_AUTH_RESPONSE); String authUrl = (String)initAuthResponse["authUrl"]; logger.Info(string.Format("requesting BRW Auth API {0}, body: \n{1}", authUrl, request)); Message response = (Message)RestClientHelper.PostForObject(authUrl, request, typeof(Message)); if (response != null) { setSessionAttribute(AUTH_RESPONSE, response); } else { throw new ArgumentException("Invalid auth response"); } 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 30 31 32 33 34 35 | <?php //AuthControllerV2.php public function auth() { $requestData = Utils::_getJsonData(); //Send data to ActiveServer to Execute Authentication (Step 10) //Get the response data from ActiveServer (Step 12) $response = $this->authService->sendAuthRequest($requestData); //Return data to 3ds-web-adapter (Step 13) Utils::_returnJson($response); } //AuthServiceV2.php function sendAuthRequest($requestData) { $_SESSION[SessionKeys::AUTH_REQUEST] = $requestData; //ActiveServer url for Execute Authentication $authUrl = $_SESSION[SessionKeys::INIT_AUTH_RESPONSE]->authUrl; //Send data to ActiveServer to Execute Authentication (Step 10) //Get the response data from ActiveServer (Step 12) $response = $this->restTemplate->post($authUrl, $requestData); if ($response != null) { $_SESSION[SessionKeys::AUTH_RESPONSE] = json_decode($response->getBody()); return $response->getBody(); } else { echo "invalid auth response"; exit; } } |
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 | //api-v2.go r.POST("/v2/auth", func(c *gin.Context) { //get authUrl from session authUrl := getSessionAttribute(c, InitAuthResponse, "authUrl").(string) if authUrl == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid authUrl"}) return } var message Message err := c.ShouldBindJSON(&message) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } callASAPI(asSession{ message: message, url: authUrl, //this url will be used as is context: c, httpClient: httpClient, config: config}) }) ... } |
Note
The authUrl
is the url to perform authentication which is defined in initAuth
response message. The initAuth
response message is stored in session when received in process 1.
The 3DS Requestor returns a response to the front-end. The returned message contains a transStatus
with value Y
, C
or D
, which will trigger frictionless flow, challenge flow or decoupled authentication flow respectively. To check how the front-end handles the transStatus
, refer here. To check the structure of the response data, refer to the API document.
Cancel challenge flow¶
When the transStatus=C
, the 3DS client can choose to start the challenge or not. If the 3DS client chooses to cancel the challenge, it may call the challengeStatus
endpoint to specify the Cancel Reason which the 3DS Requestor will send to ActiveServer. To check how the front-end handles this request, refer here.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | //AuthControllerV2.java @PostMapping("/v2/auth/challenge/status") @ResponseBody public Message challengeStatus(@RequestBody Message request) { return authServiceV2.challengeStatus(request); } //AuthServiceV2.java public Message challengeStatus(Message request) { String challengeStatusUrl = config.getAsAuthUrl() + "/api/v2/auth/challenge/status"; logger.info("request challenge status API {}, body: \n{}", challengeStatusUrl, request); Message response = sendRequest(challengeStatusUrl, request, HttpMethod.POST); logger.info("challengeStatus response: \n{}", response); return response; } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | //AuthV2Controller.cs [HttpPost, Route("v2/auth/challenge/status")] public Message challengeStatus([FromBody]Message request) { return authServiceV2.challengeStatus(request); } //AuthServiceV2.cs public Message challengeStatus(Message request) { String challengeStatusUrl = Config.AsAuthUrl + "/api/v2/auth/challenge/status"; logger.Info(string.Format("request challenge status API {0}, body: \n{1}", challengeStatusUrl, request)); Message response = (Message)RestClientHelper.PostForObject(challengeStatusUrl, request, typeof(Message)); logger.Info(string.Format("challengeStatus response: \n{0}", response)); return response; } |
1 2 3 4 5 6 7 8 9 | <?php //AuthControllerV2.php public function challengeStatus() { $requestData = Utils::_getJsonData(); $challengeStatusUrl = "/api/v2/auth/challenge/status"; $response = $this->restTemplate->post($challengeStatusUrl, $requestData); Utils::_returnJson($response->getBody()->getContents()); } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | //api-v2.go r.POST("/v2/auth/challenge/status", func(c *gin.Context) { var message Message err := c.ShouldBindJSON(&message) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } callASAPI(asSession{ message: message, url: "/api/v2/auth/challenge/status", context: c, httpClient: httpClient, config: config}) }) |
Process 3: Get Authentication Result¶
To get the authentication result, the 3DS Requestor needs to:
- Handle the
Request for authentication result
request from the 3DS-web-adapter (Step. 15(F), Step. 26(C) or Step. 19(D)). - Send a request to ActiveServer to get the result (Step. 16(F), Step. 27(C) or Step. 20(D)).
- Return the result to the front-end (Step. 17(F), Step. 28(C) or Step. 21(D), 22(D)).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | //AuthControllerV2.java @ResponseBody @GetMapping("/v2/auth/brw/result") public Message resultBRW(@RequestParam("txid") String serverTransId) { return authServiceV2.getBRWResult(serverTransId); } //AuthServiceV2.java public Message getBRWResult(String serverTransId) { //ActiveServer url for Retrieve Results String resultUrl = config.getAsAuthUrl() + "/api/v2/auth/brw/result?threeDSServerTransID=" + serverTransId; //Get authentication result from ActiveServer Message response = sendRequest(resultUrl, null, HttpMethod.GET); logger.info("authResponse: \n{}", response); 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 | /// <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("v2/auth/brw/result")] public Message authResult(String txid) { return authServiceV2.getBRWResult(txid); } //AuthServiceV2.cs public Message getBRWResult(String serverTransId) { //ActiveServer url for Retrieve Results string resultUrl = Config.AsAuthUrl + "/api/v2/auth/brw/result?threeDSServerTransID=" + serverTransId; //Get authentication result from ActiveServer Message result = (Message)RestClientHelper.GetForObject(resultUrl, typeof(Message)); return result; } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | <?php //AuthControllerV2.php public function brwResult() { $serverTransId = $_GET["txid"]; $response = $this->authService->getBrwResult($serverTransId); //Show authentication results on result.html (Step. 17(F), Step. 28(C) or Step. 21(D), 22(D)). Utils::_returnJson($response); } //AuthServiceV2.php public function getBrwResult($serverTransId) { //ActiveServer url for Retrieve Results $resultUrl = "/api/v2/auth/brw/result?threeDSServerTransID=" . $serverTransId; //Get authentication result from ActiveServer (Step 16(F), Step 27(C) or Step 20(D)) return $this->restTemplate->get($resultUrl)->getBody(); } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | //api-v2.go r.GET("/v2/auth/brw/result", func(c *gin.Context) { transId := getSessionAttribute(c, InitAuthResponse, ThreeDSServerTransId).(string) if transId == "" { c.JSON(http.StatusInternalServerError, gin.H{"error": "invalid transId"}) return } callASAPI(asSession{ url: "/api/v2/auth/brw/result?threeDSServerTransID=" + transId, context: c, httpClient: httpClient, config: config}) }) ... } |
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?
If implementing v2 of the Auth API, skip to the Sample code features page to view all the features of our Sample Code for the GPayments 3DS Requestor. Otherwise, select Next to learn about the v1 Front-end implementation for a 3DS Requestor.