3D Secure Integration in Java: Complete Implementation Guide

3D Secure is a security protocol that adds an extra layer of authentication for online credit and debit card transactions. This comprehensive guide covers implementing 3D Secure (3DS) in Java applications.

Understanding 3D Secure Flow

The 3DS2 flow involves:

  1. Initialization - Start authentication process
  2. Challenge - Customer authentication (if required)
  3. Completion - Final authentication result

Dependencies and Setup

Maven Configuration

<!-- pom.xml -->
<project>
<modelVersion>4.0.0</modelVersion>
<groupId>com.securepay</groupId>
<artifactId>threedsecure-integration</artifactId>
<version>1.0.0</version>
<properties>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<jackson.version>2.15.2</jackson.version>
<spring-boot.version>2.7.0</spring-boot.version>
</properties>
<dependencies>
<!-- Spring Boot -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>${spring-boot.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
<version>${spring-boot.version}</version>
</dependency>
<!-- JSON Processing -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<version>${jackson.version}</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>${jackson.version}</version>
</dependency>
<!-- HTTP Client -->
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.14</version>
</dependency>
<!-- Security -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
<version>${spring-boot.version}</version>
</dependency>
<!-- Testing -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<version>${spring-boot.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>${spring-boot.version}</version>
</plugin>
</plugins>
</build>
</project>

Core 3D Secure Implementation

Example 1: Configuration and Constants

package com.securepay.threedsecure.config;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import javax.validation.constraints.NotBlank;
@Component
@ConfigurationProperties(prefix = "threedsecure")
public class ThreeDSecureConfig {
@NotBlank
private String apiUrl;
@NotBlank
private String apiKey;
@NotBlank
private String apiSecret;
private String merchantId;
private String merchantName;
private String notificationUrl;
private int timeoutSeconds = 30;
// Getters and setters
public String getApiUrl() { return apiUrl; }
public void setApiUrl(String apiUrl) { this.apiUrl = apiUrl; }
public String getApiKey() { return apiKey; }
public void setApiKey(String apiKey) { this.apiKey = apiKey; }
public String getApiSecret() { return apiSecret; }
public void setApiSecret(String apiSecret) { this.apiSecret = apiSecret; }
public String getMerchantId() { return merchantId; }
public void setMerchantId(String merchantId) { this.merchantId = merchantId; }
public String getMerchantName() { return merchantName; }
public void setMerchantName(String merchantName) { this.merchantName = merchantName; }
public String getNotificationUrl() { return notificationUrl; }
public void setNotificationUrl(String notificationUrl) { this.notificationUrl = notificationUrl; }
public int getTimeoutSeconds() { return timeoutSeconds; }
public void setTimeoutSeconds(int timeoutSeconds) { this.timeoutSeconds = timeoutSeconds; }
}
// Constants for 3DS
class ThreeDSConstants {
public static final String API_VERSION = "2.2.0";
public static final String MESSAGE_CATEGORY = "payment";
public static final String TRANSACTION_TYPE = "goods";
public static final String INTERFACE_TYPE = "native";
// Message Types
public static final String AUTHENTICATION_INITIALIZATION = "AReq";
public static final String AUTHENTICATION_RESPONSE = "ARes";
public static final String CHALLENGE_REQUEST = "CReq";
public static final String CHALLENGE_RESPONSE = "CRes";
// Transaction Status
public static final String STATUS_AUTHENTICATION_SUCCESSFUL = "Y";
public static final String STATUS_AUTHENTICATION_REJECTED = "N";
public static final String STATUS_AUTHENTICATION_UNAVAILABLE = "U";
public static final String STATUS_CHALLENGE_REQUIRED = "C";
public static final String STATUS_AUTHENTICATION_ATTEMPTED = "A";
// Challenge Window Sizes
public static final String WINDOW_SIZE_250_400 = "01";
public static final String WINDOW_SIZE_390_400 = "02";
public static final String WINDOW_SIZE_500_600 = "03";
public static final String WINDOW_SIZE_600_400 = "04";
public static final String WINDOW_SIZE_FULL_SCREEN = "05";
}

Example 2: Data Models

package com.securepay.threedsecure.model;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import javax.validation.constraints.*;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.Map;
// Main authentication request
@JsonInclude(JsonInclude.Include.NON_NULL)
public class AuthenticationRequest {
@NotBlank(message = "API version is required")
@JsonProperty("apiVersion")
private String apiVersion = ThreeDSConstants.API_VERSION;
@NotBlank(message = "Message category is required")
@JsonProperty("messageCategory")
private String messageCategory = ThreeDSConstants.MESSAGE_CATEGORY;
@NotBlank(message = "Transaction ID is required")
@JsonProperty("threeDSServerTransID")
private String threeDSServerTransID;
@NotBlank(message = "Account number is required")
@JsonProperty("acctNumber")
private String acctNumber;
@NotBlank(message = "Merchant ID is required")
@JsonProperty("merchantID")
private String merchantID;
@NotBlank(message = "Merchant name is required")
@JsonProperty("merchantName")
private String merchantName;
@NotNull(message = "Purchase amount is required")
@JsonProperty("purchaseAmount")
private BigDecimal purchaseAmount;
@NotBlank(message = "Purchase currency is required")
@JsonProperty("purchaseCurrency")
private String purchaseCurrency;
@NotBlank(message = "Purchase exponent is required")
@JsonProperty("purchaseExponent")
private String purchaseExponent = "2";
@JsonProperty("purchaseDate")
private String purchaseDate;
@JsonProperty("notificationURL")
private String notificationURL;
@JsonProperty("browserJavaEnabled")
private Boolean browserJavaEnabled = false;
@JsonProperty("browserJavaScriptEnabled")
private Boolean browserJavaScriptEnabled = true;
@JsonProperty("browserLanguage")
private String browserLanguage = "en-US";
@JsonProperty("browserColorDepth")
private String browserColorDepth = "24";
@JsonProperty("browserScreenHeight")
private String browserScreenHeight = "1080";
@JsonProperty("browserScreenWidth")
private String browserScreenWidth = "1920";
@JsonProperty("browserTimeZone")
private String browserTimeZone = "-300";
@JsonProperty("browserAcceptHeader")
private String browserAcceptHeader = "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8";
@JsonProperty("browserUserAgent")
private String browserUserAgent;
@JsonProperty("sdkAppID")
private String sdkAppID;
@JsonProperty("sdkEncData")
private String sdkEncData;
@JsonProperty("sdkEphemPubKey")
private String sdkEphemPubKey;
@JsonProperty("sdkMaxTimeout")
private String sdkMaxTimeout = "10";
@JsonProperty("sdkReferenceNumber")
private String sdkReferenceNumber;
@JsonProperty("sdkTransID")
private String sdkTransID;
// Constructors, getters, and setters
public AuthenticationRequest() {}
public AuthenticationRequest(String threeDSServerTransID, String acctNumber, 
String merchantID, String merchantName, 
BigDecimal purchaseAmount, String purchaseCurrency) {
this.threeDSServerTransID = threeDSServerTransID;
this.acctNumber = acctNumber;
this.merchantID = merchantID;
this.merchantName = merchantName;
this.purchaseAmount = purchaseAmount;
this.purchaseCurrency = purchaseCurrency;
this.purchaseDate = LocalDateTime.now().toString();
}
// Getters and setters for all fields
public String getApiVersion() { return apiVersion; }
public void setApiVersion(String apiVersion) { this.apiVersion = apiVersion; }
public String getMessageCategory() { return messageCategory; }
public void setMessageCategory(String messageCategory) { this.messageCategory = messageCategory; }
public String getThreeDSServerTransID() { return threeDSServerTransID; }
public void setThreeDSServerTransID(String threeDSServerTransID) { this.threeDSServerTransID = threeDSServerTransID; }
public String getAcctNumber() { return acctNumber; }
public void setAcctNumber(String acctNumber) { this.acctNumber = acctNumber; }
public String getMerchantID() { return merchantID; }
public void setMerchantID(String merchantID) { this.merchantID = merchantID; }
public String getMerchantName() { return merchantName; }
public void setMerchantName(String merchantName) { this.merchantName = merchantName; }
public BigDecimal getPurchaseAmount() { return purchaseAmount; }
public void setPurchaseAmount(BigDecimal purchaseAmount) { this.purchaseAmount = purchaseAmount; }
public String getPurchaseCurrency() { return purchaseCurrency; }
public void setPurchaseCurrency(String purchaseCurrency) { this.purchaseCurrency = purchaseCurrency; }
public String getPurchaseExponent() { return purchaseExponent; }
public void setPurchaseExponent(String purchaseExponent) { this.purchaseExponent = purchaseExponent; }
public String getPurchaseDate() { return purchaseDate; }
public void setPurchaseDate(String purchaseDate) { this.purchaseDate = purchaseDate; }
public String getNotificationURL() { return notificationURL; }
public void setNotificationURL(String notificationURL) { this.notificationURL = notificationURL; }
// ... additional getters and setters for all fields
}
// Authentication response
@JsonInclude(JsonInclude.Include.NON_NULL)
public class AuthenticationResponse {
@JsonProperty("apiVersion")
private String apiVersion;
@JsonProperty("threeDSServerTransID")
private String threeDSServerTransID;
@JsonProperty("transStatus")
private String transStatus;
@JsonProperty("authenticationValue")
private String authenticationValue;
@JsonProperty("eci")
private String eci;
@JsonProperty("acsTransID")
private String acsTransID;
@JsonProperty("dsTransID")
private String dsTransID;
@JsonProperty("messageVersion")
private String messageVersion;
@JsonProperty("messageType")
private String messageType;
@JsonProperty("challengeCompletionIndicator")
private String challengeCompletionIndicator;
@JsonProperty("acsURL")
private String acsURL;
@JsonProperty("creq")
private String creq;
@JsonProperty("acsRenderingType")
private Map<String, Object> acsRenderingType;
@JsonProperty("acsChallengeMandated")
private String acsChallengeMandated;
@JsonProperty("authenticationType")
private String authenticationType;
@JsonProperty("messageExtension")
private Map<String, Object>[] messageExtension;
@JsonProperty("sdkTransID")
private String sdkTransID;
@JsonProperty("errorCode")
private String errorCode;
@JsonProperty("errorDescription")
private String errorDescription;
@JsonProperty("errorDetail")
private String errorDetail;
// Constructors
public AuthenticationResponse() {}
// Getters and setters
public String getApiVersion() { return apiVersion; }
public void setApiVersion(String apiVersion) { this.apiVersion = apiVersion; }
public String getThreeDSServerTransID() { return threeDSServerTransID; }
public void setThreeDSServerTransID(String threeDSServerTransID) { this.threeDSServerTransID = threeDSServerTransID; }
public String getTransStatus() { return transStatus; }
public void setTransStatus(String transStatus) { this.transStatus = transStatus; }
public String getAuthenticationValue() { return authenticationValue; }
public void setAuthenticationValue(String authenticationValue) { this.authenticationValue = authenticationValue; }
public String getEci() { return eci; }
public void setEci(String eci) { this.eci = eci; }
public String getAcsTransID() { return acsTransID; }
public void setAcsTransID(String acsTransID) { this.acsTransID = acsTransID; }
public String getDsTransID() { return dsTransID; }
public void setDsTransID(String dsTransID) { this.dsTransID = dsTransID; }
public String getMessageVersion() { return messageVersion; }
public void setMessageVersion(String messageVersion) { this.messageVersion = messageVersion; }
public String getMessageType() { return messageType; }
public void setMessageType(String messageType) { this.messageType = messageType; }
public String getChallengeCompletionIndicator() { return challengeCompletionIndicator; }
public void setChallengeCompletionIndicator(String challengeCompletionIndicator) { this.challengeCompletionIndicator = challengeCompletionIndicator; }
public String getAcsURL() { return acsURL; }
public void setAcsURL(String acsURL) { this.acsURL = acsURL; }
public String getCreq() { return creq; }
public void setCreq(String creq) { this.creq = creq; }
// ... additional getters and setters
}
// Challenge request
public class ChallengeRequest {
@NotBlank
@JsonProperty("threeDSServerTransID")
private String threeDSServerTransID;
@NotBlank
@JsonProperty("acsTransID")
private String acsTransID;
@NotBlank
@JsonProperty("challengeWindowSize")
private String challengeWindowSize;
@NotBlank
@JsonProperty("messageType")
private String messageType = ThreeDSConstants.CHALLENGE_REQUEST;
@JsonProperty("messageVersion")
private String messageVersion = ThreeDSConstants.API_VERSION;
// Getters and setters
public String getThreeDSServerTransID() { return threeDSServerTransID; }
public void setThreeDSServerTransID(String threeDSServerTransID) { this.threeDSServerTransID = threeDSServerTransID; }
public String getAcsTransID() { return acsTransID; }
public void setAcsTransID(String acsTransID) { this.acsTransID = acsTransID; }
public String getChallengeWindowSize() { return challengeWindowSize; }
public void setChallengeWindowSize(String challengeWindowSize) { this.challengeWindowSize = challengeWindowSize; }
public String getMessageType() { return messageType; }
public void setMessageType(String messageType) { this.messageType = messageType; }
public String getMessageVersion() { return messageVersion; }
public void setMessageVersion(String messageVersion) { this.messageVersion = messageVersion; }
}
// Transaction result
public class TransactionResult {
private boolean success;
private String transactionId;
private String status;
private String eci;
private String cavv;
private String xid;
private String errorCode;
private String errorMessage;
private LocalDateTime timestamp;
// Constructors
public TransactionResult() {
this.timestamp = LocalDateTime.now();
}
public TransactionResult(boolean success, String transactionId, String status) {
this();
this.success = success;
this.transactionId = transactionId;
this.status = status;
}
// Getters and setters
public boolean isSuccess() { return success; }
public void setSuccess(boolean success) { this.success = success; }
public String getTransactionId() { return transactionId; }
public void setTransactionId(String transactionId) { this.transactionId = transactionId; }
public String getStatus() { return status; }
public void setStatus(String status) { this.status = status; }
public String getEci() { return eci; }
public void setEci(String eci) { this.eci = eci; }
public String getCavv() { return cavv; }
public void setCavv(String cavv) { this.cavv = cavv; }
public String getXid() { return xid; }
public void setXid(String xid) { this.xid = xid; }
public String getErrorCode() { return errorCode; }
public void setErrorCode(String errorCode) { this.errorCode = errorCode; }
public String getErrorMessage() { return errorMessage; }
public void setErrorMessage(String errorMessage) { this.errorMessage = errorMessage; }
public LocalDateTime getTimestamp() { return timestamp; }
public void setTimestamp(LocalDateTime timestamp) { this.timestamp = timestamp; }
}

Example 3: Core 3D Secure Service

package com.securepay.threedsecure.service;
import com.securepay.threedsecure.config.ThreeDSecureConfig;
import com.securepay.threedsecure.model.*;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.SecureRandom;
import java.time.LocalDateTime;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;
@Service
public class ThreeDSecureService {
private static final Logger logger = LoggerFactory.getLogger(ThreeDSecureService.class);
private final ThreeDSecureConfig config;
private final ObjectMapper objectMapper;
private final CloseableHttpClient httpClient;
@Autowired
public ThreeDSecureService(ThreeDSecureConfig config, ObjectMapper objectMapper) {
this.config = config;
this.objectMapper = objectMapper;
this.httpClient = HttpClients.createDefault();
}
/**
* Initiate 3D Secure authentication
*/
public AuthenticationResponse initiateAuthentication(AuthenticationRequest authRequest) {
try {
logger.info("Initiating 3DS authentication for transaction: {}", 
authRequest.getThreeDSServerTransID());
// Generate authentication request
authRequest.setNotificationURL(config.getNotificationUrl());
authRequest.setMerchantID(config.getMerchantId());
authRequest.setMerchantName(config.getMerchantName());
// Send request to 3DS server
String requestBody = objectMapper.writeValueAsString(authRequest);
Map<String, String> headers = createAuthHeaders(requestBody);
HttpPost httpPost = new HttpPost(config.getApiUrl() + "/api/v2/authenticate");
httpPost.setHeader("Content-Type", "application/json");
headers.forEach(httpPost::setHeader);
httpPost.setEntity(new StringEntity(requestBody, StandardCharsets.UTF_8));
try (CloseableHttpResponse response = httpClient.execute(httpPost)) {
String responseBody = EntityUtils.toString(response.getEntity());
if (response.getStatusLine().getStatusCode() == 200) {
AuthenticationResponse authResponse = 
objectMapper.readValue(responseBody, AuthenticationResponse.class);
logger.info("3DS authentication response received. Status: {}", 
authResponse.getTransStatus());
return authResponse;
} else {
logger.error("3DS authentication failed. HTTP Status: {}, Response: {}", 
response.getStatusLine().getStatusCode(), responseBody);
throw new ThreeDSecureException("Authentication request failed", responseBody);
}
}
} catch (Exception e) {
logger.error("Error initiating 3DS authentication", e);
throw new ThreeDSecureException("Authentication initiation failed", e);
}
}
/**
* Process challenge request
*/
public AuthenticationResponse processChallenge(ChallengeRequest challengeRequest) {
try {
logger.info("Processing challenge for transaction: {}", 
challengeRequest.getThreeDSServerTransID());
String requestBody = objectMapper.writeValueAsString(challengeRequest);
Map<String, String> headers = createAuthHeaders(requestBody);
HttpPost httpPost = new HttpPost(config.getApiUrl() + "/api/v2/challenge");
httpPost.setHeader("Content-Type", "application/json");
headers.forEach(httpPost::setHeader);
httpPost.setEntity(new StringEntity(requestBody, StandardCharsets.UTF_8));
try (CloseableHttpResponse response = httpClient.execute(httpPost)) {
String responseBody = EntityUtils.toString(response.getEntity());
if (response.getStatusLine().getStatusCode() == 200) {
AuthenticationResponse challengeResponse = 
objectMapper.readValue(responseBody, AuthenticationResponse.class);
logger.info("Challenge response received. Status: {}", 
challengeResponse.getTransStatus());
return challengeResponse;
} else {
logger.error("Challenge processing failed. HTTP Status: {}, Response: {}", 
response.getStatusLine().getStatusCode(), responseBody);
throw new ThreeDSecureException("Challenge processing failed", responseBody);
}
}
} catch (Exception e) {
logger.error("Error processing challenge", e);
throw new ThreeDSecureException("Challenge processing failed", e);
}
}
/**
* Complete authentication and get final result
*/
public TransactionResult completeAuthentication(String threeDSServerTransID) {
try {
logger.info("Completing authentication for transaction: {}", threeDSServerTransID);
Map<String, String> request = new HashMap<>();
request.put("threeDSServerTransID", threeDSServerTransID);
String requestBody = objectMapper.writeValueAsString(request);
Map<String, String> headers = createAuthHeaders(requestBody);
HttpPost httpPost = new HttpPost(config.getApiUrl() + "/api/v2/complete");
httpPost.setHeader("Content-Type", "application/json");
headers.forEach(httpPost::setHeader);
httpPost.setEntity(new StringEntity(requestBody, StandardCharsets.UTF_8));
try (CloseableHttpResponse response = httpClient.execute(httpPost)) {
String responseBody = EntityUtils.toString(response.getEntity());
if (response.getStatusLine().getStatusCode() == 200) {
AuthenticationResponse finalResponse = 
objectMapper.readValue(responseBody, AuthenticationResponse.class);
TransactionResult result = createTransactionResult(finalResponse);
logger.info("Authentication completed. Final status: {}", result.getStatus());
return result;
} else {
logger.error("Authentication completion failed. HTTP Status: {}, Response: {}", 
response.getStatusLine().getStatusCode(), responseBody);
throw new ThreeDSecureException("Authentication completion failed", responseBody);
}
}
} catch (Exception e) {
logger.error("Error completing authentication", e);
throw new ThreeDSecureException("Authentication completion failed", e);
}
}
/**
* Generate unique transaction ID
*/
public String generateTransactionId() {
SecureRandom random = new SecureRandom();
byte[] bytes = new byte[16];
random.nextBytes(bytes);
return Base64.getUrlEncoder().withoutPadding().encodeToString(bytes);
}
/**
* Validate authentication response
*/
public boolean validateAuthentication(AuthenticationResponse response) {
if (response == null) {
return false;
}
// Check if response has required fields
if (response.getTransStatus() == null || response.getThreeDSServerTransID() == null) {
return false;
}
// Validate status
String status = response.getTransStatus();
return status.equals(ThreeDSConstants.STATUS_AUTHENTICATION_SUCCESSFUL) ||
status.equals(ThreeDSConstants.STATUS_AUTHENTICATION_ATTEMPTED) ||
status.equals(ThreeDSConstants.STATUS_CHALLENGE_REQUIRED);
}
/**
* Create authentication headers with HMAC signature
*/
private Map<String, String> createAuthHeaders(String requestBody) {
Map<String, String> headers = new HashMap<>();
try {
String timestamp = String.valueOf(System.currentTimeMillis());
String nonce = generateNonce();
String dataToSign = config.getApiKey() + timestamp + nonce + requestBody;
String signature = generateHmacSignature(dataToSign, config.getApiSecret());
headers.put("X-API-Key", config.getApiKey());
headers.put("X-Timestamp", timestamp);
headers.put("X-Nonce", nonce);
headers.put("X-Signature", signature);
} catch (Exception e) {
logger.error("Error creating authentication headers", e);
throw new ThreeDSecureException("Header generation failed", e);
}
return headers;
}
/**
* Generate HMAC signature
*/
private String generateHmacSignature(String data, String secret) {
try {
Mac mac = Mac.getInstance("HmacSHA256");
SecretKeySpec secretKeySpec = new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256");
mac.init(secretKeySpec);
byte[] signatureBytes = mac.doFinal(data.getBytes(StandardCharsets.UTF_8));
return Base64.getEncoder().encodeToString(signatureBytes);
} catch (Exception e) {
throw new ThreeDSecureException("HMAC signature generation failed", e);
}
}
/**
* Generate random nonce
*/
private String generateNonce() {
SecureRandom random = new SecureRandom();
byte[] nonceBytes = new byte[16];
random.nextBytes(nonceBytes);
return Base64.getUrlEncoder().withoutPadding().encodeToString(nonceBytes);
}
/**
* Create transaction result from authentication response
*/
private TransactionResult createTransactionResult(AuthenticationResponse response) {
TransactionResult result = new TransactionResult();
result.setTransactionId(response.getThreeDSServerTransID());
result.setStatus(response.getTransStatus());
result.setEci(response.getEci());
result.setCavv(response.getAuthenticationValue());
// Determine success based on transaction status
boolean isSuccess = response.getTransStatus().equals(ThreeDSConstants.STATUS_AUTHENTICATION_SUCCESSFUL) ||
response.getTransStatus().equals(ThreeDSConstants.STATUS_AUTHENTICATION_ATTEMPTED);
result.setSuccess(isSuccess);
if (!isSuccess && response.getErrorCode() != null) {
result.setErrorCode(response.getErrorCode());
result.setErrorMessage(response.getErrorDescription());
}
return result;
}
}
// Custom exception for 3DS errors
class ThreeDSecureException extends RuntimeException {
private final String errorDetails;
public ThreeDSecureException(String message) {
super(message);
this.errorDetails = null;
}
public ThreeDSecureException(String message, String errorDetails) {
super(message);
this.errorDetails = errorDetails;
}
public ThreeDSecureException(String message, Throwable cause) {
super(message, cause);
this.errorDetails = null;
}
public String getErrorDetails() {
return errorDetails;
}
}

Example 4: REST Controller

package com.securepay.threedsecure.controller;
import com.securepay.threedsecure.model.*;
import com.securepay.threedsecure.service.ThreeDSecureService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import javax.validation.Valid;
import java.math.BigDecimal;
import java.util.HashMap;
import java.util.Map;
@RestController
@RequestMapping("/api/3ds")
public class ThreeDSecureController {
private final ThreeDSecureService threeDSecureService;
@Autowired
public ThreeDSecureController(ThreeDSecureService threeDSecureService) {
this.threeDSecureService = threeDSecureService;
}
/**
* Initiate 3D Secure authentication
*/
@PostMapping("/initiate")
public ResponseEntity<?> initiateAuthentication(@Valid @RequestBody InitiateAuthRequest request) {
try {
// Generate transaction ID
String transactionId = threeDSecureService.generateTransactionId();
// Create authentication request
AuthenticationRequest authRequest = new AuthenticationRequest(
transactionId,
request.getCardNumber(),
request.getMerchantId(),
request.getMerchantName(),
request.getAmount(),
request.getCurrency()
);
// Set additional browser information
authRequest.setBrowserUserAgent(request.getUserAgent());
authRequest.setBrowserAcceptHeader(request.getAcceptHeader());
authRequest.setBrowserScreenHeight(request.getScreenHeight());
authRequest.setBrowserScreenWidth(request.getScreenWidth());
// Initiate authentication
AuthenticationResponse response = threeDSecureService.initiateAuthentication(authRequest);
Map<String, Object> result = new HashMap<>();
result.put("success", true);
result.put("transactionId", transactionId);
result.put("authenticationResponse", response);
// Check if challenge is required
if (ThreeDSConstants.STATUS_CHALLENGE_REQUIRED.equals(response.getTransStatus())) {
result.put("challengeRequired", true);
result.put("acsUrl", response.getAcsURL());
result.put("creq", response.getCreq());
} else {
result.put("challengeRequired", false);
}
return ResponseEntity.ok(result);
} catch (Exception e) {
return ResponseEntity.badRequest().body(createErrorResponse(e));
}
}
/**
* Process challenge response
*/
@PostMapping("/challenge")
public ResponseEntity<?> processChallenge(@Valid @RequestBody ChallengeRequest request) {
try {
AuthenticationResponse response = threeDSecureService.processChallenge(request);
Map<String, Object> result = new HashMap<>();
result.put("success", true);
result.put("authenticationResponse", response);
return ResponseEntity.ok(result);
} catch (Exception e) {
return ResponseEntity.badRequest().body(createErrorResponse(e));
}
}
/**
* Complete authentication
*/
@PostMapping("/complete")
public ResponseEntity<?> completeAuthentication(@RequestBody CompleteAuthRequest request) {
try {
TransactionResult result = threeDSecureService.completeAuthentication(request.getTransactionId());
Map<String, Object> response = new HashMap<>();
response.put("success", result.isSuccess());
response.put("transactionResult", result);
return ResponseEntity.ok(response);
} catch (Exception e) {
return ResponseEntity.badRequest().body(createErrorResponse(e));
}
}
/**
* Webhook for 3DS server notifications
*/
@PostMapping("/webhook/notification")
public ResponseEntity<?> handleNotification(@RequestBody Map<String, Object> notification) {
try {
logger.info("Received 3DS notification: {}", notification);
// Process notification (store in database, update transaction status, etc.)
processNotification(notification);
return ResponseEntity.ok().build();
} catch (Exception e) {
logger.error("Error processing 3DS notification", e);
return ResponseEntity.badRequest().build();
}
}
/**
* Validate card for 3DS
*/
@PostMapping("/validate-card")
public ResponseEntity<?> validateCard(@RequestBody CardValidationRequest request) {
try {
// Basic card validation
boolean isValid = validateCardNumber(request.getCardNumber());
boolean supports3DS = check3DSSupport(request.getCardNumber());
Map<String, Object> response = new HashMap<>();
response.put("valid", isValid);
response.put("supports3ds", supports3DS);
response.put("cardScheme", detectCardScheme(request.getCardNumber()));
return ResponseEntity.ok(response);
} catch (Exception e) {
return ResponseEntity.badRequest().body(createErrorResponse(e));
}
}
private Map<String, Object> createErrorResponse(Exception e) {
Map<String, Object> errorResponse = new HashMap<>();
errorResponse.put("success", false);
errorResponse.put("error", e.getMessage());
if (e instanceof ThreeDSecureException) {
String errorDetails = ((ThreeDSecureException) e).getErrorDetails();
if (errorDetails != null) {
errorResponse.put("errorDetails", errorDetails);
}
}
return errorResponse;
}
private void processNotification(Map<String, Object> notification) {
// Implement notification processing logic
// This could include updating database records, sending emails, etc.
logger.info("Processing 3DS notification: {}", notification);
}
private boolean validateCardNumber(String cardNumber) {
// Implement Luhn algorithm for card validation
return cardNumber != null && cardNumber.matches("\\d{13,19}");
}
private boolean check3DSSupport(String cardNumber) {
// Check if card scheme supports 3DS
// This would typically involve checking against a database or external service
String scheme = detectCardScheme(cardNumber);
return scheme != null && !"unknown".equals(scheme);
}
private String detectCardScheme(String cardNumber) {
if (cardNumber == null) return "unknown";
if (cardNumber.startsWith("4")) return "visa";
if (cardNumber.startsWith("5")) return "mastercard";
if (cardNumber.startsWith("34") || cardNumber.startsWith("37")) return "amex";
if (cardNumber.startsWith("6")) return "discover";
return "unknown";
}
}
// Request DTOs
class InitiateAuthRequest {
private String cardNumber;
private String merchantId;
private String merchantName;
private BigDecimal amount;
private String currency;
private String userAgent;
private String acceptHeader;
private String screenHeight;
private String screenWidth;
// Getters and setters
public String getCardNumber() { return cardNumber; }
public void setCardNumber(String cardNumber) { this.cardNumber = cardNumber; }
public String getMerchantId() { return merchantId; }
public void setMerchantId(String merchantId) { this.merchantId = merchantId; }
public String getMerchantName() { return merchantName; }
public void setMerchantName(String merchantName) { this.merchantName = merchantName; }
public BigDecimal getAmount() { return amount; }
public void setAmount(BigDecimal amount) { this.amount = amount; }
public String getCurrency() { return currency; }
public void setCurrency(String currency) { this.currency = currency; }
public String getUserAgent() { return userAgent; }
public void setUserAgent(String userAgent) { this.userAgent = userAgent; }
public String getAcceptHeader() { return acceptHeader; }
public void setAcceptHeader(String acceptHeader) { this.acceptHeader = acceptHeader; }
public String getScreenHeight() { return screenHeight; }
public void setScreenHeight(String screenHeight) { this.screenHeight = screenHeight; }
public String getScreenWidth() { return screenWidth; }
public void setScreenWidth(String screenWidth) { this.screenWidth = screenWidth; }
}
class CompleteAuthRequest {
private String transactionId;
public String getTransactionId() { return transactionId; }
public void setTransactionId(String transactionId) { this.transactionId = transactionId; }
}
class CardValidationRequest {
private String cardNumber;
public String getCardNumber() { return cardNumber; }
public void setCardNumber(String cardNumber) { this.cardNumber = cardNumber; }
}

Example 5: Configuration and Security

package com.securepay.threedsecure.config;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.web.client.RestTemplate;
import java.time.Duration;
@Configuration
public class AppConfig {
@Bean
public ObjectMapper objectMapper() {
ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(new JavaTimeModule());
return mapper;
}
@Bean
public RestTemplate restTemplate(RestTemplateBuilder builder, ThreeDSecureConfig config) {
CloseableHttpClient httpClient = HttpClientBuilder.create()
.setMaxConnTotal(100)
.setMaxConnPerRoute(20)
.build();
HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory();
factory.setHttpClient(httpClient);
factory.setConnectTimeout(Duration.ofSeconds(config.getTimeoutSeconds()));
factory.setReadTimeout(Duration.ofSeconds(config.getTimeoutSeconds()));
return builder
.requestFactory(() -> factory)
.build();
}
}
// Security configuration
@Configuration
@EnableWebSecurity
class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
.authorizeRequests()
.antMatchers("/api/3ds/**").permitAll() // Public endpoints for 3DS
.antMatchers("/health").permitAll()
.anyRequest().authenticated()
.and()
.httpBasic();
}
}

Application Properties

# application.properties
threedsecure.api-url=https://api.3dsecure-provider.com
threedsecure.api-key=your-api-key
threedsecure.api-secret=your-api-secret
threedsecure.merchant-id=MERCHANT123
threedsecure.merchant-name=Your Store Name
threedsecure.notification-url=https://your-domain.com/api/3ds/webhook/notification
threedsecure.timeout-seconds=30
# Logging
logging.level.com.securepay.threedsecure=DEBUG
logging.level.org.apache.http=DEBUG
# Server
server.port=8080
server.servlet.context-path=/threedsecure

Testing the Implementation

Example 6: Test Cases

package com.securepay.threedsecure.test;
import com.securepay.threedsecure.model.*;
import com.securepay.threedsecure.service.ThreeDSecureService;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import java.math.BigDecimal;
import static org.junit.jupiter.api.Assertions.*;
@SpringBootTest
class ThreeDSecureServiceTest {
@Autowired
private ThreeDSecureService threeDSecureService;
@Test
void testGenerateTransactionId() {
String transactionId = threeDSecureService.generateTransactionId();
assertNotNull(transactionId);
assertTrue(transactionId.length() > 0);
}
@Test
void testValidateAuthenticationResponse() {
AuthenticationResponse validResponse = new AuthenticationResponse();
validResponse.setTransStatus("Y");
validResponse.setThreeDSServerTransID("test-trans-id");
assertTrue(threeDSecureService.validateAuthentication(validResponse));
AuthenticationResponse invalidResponse = new AuthenticationResponse();
assertFalse(threeDSecureService.validateAuthentication(invalidResponse));
}
@Test
void testCreateAuthenticationRequest() {
AuthenticationRequest request = new AuthenticationRequest(
"test-trans-id",
"4111111111111111",
"MERCHANT123",
"Test Merchant",
new BigDecimal("100.00"),
"USD"
);
assertNotNull(request);
assertEquals("4111111111111111", request.getAcctNumber());
assertEquals("USD", request.getPurchaseCurrency());
}
}

Best Practices and Security Considerations

Security Best Practices

package com.securepay.threedsecure.security;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import javax.crypto.Cipher;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.SecureRandom;
import java.util.Base64;
@Component
public class SecurityUtils {
private static final Logger logger = LoggerFactory.getLogger(SecurityUtils.class);
private static final String ALGORITHM = "AES/GCM/NoPadding";
private static final int TAG_LENGTH_BIT = 128;
private static final int IV_LENGTH_BYTE = 12;
/**
* Encrypt sensitive data (like card numbers)
*/
public String encrypt(String data, String secretKey) {
try {
byte[] iv = new byte[IV_LENGTH_BYTE];
new SecureRandom().nextBytes(iv);
Cipher cipher = Cipher.getInstance(ALGORITHM);
SecretKeySpec keySpec = new SecretKeySpec(secretKey.getBytes(StandardCharsets.UTF_8), "AES");
GCMParameterSpec gcmSpec = new GCMParameterSpec(TAG_LENGTH_BIT, iv);
cipher.init(Cipher.ENCRYPT_MODE, keySpec, gcmSpec);
byte[] encryptedData = cipher.doFinal(data.getBytes(StandardCharsets.UTF_8));
byte[] combined = new byte[iv.length + encryptedData.length];
System.arraycopy(iv, 0, combined, 0, iv.length);
System.arraycopy(encryptedData, 0, combined, iv.length, encryptedData.length);
return Base64.getEncoder().encodeToString(combined);
} catch (Exception e) {
logger.error("Encryption failed", e);
throw new RuntimeException("Encryption failed", e);
}
}
/**
* Decrypt sensitive data
*/
public String decrypt(String encryptedData, String secretKey) {
try {
byte[] combined = Base64.getDecoder().decode(encryptedData);
byte[] iv = new byte[IV_LENGTH_BYTE];
byte[] encrypted = new byte[combined.length - IV_LENGTH_BYTE];
System.arraycopy(combined, 0, iv, 0, IV_LENGTH_BYTE);
System.arraycopy(combined, IV_LENGTH_BYTE, encrypted, 0, encrypted.length);
Cipher cipher = Cipher.getInstance(ALGORITHM);
SecretKeySpec keySpec = new SecretKeySpec(secretKey.getBytes(StandardCharsets.UTF_8), "AES");
GCMParameterSpec gcmSpec = new GCMParameterSpec(TAG_LENGTH_BIT, iv);
cipher.init(Cipher.DECRYPT_MODE, keySpec, gcmSpec);
byte[] decryptedData = cipher.doFinal(encrypted);
return new String(decryptedData, StandardCharsets.UTF_8);
} catch (Exception e) {
logger.error("Decryption failed", e);
throw new RuntimeException("Decryption failed", e);
}
}
/**
* Mask card number for logging
*/
public String maskCardNumber(String cardNumber) {
if (cardNumber == null || cardNumber.length() < 8) {
return "****";
}
String firstFour = cardNumber.substring(0, 4);
String lastFour = cardNumber.substring(cardNumber.length() - 4);
return firstFour + "****" + lastFour;
}
}

Conclusion

This comprehensive 3D Secure implementation provides:

  • Complete 3DS2 Flow: Authentication initiation, challenge handling, and completion
  • Security: HMAC signatures, encryption, and secure communication
  • Error Handling: Comprehensive exception handling and logging
  • Validation: Input validation and response verification
  • Extensibility: Modular design for easy extension

Key Integration Points:

  1. Payment Gateway: Integrate with your payment processor
  2. Frontend: Handle challenge iframes and user interaction
  3. Database: Store transaction results and authentication data
  4. Monitoring: Track success rates and error patterns
  5. Compliance: Ensure PCI DSS compliance for card data handling

Production Considerations:

  • Use HTTPS for all communications
  • Implement proper key management
  • Add rate limiting and fraud detection
  • Monitor 3DS success rates and fallback strategies
  • Keep dependencies updated for security patches
  • Implement comprehensive logging and monitoring
  • Conduct regular security audits and penetration testing

This implementation provides a solid foundation for 3D Secure integration that can be customized for specific payment processors and business requirements.

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper