Strong Customer Authentication (SCA) in Java: Complete Guide

Introduction to Strong Customer Authentication

Strong Customer Authentication (SCA) is a requirement of PSD2 that mandates multi-factor authentication for electronic payments. It requires authentication based on two or more independent elements from the categories: knowledge (something only the user knows), possession (something only the user possesses), and inherence (something the user is).


System Architecture Overview

Strong Customer Authentication Architecture
├── Authentication Factors
│   ├── Knowledge (PIN, Password, Pattern)
│   ├── Possession (OTP, Token, Mobile Device)
│   └── Inherence (Fingerprint, Face, Voice)
├── SCA Flow
│   ├── Transaction Initiation
│   ├── Challenge Generation
│   ├── Factor Verification
│   ├── Dynamic Linking
│   └── Authorization Result
├── Security Features
│   ├── Dynamic Linking (Transaction Data)
│   ├── Challenge-Response
│   ├── Time-based OTP (TOTP)
│   ├── Push Notifications
│   └── Biometric Verification
└── Compliance
├── PSD2 Regulatory Requirements
├── EBA Guidelines
├── Audit Trail
└── Risk-Based Analysis

Core Implementation

1. Maven Dependencies

<properties>
<spring.boot.version>3.2.0</spring.boot.version>
<bouncycastle.version>1.78</bouncycastle.version>
<zxing.version>3.5.2</zxing.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-data-redis</artifactId>
<version>${spring.boot.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
<version>${spring.boot.version}</version>
</dependency>
<!-- Bouncy Castle for crypto -->
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk18on</artifactId>
<version>${bouncycastle.version}</version>
</dependency>
<!-- ZXing for QR codes -->
<dependency>
<groupId>com.google.zxing</groupId>
<artifactId>core</artifactId>
<version>${zxing.version}</version>
</dependency>
<dependency>
<groupId>com.google.zxing</groupId>
<artifactId>javase</artifactId>
<version>${zxing.version}</version>
</dependency>
<!-- JWT for tokens -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.11.5</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.30</version>
<scope>provided</scope>
</dependency>
<!-- Testing -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<version>${spring.boot.version}</version>
<scope>test</scope>
</dependency>
</dependencies>

2. SCA Core Service

package com.sca.core;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import java.security.SecureRandom;
import java.time.Duration;
import java.time.LocalDateTime;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
@Slf4j
@Service
public class SCACoreService {
private final RedisTemplate<String, String> redisTemplate;
private final ObjectMapper objectMapper;
private final SecureRandom secureRandom = new SecureRandom();
private final Map<String, SCASession> sessionCache = new ConcurrentHashMap<>();
public SCACoreService(RedisTemplate<String, String> redisTemplate) {
this.redisTemplate = redisTemplate;
this.objectMapper = new ObjectMapper();
}
/**
* Authentication factors
*/
public enum AuthenticationFactor {
KNOWLEDGE_PIN,
KNOWLEDGE_PASSWORD,
KNOWLEDGE_PATTERN,
POSSESSION_OTP_SMS,
POSSESSION_OTP_APP,
POSSESSION_HARDWARE_TOKEN,
POSSESSION_PUSH,
INHERENCE_FINGERPRINT,
INHERENCE_FACE,
INHERENCE_VOICE,
INHERENCE_IRIS
}
/**
* SCA session status
*/
public enum SCAStatus {
PENDING,
CHALLENGE_SENT,
AWAITING_VALIDATION,
VERIFIED,
FAILED,
EXPIRED,
BLOCKED
}
/**
* SCA session
*/
@Data
@Builder
public static class SCASession {
private String scaId;
private String userId;
private String transactionId;
private BigDecimal transactionAmount;
private String currency;
private String creditorAccount;
private String debtorAccount;
private String merchantName;
private LocalDateTime timestamp;
private SCAStatus status;
private List<AuthenticationFactor> requiredFactors;
private List<AuthenticationFactor> completedFactors;
private Map<AuthenticationFactor, String> challenges;
private Map<AuthenticationFactor, LocalDateTime> challengeExpiry;
private int retryCount;
private int maxRetries;
private LocalDateTime expiresAt;
private String dynamicLinkingData;
private String riskScore;
private Map<String, Object> metadata;
}
/**
* SCA request
*/
@Data
@Builder
public static class SCARequest {
private String userId;
private String transactionId;
private BigDecimal amount;
private String currency;
private String creditorAccount;
private String debtorAccount;
private String merchantName;
private List<AuthenticationFactor> requiredFactors;
private String riskScore;
private Map<String, Object> context;
}
/**
* Initiate SCA session
*/
public SCASession initiateSCA(SCARequest request) {
String scaId = generateSCAId();
LocalDateTime now = LocalDateTime.now();
SCASession session = SCASession.builder()
.scaId(scaId)
.userId(request.getUserId())
.transactionId(request.getTransactionId())
.transactionAmount(request.getAmount())
.currency(request.getCurrency())
.creditorAccount(request.getCreditorAccount())
.debtorAccount(request.getDebtorAccount())
.merchantName(request.getMerchantName())
.timestamp(now)
.status(SCAStatus.PENDING)
.requiredFactors(request.getRequiredFactors())
.completedFactors(new ArrayList<>())
.challenges(new HashMap<>())
.challengeExpiry(new HashMap<>())
.retryCount(0)
.maxRetries(3)
.expiresAt(now.plusMinutes(10))
.dynamicLinkingData(generateDynamicLinkingData(request))
.riskScore(request.getRiskScore())
.metadata(request.getContext() != null ? request.getContext() : new HashMap<>())
.build();
// Generate challenges for required factors
generateChallenges(session);
// Store session
storeSession(session);
log.info("SCA session initiated: {} for user: {}", scaId, request.getUserId());
return session;
}
/**
* Verify authentication factor
*/
public SCAVerificationResult verifyFactor(String scaId, 
AuthenticationFactor factor,
String response) {
SCASession session = getSession(scaId);
if (session == null) {
return SCAVerificationResult.failure("Session not found");
}
// Check session status
if (session.getStatus() != SCAStatus.AWAITING_VALIDATION && 
session.getStatus() != SCAStatus.CHALLENGE_SENT) {
return SCAVerificationResult.failure("Invalid session state");
}
// Check expiration
if (session.getExpiresAt().isBefore(LocalDateTime.now())) {
session.setStatus(SCAStatus.EXPIRED);
updateSession(session);
return SCAVerificationResult.failure("Session expired");
}
// Check retry count
if (session.getRetryCount() >= session.getMaxRetries()) {
session.setStatus(SCAStatus.BLOCKED);
updateSession(session);
return SCAVerificationResult.failure("Maximum retries exceeded");
}
// Get challenge
String challenge = session.getChallenges().get(factor);
LocalDateTime expiry = session.getChallengeExpiry().get(factor);
// Check challenge expiry
if (expiry != null && expiry.isBefore(LocalDateTime.now())) {
session.setRetryCount(session.getRetryCount() + 1);
updateSession(session);
return SCAVerificationResult.failure("Challenge expired");
}
// Verify response
boolean verified = verifyResponse(factor, challenge, response, session);
if (verified) {
// Mark factor as completed
session.getCompletedFactors().add(factor);
// Check if all required factors are completed
if (session.getCompletedFactors().containsAll(session.getRequiredFactors())) {
session.setStatus(SCAStatus.VERIFIED);
log.info("SCA completed successfully: {}", scaId);
} else {
session.setStatus(SCAStatus.AWAITING_VALIDATION);
}
updateSession(session);
return SCAVerificationResult.success(
session.getStatus() == SCAStatus.VERIFIED,
session.getCompletedFactors()
);
} else {
session.setRetryCount(session.getRetryCount() + 1);
updateSession(session);
return SCAVerificationResult.failure("Invalid response");
}
}
/**
* Get SCA session
*/
public SCASession getSession(String scaId) {
// Try cache first
SCASession session = sessionCache.get(scaId);
if (session == null) {
// Try Redis
String json = redisTemplate.opsForValue().get("sca:session:" + scaId);
if (json != null) {
try {
session = objectMapper.readValue(json, SCASession.class);
sessionCache.put(scaId, session);
} catch (Exception e) {
log.error("Failed to deserialize session", e);
}
}
}
return session;
}
/**
* Determine required SCA factors based on transaction risk
*/
public List<AuthenticationFactor> determineRequiredFactors(SCARequest request) {
List<AuthenticationFactor> factors = new ArrayList<>();
// PSD2 requirements: At least two factors from different categories
// First factor - always knowledge
factors.add(AuthenticationFactor.KNOWLEDGE_PIN);
// Second factor based on availability and risk
if (isHighRiskTransaction(request)) {
// For high-risk, use possession + inherence
factors.add(AuthenticationFactor.POSSESSION_OTP_APP);
factors.add(AuthenticationFactor.INHERENCE_FINGERPRINT);
} else {
// For normal risk, just possession
factors.add(AuthenticationFactor.POSSESSION_OTP_SMS);
}
return factors;
}
/**
* Check if SCA is required for transaction
*/
public boolean isSCARequired(SCARequest request) {
// PSD2 SCA requirements:
// 1. Transaction amount > 30 EUR
// 2. High-risk transaction
// 3. First transaction with new merchant
// 4. Unusual transaction pattern
if (request.getAmount().compareTo(new BigDecimal("30.00")) > 0) {
return true;
}
if (isHighRiskTransaction(request)) {
return true;
}
// Additional checks based on PSD2
return false;
}
/**
* Apply exemptions (low-value, trusted beneficiaries, etc.)
*/
public boolean applyExemption(SCARequest request) {
// PSD2 exemptions:
// 1. Low-value transactions (< 30 EUR)
// 2. Trusted beneficiaries
// 3. Recurring transactions
// 4. Corporate payments
if (request.getAmount().compareTo(new BigDecimal("30.00")) <= 0) {
return true;
}
// Check if beneficiary is trusted
if (isTrustedBeneficiary(request)) {
return true;
}
return false;
}
/**
* Generate challenge for authentication factor
*/
private void generateChallenges(SCASession session) {
for (AuthenticationFactor factor : session.getRequiredFactors()) {
String challenge = generateChallenge(factor, session);
session.getChallenges().put(factor, challenge);
session.getChallengeExpiry().put(factor, 
LocalDateTime.now().plusMinutes(getChallengeExpiryMinutes(factor)));
}
session.setStatus(SCAStatus.CHALLENGE_SENT);
}
/**
* Generate challenge based on factor type
*/
private String generateChallenge(AuthenticationFactor factor, SCASession session) {
switch (factor) {
case KNOWLEDGE_PIN:
case KNOWLEDGE_PASSWORD:
return generateKnowledgeChallenge(session);
case POSSESSION_OTP_SMS:
case POSSESSION_OTP_APP:
return generateOTP(6);
case POSSESSION_HARDWARE_TOKEN:
return generateHardwareTokenChallenge();
case POSSESSION_PUSH:
return generatePushNotification(session);
case INHERENCE_FINGERPRINT:
case INHERENCE_FACE:
case INHERENCE_VOICE:
return generateBiometricChallenge(session);
default:
return UUID.randomUUID().toString();
}
}
/**
* Generate OTP
*/
private String generateOTP(int length) {
StringBuilder otp = new StringBuilder();
for (int i = 0; i < length; i++) {
otp.append(secureRandom.nextInt(10));
}
return otp.toString();
}
/**
* Generate TOTP (Time-based OTP)
*/
public String generateTOTP(String secret) {
// TOTP implementation (RFC 6238)
long timeWindow = System.currentTimeMillis() / 30000; // 30-second window
return generateHOTP(secret, timeWindow);
}
/**
* Generate HOTP (HMAC-based OTP)
*/
private String generateHOTP(String secret, long counter) {
try {
// HOTP implementation (RFC 4226)
byte[] counterBytes = new byte[8];
for (int i = 0; i < 8; i++) {
counterBytes[7 - i] = (byte) (counter >>> (i * 8));
}
javax.crypto.Mac mac = javax.crypto.Mac.getInstance("HmacSHA1");
javax.crypto.spec.SecretKeySpec keySpec = 
new javax.crypto.spec.SecretKeySpec(secret.getBytes(), "HmacSHA1");
mac.init(keySpec);
byte[] hash = mac.doFinal(counterBytes);
int offset = hash[hash.length - 1] & 0xF;
int binary = ((hash[offset] & 0x7F) << 24) |
((hash[offset + 1] & 0xFF) << 16) |
((hash[offset + 2] & 0xFF) << 8) |
(hash[offset + 3] & 0xFF);
int otp = binary % 1000000;
return String.format("%06d", otp);
} catch (Exception e) {
log.error("HOTP generation failed", e);
return null;
}
}
/**
* Generate knowledge-based challenge
*/
private String generateKnowledgeChallenge(SCASession session) {
// Could be a simple PIN request or security questions
return "Please enter your PIN";
}
/**
* Generate hardware token challenge
*/
private String generateHardwareTokenChallenge() {
// Generate challenge for hardware token (e.g., Card reader)
return UUID.randomUUID().toString().substring(0, 8);
}
/**
* Generate push notification
*/
private String generatePushNotification(SCASession session) {
// Generate push notification content
Map<String, Object> pushData = new HashMap<>();
pushData.put("transactionId", session.getTransactionId());
pushData.put("amount", session.getTransactionAmount());
pushData.put("merchant", session.getMerchantName());
pushData.put("timestamp", session.getTimestamp());
return pushData.toString();
}
/**
* Generate biometric challenge
*/
private String generateBiometricChallenge(SCASession session) {
// For biometrics, challenge is typically a random nonce
return UUID.randomUUID().toString();
}
/**
* Verify response
*/
private boolean verifyResponse(AuthenticationFactor factor,
String challenge,
String response,
SCASession session) {
switch (factor) {
case KNOWLEDGE_PIN:
case KNOWLEDGE_PASSWORD:
return verifyKnowledgeResponse(session.getUserId(), response);
case POSSESSION_OTP_SMS:
case POSSESSION_OTP_APP:
return challenge.equals(response);
case POSSESSION_HARDWARE_TOKEN:
return verifyHardwareTokenResponse(challenge, response);
case POSSESSION_PUSH:
return verifyPushResponse(session, response);
case INHERENCE_FINGERPRINT:
case INHERENCE_FACE:
case INHERENCE_VOICE:
return verifyBiometricResponse(challenge, response);
default:
return false;
}
}
/**
* Verify knowledge response (PIN/Password)
*/
private boolean verifyKnowledgeResponse(String userId, String response) {
// In production, validate against stored credentials
// Use proper password hashing (BCrypt, etc.)
return true; // Simplified
}
/**
* Verify hardware token response
*/
private boolean verifyHardwareTokenResponse(String challenge, String response) {
// Validate challenge-response from hardware token
return true; // Simplified
}
/**
* Verify push notification response
*/
private boolean verifyPushResponse(SCASession session, String response) {
// Push response typically contains approval/rejection
return "APPROVED".equals(response);
}
/**
* Verify biometric response
*/
private boolean verifyBiometricResponse(String challenge, String response) {
// Verify biometric signature/nonce
return true; // Simplified
}
/**
* Generate dynamic linking data (required by PSD2)
*/
private String generateDynamicLinkingData(SCARequest request) {
// Creates a unique hash linking the authentication to the transaction
String data = String.format("%s|%s|%s|%s|%s",
request.getTransactionId(),
request.getAmount(),
request.getCurrency(),
request.getCreditorAccount(),
request.getDebtorAccount()
);
try {
java.security.MessageDigest digest = 
java.security.MessageDigest.getInstance("SHA-256");
byte[] hash = digest.digest(data.getBytes());
return Base64.getEncoder().encodeToString(hash);
} catch (Exception e) {
log.error("Failed to generate dynamic linking data", e);
return null;
}
}
/**
* Check if transaction is high risk
*/
private boolean isHighRiskTransaction(SCARequest request) {
// Implement risk-based analysis
// Factors: amount, country, merchant, user behavior, etc.
if (request.getAmount().compareTo(new BigDecimal("500.00")) > 0) {
return true;
}
// Additional risk checks
return false;
}
/**
* Check if beneficiary is trusted
*/
private boolean isTrustedBeneficiary(SCARequest request) {
// Check against user's trusted beneficiaries list
return false; // Simplified
}
/**
* Get challenge expiry in minutes
*/
private int getChallengeExpiryMinutes(AuthenticationFactor factor) {
switch (factor) {
case POSSESSION_OTP_SMS:
case POSSESSION_OTP_APP:
return 5;
case POSSESSION_PUSH:
return 10;
default:
return 15;
}
}
/**
* Generate unique SCA ID
*/
private String generateSCAId() {
return "SCA-" + UUID.randomUUID().toString().substring(0, 8).toUpperCase();
}
/**
* Store session in Redis
*/
private void storeSession(SCASession session) {
try {
String json = objectMapper.writeValueAsString(session);
redisTemplate.opsForValue().set(
"sca:session:" + session.getScaId(),
json,
Duration.between(LocalDateTime.now(), session.getExpiresAt())
);
sessionCache.put(session.getScaId(), session);
} catch (Exception e) {
log.error("Failed to store session", e);
}
}
/**
* Update session
*/
private void updateSession(SCASession session) {
sessionCache.put(session.getScaId(), session);
storeSession(session);
}
/**
* SCA verification result
*/
@Data
public static class SCAVerificationResult {
private final boolean success;
private final boolean completed;
private final List<AuthenticationFactor> completedFactors;
private final String error;
private SCAVerificationResult(boolean success, boolean completed,
List<AuthenticationFactor> factors, String error) {
this.success = success;
this.completed = completed;
this.completedFactors = factors;
this.error = error;
}
public static SCAVerificationResult success(boolean completed,
List<AuthenticationFactor> factors) {
return new SCAVerificationResult(true, completed, factors, null);
}
public static SCAVerificationResult failure(String error) {
return new SCAVerificationResult(false, false, null, error);
}
public boolean isSuccess() { return success; }
public boolean isCompleted() { return completed; }
public List<AuthenticationFactor> getCompletedFactors() { return completedFactors; }
public String getError() { return error; }
}
}

3. SMS OTP Service

package com.sca.factors.sms;
import com.sca.core.SCACoreService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@Slf4j
@Service
public class SMSOTPService {
private final SCACoreService scaCoreService;
private final Map<String, SMSDeliveryStatus> deliveryStatus = new ConcurrentHashMap<>();
public SMSOTPService(SCACoreService scaCoreService) {
this.scaCoreService = scaCoreService;
}
/**
* SMS delivery status
*/
public static class SMSDeliveryStatus {
private final String phoneNumber;
private final String messageId;
private final boolean delivered;
private final long timestamp;
public SMSDeliveryStatus(String phoneNumber, String messageId, boolean delivered) {
this.phoneNumber = phoneNumber;
this.messageId = messageId;
this.delivered = delivered;
this.timestamp = System.currentTimeMillis();
}
public boolean isDelivered() { return delivered; }
public String getMessageId() { return messageId; }
public long getTimestamp() { return timestamp; }
}
/**
* Send OTP via SMS
*/
public boolean sendOTP(String scaId, String phoneNumber) {
SCACoreService.SCASession session = scaCoreService.getSession(scaId);
if (session == null) {
log.warn("Session not found: {}", scaId);
return false;
}
// Get OTP from session
String otp = session.getChallenges().get(SCACoreService.AuthenticationFactor.POSSESSION_OTP_SMS);
if (otp == null) {
log.warn("OTP not found in session: {}", scaId);
return false;
}
// Send SMS (mock implementation)
boolean sent = sendSMS(phoneNumber, otp, session);
if (sent) {
deliveryStatus.put(scaId, new SMSDeliveryStatus(phoneNumber, scaId + "-sms", true));
log.info("OTP sent to {} for session: {}", phoneNumber, scaId);
} else {
log.error("Failed to send OTP to {} for session: {}", phoneNumber, scaId);
}
return sent;
}
/**
* Resend OTP
*/
public boolean resendOTP(String scaId, String phoneNumber) {
// Check retry count
SCACoreService.SCASession session = scaCoreService.getSession(scaId);
if (session == null || session.getRetryCount() >= session.getMaxRetries()) {
return false;
}
// Generate new OTP (optional - could reuse existing)
return sendOTP(scaId, phoneNumber);
}
/**
* Verify OTP
*/
public boolean verifyOTP(String scaId, String otp) {
SCACoreService.SCAVerificationResult result = 
scaCoreService.verifyFactor(
scaId,
SCACoreService.AuthenticationFactor.POSSESSION_OTP_SMS,
otp
);
if (result.isSuccess()) {
log.info("OTP verified for session: {}", scaId);
return true;
} else {
log.warn("OTP verification failed for session: {} - {}", scaId, result.getError());
return false;
}
}
/**
* Get delivery status
*/
public SMSDeliveryStatus getDeliveryStatus(String scaId) {
return deliveryStatus.get(scaId);
}
/**
* Mock SMS sending
*/
private boolean sendSMS(String phoneNumber, String otp, SCACoreService.SCASession session) {
// In production, integrate with SMS provider (Twilio, etc.)
log.info("=== SMS OTP ===");
log.info("To: {}", phoneNumber);
log.info("OTP: {}", otp);
log.info("Transaction: {} {} {}", 
session.getTransactionAmount(), 
session.getCurrency(),
session.getMerchantName()
);
log.info("==============");
return true;
}
}

4. Push Notification Service

package com.sca.factors.push;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.sca.core.SCACoreService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
@Slf4j
@Service
public class PushNotificationService {
private final SCACoreService scaCoreService;
private final ObjectMapper objectMapper;
private final Map<String, PushRequest> pendingRequests = new ConcurrentHashMap<>();
public PushNotificationService(SCACoreService scaCoreService) {
this.scaCoreService = scaCoreService;
this.objectMapper = new ObjectMapper();
}
/**
* Push notification request
*/
public static class PushRequest {
private final String pushId;
private final String scaId;
private final String userId;
private final PushNotification notification;
private final long timestamp;
private CompletableFuture<PushResponse> future;
public PushRequest(String scaId, String userId, PushNotification notification) {
this.pushId = UUID.randomUUID().toString();
this.scaId = scaId;
this.userId = userId;
this.notification = notification;
this.timestamp = System.currentTimeMillis();
}
// getters
}
/**
* Push notification
*/
public static class PushNotification {
private final String title;
private final String body;
private final Map<String, Object> data;
public PushNotification(String title, String body, Map<String, Object> data) {
this.title = title;
this.body = body;
this.data = data;
}
// getters
}
/**
* Push response
*/
public static class PushResponse {
private final String pushId;
private final String action; // APPROVE, REJECT
private final long timestamp;
public PushResponse(String pushId, String action) {
this.pushId = pushId;
this.action = action;
this.timestamp = System.currentTimeMillis();
}
// getters
}
/**
* Send push notification for authentication
*/
public CompletableFuture<PushResponse> sendPushAuth(String scaId, String userId) {
SCACoreService.SCASession session = scaCoreService.getSession(scaId);
if (session == null) {
CompletableFuture<PushResponse> future = new CompletableFuture<>();
future.completeExceptionally(new RuntimeException("Session not found"));
return future;
}
// Create push notification
PushNotification notification = new PushNotification(
"Authorize Payment",
String.format("Authorize payment of %s %s to %s",
session.getTransactionAmount(),
session.getCurrency(),
session.getMerchantName()
),
Map.of(
"scaId", scaId,
"transactionId", session.getTransactionId(),
"amount", session.getTransactionAmount(),
"merchant", session.getMerchantName(),
"timestamp", session.getTimestamp().toString()
)
);
// Create push request
PushRequest request = new PushRequest(scaId, userId, notification);
// Create future for response
CompletableFuture<PushResponse> future = new CompletableFuture<>();
request.future = future;
// Store pending request
pendingRequests.put(request.pushId, request);
// Send push notification (mock)
sendPushNotification(userId, notification);
// Set timeout
future.orTimeout(120, TimeUnit.SECONDS)
.whenComplete((response, error) -> {
pendingRequests.remove(request.pushId);
if (error != null) {
log.warn("Push authentication timeout for session: {}", scaId);
}
});
return future;
}
/**
* Handle push response from mobile app
*/
public boolean handlePushResponse(String pushId, String action) {
PushRequest request = pendingRequests.get(pushId);
if (request == null) {
log.warn("Push request not found: {}", pushId);
return false;
}
PushResponse response = new PushResponse(pushId, action);
// Complete the future
request.future.complete(response);
// Verify with SCA service if approved
if ("APPROVE".equals(action)) {
SCACoreService.SCAVerificationResult result = 
scaCoreService.verifyFactor(
request.scaId,
SCACoreService.AuthenticationFactor.POSSESSION_PUSH,
"APPROVED"
);
return result.isSuccess();
}
return true;
}
/**
* Mock push notification sending
*/
private void sendPushNotification(String userId, PushNotification notification) {
log.info("=== PUSH NOTIFICATION ===");
log.info("To user: {}", userId);
log.info("Title: {}", notification.getTitle());
log.info("Body: {}", notification.getBody());
log.info("Data: {}", notification.getData());
log.info("=========================");
}
}

5. Biometric Authentication Service

package com.sca.factors.biometric;
import com.sca.core.SCACoreService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.security.PublicKey;
import java.security.Signature;
import java.util.Base64;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@Slf4j
@Service
public class BiometricAuthenticationService {
private final SCACoreService scaCoreService;
private final Map<String, BiometricChallenge> challenges = new ConcurrentHashMap<>();
public BiometricAuthenticationService(SCACoreService scaCoreService) {
this.scaCoreService = scaCoreService;
}
/**
* Biometric types
*/
public enum BiometricType {
FINGERPRINT,
FACE,
VOICE,
IRIS
}
/**
* Biometric challenge
*/
public static class BiometricChallenge {
private final String challengeId;
private final String scaId;
private final BiometricType type;
private final String nonce;
private final long timestamp;
private final long expiresAt;
public BiometricChallenge(String scaId, BiometricType type) {
this.challengeId = java.util.UUID.randomUUID().toString();
this.scaId = scaId;
this.type = type;
this.nonce = generateNonce();
this.timestamp = System.currentTimeMillis();
this.expiresAt = timestamp + 300000; // 5 minutes
}
public String getChallengeId() { return challengeId; }
public String getScaId() { return scaId; }
public BiometricType getType() { return type; }
public String getNonce() { return nonce; }
public boolean isExpired() { return System.currentTimeMillis() > expiresAt; }
}
/**
* Biometric verification result
*/
public static class BiometricVerificationResult {
private final boolean success;
private final float confidence;
private final String error;
public BiometricVerificationResult(boolean success, float confidence, String error) {
this.success = success;
this.confidence = confidence;
this.error = error;
}
public static BiometricVerificationResult success(float confidence) {
return new BiometricVerificationResult(true, confidence, null);
}
public static BiometricVerificationResult failure(String error) {
return new BiometricVerificationResult(false, 0, error);
}
// getters
}
/**
* Generate biometric challenge
*/
public BiometricChallenge generateChallenge(String scaId, BiometricType type) {
SCACoreService.SCASession session = scaCoreService.getSession(scaId);
if (session == null) {
return null;
}
BiometricChallenge challenge = new BiometricChallenge(scaId, type);
challenges.put(challenge.getChallengeId(), challenge);
log.info("Biometric challenge generated for session: {}, type: {}", scaId, type);
return challenge;
}
/**
* Verify biometric response
*/
public boolean verifyBiometric(String challengeId, byte[] biometricData, 
BiometricType type, PublicKey publicKey) {
BiometricChallenge challenge = challenges.get(challengeId);
if (challenge == null) {
log.warn("Challenge not found: {}", challengeId);
return false;
}
if (challenge.isExpired()) {
log.warn("Challenge expired: {}", challengeId);
return false;
}
try {
// Verify the biometric signature
boolean signatureValid = verifySignature(biometricData, publicKey, challenge.getNonce());
if (!signatureValid) {
log.warn("Invalid biometric signature for challenge: {}", challengeId);
return false;
}
// Perform biometric matching (mock)
BiometricVerificationResult result = matchBiometric(biometricData, type);
if (result.isSuccess() && result.getConfidence() > 0.95) {
// Verify with SCA service
SCACoreService.SCAVerificationResult scaResult = 
scaCoreService.verifyFactor(
challenge.getScaId(),
mapToSCAFactor(type),
challenge.getNonce()
);
if (scaResult.isSuccess()) {
log.info("Biometric verification successful for session: {}", 
challenge.getScaId());
challenges.remove(challengeId);
return true;
}
}
} catch (Exception e) {
log.error("Biometric verification failed", e);
}
return false;
}
/**
* Generate nonce for biometric challenge
*/
private String generateNonce() {
byte[] nonce = new byte[32];
java.security.SecureRandom secureRandom = new java.security.SecureRandom();
secureRandom.nextBytes(nonce);
return Base64.getEncoder().encodeToString(nonce);
}
/**
* Verify signature of biometric data
*/
private boolean verifySignature(byte[] data, PublicKey publicKey, String nonce) 
throws Exception {
// The data should contain the nonce and biometric template
// Signed with the device's private key
Signature sig = Signature.getInstance("SHA256withRSA");
sig.initVerify(publicKey);
sig.update(data);
return sig.verify(data); // Simplified
}
/**
* Match biometric data (mock)
*/
private BiometricVerificationResult matchBiometric(byte[] biometricData, BiometricType type) {
// In production, integrate with biometric matching service
return BiometricVerificationResult.success(0.98f);
}
/**
* Map biometric type to SCA factor
*/
private SCACoreService.AuthenticationFactor mapToSCAFactor(BiometricType type) {
switch (type) {
case FINGERPRINT:
return SCACoreService.AuthenticationFactor.INHERENCE_FINGERPRINT;
case FACE:
return SCACoreService.AuthenticationFactor.INHERENCE_FACE;
case VOICE:
return SCACoreService.AuthenticationFactor.INHERENCE_VOICE;
default:
return SCACoreService.AuthenticationFactor.INHERENCE_FINGERPRINT;
}
}
}

6. TOTP (Time-based OTP) Service

package com.sca.factors.totp;
import com.google.zxing.BarcodeFormat;
import com.google.zxing.client.j2se.MatrixToImageWriter;
import com.google.zxing.common.BitMatrix;
import com.google.zxing.qrcode.QRCodeWriter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.io.ByteArrayOutputStream;
import java.security.SecureRandom;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@Slf4j
@Service
public class TOTPService {
private final Map<String, TOTPRegistration> registrations = new ConcurrentHashMap<>();
private final SecureRandom secureRandom = new SecureRandom();
private static final int DEFAULT_TIME_STEP = 30; // seconds
private static final int DEFAULT_DIGITS = 6;
private static final String DEFAULT_ALGORITHM = "HmacSHA1";
/**
* TOTP registration
*/
public static class TOTPRegistration {
private final String userId;
private final String secret;
private final String qrCodeData;
private final long registeredAt;
public TOTPRegistration(String userId, String secret, String qrCodeData) {
this.userId = userId;
this.secret = secret;
this.qrCodeData = qrCodeData;
this.registeredAt = System.currentTimeMillis();
}
public String getSecret() { return secret; }
public String getQrCodeData() { return qrCodeData; }
}
/**
* Generate TOTP secret for user
*/
public TOTPRegistration generateSecret(String userId, String issuer) {
// Generate random secret (20 bytes = 160 bits)
byte[] secretBytes = new byte[20];
secureRandom.nextBytes(secretBytes);
String secret = Base64.getEncoder().encodeToString(secretBytes);
// Create QR code data (otpauth URI)
String qrData = String.format(
"otpauth://totp/%s:%s?secret=%s&issuer=%s&algorithm=%s&digits=%d&period=%d",
issuer,
userId,
secret,
issuer,
DEFAULT_ALGORITHM,
DEFAULT_DIGITS,
DEFAULT_TIME_STEP
);
TOTPRegistration registration = new TOTPRegistration(userId, secret, qrData);
registrations.put(userId, registration);
log.info("TOTP secret generated for user: {}", userId);
return registration;
}
/**
* Generate QR code as PNG
*/
public byte[] generateQRCode(String qrData, int width, int height) throws Exception {
QRCodeWriter qrCodeWriter = new QRCodeWriter();
BitMatrix bitMatrix = qrCodeWriter.encode(qrData, BarcodeFormat.QR_CODE, width, height);
ByteArrayOutputStream pngOutputStream = new ByteArrayOutputStream();
MatrixToImageWriter.writeToStream(bitMatrix, "PNG", pngOutputStream);
return pngOutputStream.toByteArray();
}
/**
* Generate current TOTP
*/
public String generateTOTP(String secret) {
return generateTOTP(secret, System.currentTimeMillis() / 1000);
}
/**
* Generate TOTP for specific time
*/
public String generateTOTP(String secret, long time) {
try {
long counter = time / DEFAULT_TIME_STEP;
return generateHOTP(secret, counter);
} catch (Exception e) {
log.error("TOTP generation failed", e);
return null;
}
}
/**
* Generate HOTP (HMAC-based OTP)
*/
private String generateHOTP(String secret, long counter) throws Exception {
byte[] counterBytes = new byte[8];
for (int i = 0; i < 8; i++) {
counterBytes[7 - i] = (byte) (counter >>> (i * 8));
}
Mac mac = Mac.getInstance(DEFAULT_ALGORITHM);
SecretKeySpec keySpec = new SecretKeySpec(Base64.getDecoder().decode(secret), DEFAULT_ALGORITHM);
mac.init(keySpec);
byte[] hash = mac.doFinal(counterBytes);
int offset = hash[hash.length - 1] & 0xF;
int binary = ((hash[offset] & 0x7F) << 24) |
((hash[offset + 1] & 0xFF) << 16) |
((hash[offset + 2] & 0xFF) << 8) |
(hash[offset + 3] & 0xFF);
int otp = binary % (int) Math.pow(10, DEFAULT_DIGITS);
return String.format("%0" + DEFAULT_DIGITS + "d", otp);
}
/**
* Verify TOTP
*/
public boolean verifyTOTP(String userId, String otp, int allowedDrift) {
TOTPRegistration registration = registrations.get(userId);
if (registration == null) {
log.warn("No TOTP registration found for user: {}", userId);
return false;
}
// Check current and adjacent time windows (for clock drift)
long currentTime = System.currentTimeMillis() / 1000;
for (int drift = -allowedDrift; drift <= allowedDrift; drift++) {
long time = currentTime + (drift * DEFAULT_TIME_STEP);
String expectedOtp = generateTOTP(registration.getSecret(), time);
if (otp.equals(expectedOtp)) {
log.info("TOTP verified for user: {}", userId);
return true;
}
}
log.warn("Invalid TOTP for user: {}", userId);
return false;
}
/**
* Remove TOTP registration
*/
public void removeRegistration(String userId) {
registrations.remove(userId);
log.info("TOTP registration removed for user: {}", userId);
}
}

7. Hardware Token Service

package com.sca.factors.token;
import com.sca.core.SCACoreService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.security.MessageDigest;
import java.security.SecureRandom;
import java.time.LocalDateTime;
import java.util.Base64;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@Slf4j
@Service
public class HardwareTokenService {
private final SCACoreService scaCoreService;
private final Map<String, TokenRegistration> tokens = new ConcurrentHashMap<>();
private final SecureRandom secureRandom = new SecureRandom();
public HardwareTokenService(SCACoreService scaCoreService) {
this.scaCoreService = scaCoreService;
}
/**
* Token types
*/
public enum TokenType {
CARD_READER,
USB_TOKEN,
SMART_CARD,
MOBILE_TOKEN
}
/**
* Token registration
*/
public static class TokenRegistration {
private final String tokenId;
private final String userId;
private final TokenType type;
private final String serialNumber;
private final byte[] publicKey;
private final LocalDateTime registeredAt;
private LocalDateTime lastUsed;
public TokenRegistration(String tokenId, String userId, TokenType type,
String serialNumber, byte[] publicKey) {
this.tokenId = tokenId;
this.userId = userId;
this.type = type;
this.serialNumber = serialNumber;
this.publicKey = publicKey;
this.registeredAt = LocalDateTime.now();
}
// getters and setters
}
/**
* Register hardware token
*/
public TokenRegistration registerToken(String userId, TokenType type,
String serialNumber, byte[] publicKey) {
String tokenId = generateTokenId();
TokenRegistration registration = new TokenRegistration(
tokenId, userId, type, serialNumber, publicKey
);
tokens.put(tokenId, registration);
log.info("Hardware token registered: {} for user: {}", tokenId, userId);
return registration;
}
/**
* Generate challenge for token
*/
public String generateChallenge(String tokenId, String scaId) {
TokenRegistration registration = tokens.get(tokenId);
if (registration == null) {
log.warn("Token not found: {}", tokenId);
return null;
}
// Generate random challenge
byte[] challenge = new byte[32];
secureRandom.nextBytes(challenge);
String challengeB64 = Base64.getEncoder().encodeToString(challenge);
// Store challenge in session
SCACoreService.SCASession session = scaCoreService.getSession(scaId);
if (session != null) {
session.getMetadata().put("tokenChallenge_" + tokenId, challengeB64);
session.getMetadata().put("tokenId", tokenId);
}
return challengeB64;
}
/**
* Verify token response
*/
public boolean verifyResponse(String tokenId, String scaId, String response) {
TokenRegistration registration = tokens.get(tokenId);
if (registration == null) {
log.warn("Token not found: {}", tokenId);
return false;
}
SCACoreService.SCASession session = scaCoreService.getSession(scaId);
if (session == null) {
log.warn("Session not found: {}", scaId);
return false;
}
// Get challenge from session
String challenge = (String) session.getMetadata().get("tokenChallenge_" + tokenId);
if (challenge == null) {
log.warn("No challenge found for token: {}", tokenId);
return false;
}
// Verify response (in production, validate cryptographic signature)
boolean verified = verifyTokenResponse(challenge, response, registration);
if (verified) {
registration.lastUsed = LocalDateTime.now();
// Verify with SCA service
SCACoreService.SCAVerificationResult result = 
scaCoreService.verifyFactor(
scaId,
SCACoreService.AuthenticationFactor.POSSESSION_HARDWARE_TOKEN,
response
);
return result.isSuccess();
}
return false;
}
/**
* Get token status
*/
public TokenStatus getTokenStatus(String tokenId) {
TokenRegistration registration = tokens.get(tokenId);
if (registration == null) {
return TokenStatus.NOT_FOUND;
}
if (isTokenExpired(registration)) {
return TokenStatus.EXPIRED;
}
if (isTokenBlocked(registration)) {
return TokenStatus.BLOCKED;
}
return TokenStatus.ACTIVE;
}
/**
* Verify token response (mock)
*/
private boolean verifyTokenResponse(String challenge, String response,
TokenRegistration registration) {
// In production, verify cryptographic signature
// using the token's public key
return true;
}
/**
* Generate unique token ID
*/
private String generateTokenId() {
byte[] id = new byte[16];
secureRandom.nextBytes(id);
return "TOK-" + Base64.getEncoder().encodeToString(id).substring(0, 8);
}
private boolean isTokenExpired(TokenRegistration registration) {
// Check if token is expired (e.g., 5 years)
return false;
}
private boolean isTokenBlocked(TokenRegistration registration) {
// Check if token is blocked due to too many failures
return false;
}
public enum TokenStatus {
ACTIVE,
EXPIRED,
BLOCKED,
NOT_FOUND
}
}

8. SCA Risk Analysis Engine

package com.sca.risk;
import com.sca.core.SCACoreService;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
@Slf4j
@Service
public class SCARiskAnalysisEngine {
// User behavior patterns
private final Map<String, UserBehaviorProfile> userProfiles = new ConcurrentHashMap<>();
// Transaction limits
private static final BigDecimal HIGH_RISK_AMOUNT = new BigDecimal("500.00");
private static final BigDecimal MEDIUM_RISK_AMOUNT = new BigDecimal("100.00");
// Time windows (24h format)
private static final int BUSINESS_HOURS_START = 8;
private static final int BUSINESS_HOURS_END = 20;
/**
* User behavior profile
*/
@Data
public static class UserBehaviorProfile {
private String userId;
private BigDecimal averageTransactionAmount;
private BigDecimal maxTransactionAmount;
private List<String> frequentMerchants;
private List<String> frequentCountries;
private List<LocalTime> frequentTransactionTimes;
private Set<String> trustedBeneficiaries;
private int transactionCount7d;
private int transactionCount30d;
private int failedSCACount;
private double fraudScore;
private LocalDateTime lastUpdated;
}
/**
* Transaction risk assessment
*/
@Data
@Builder
public static class RiskAssessment {
private String assessmentId;
private String userId;
private BigDecimal amount;
private String merchant;
private String country;
private LocalDateTime timestamp;
private RiskLevel riskLevel;
private double riskScore;
private List<String> riskFactors;
private boolean scaRequired;
private List<SCACoreService.AuthenticationFactor> recommendedFactors;
private String exemptionReason;
}
public enum RiskLevel {
LOW,
MEDIUM,
HIGH,
CRITICAL
}
/**
* Assess transaction risk
*/
public RiskAssessment assessTransactionRisk(String userId,
BigDecimal amount,
String merchant,
String country) {
UserBehaviorProfile profile = getUserProfile(userId);
RiskAssessment.RiskAssessmentBuilder builder = RiskAssessment.builder()
.assessmentId(UUID.randomUUID().toString())
.userId(userId)
.amount(amount)
.merchant(merchant)
.country(country)
.timestamp(LocalDateTime.now())
.riskFactors(new ArrayList<>());
double riskScore = 0.0;
// Amount-based risk
riskScore += assessAmountRisk(amount, profile);
// Merchant-based risk
riskScore += assessMerchantRisk(merchant, profile);
// Country-based risk
riskScore += assessCountryRisk(country, profile);
// Time-based risk
riskScore += assessTimeRisk(LocalDateTime.now());
// User behavior risk
riskScore += assessUserBehaviorRisk(profile);
// Velocity check
riskScore += assessTransactionVelocity(userId);
// Determine risk level
RiskLevel riskLevel = determineRiskLevel(riskScore);
builder.riskLevel(riskLevel);
builder.riskScore(riskScore);
// Determine SCA requirements
boolean scaRequired = isSCARequired(riskLevel, amount);
builder.scaRequired(scaRequired);
if (scaRequired) {
builder.recommendedFactors(determineSCAFactors(riskLevel));
} else {
builder.exemptionReason(determineExemptionReason(amount, merchant, profile));
}
return builder.build();
}
/**
* Update user behavior profile after transaction
*/
public void updateUserProfile(String userId, TransactionResult result) {
UserBehaviorProfile profile = userProfiles.computeIfAbsent(userId, 
k -> createNewProfile(userId));
// Update average amount
BigDecimal newAvg = profile.getAverageTransactionAmount()
.multiply(new BigDecimal(profile.getTransactionCount30d()))
.add(result.getAmount())
.divide(new BigDecimal(profile.getTransactionCount30d() + 1));
profile.setAverageTransactionAmount(newAvg);
// Update max amount
if (result.getAmount().compareTo(profile.getMaxTransactionAmount()) > 0) {
profile.setMaxTransactionAmount(result.getAmount());
}
// Update merchant frequency
// Update country frequency
// Update transaction counts
profile.setLastUpdated(LocalDateTime.now());
}
/**
* Assess amount-based risk
*/
private double assessAmountRisk(BigDecimal amount, UserBehaviorProfile profile) {
if (amount.compareTo(HIGH_RISK_AMOUNT) > 0) {
return 40.0;
} else if (amount.compareTo(MEDIUM_RISK_AMOUNT) > 0) {
return 20.0;
} else if (amount.compareTo(profile.getAverageTransactionAmount()) > 0) {
return 10.0;
}
return 0.0;
}
/**
* Assess merchant risk
*/
private double assessMerchantRisk(String merchant, UserBehaviorProfile profile) {
if (profile.getFrequentMerchants().contains(merchant)) {
return -10.0; // Trusted merchant reduces risk
}
// High-risk merchant categories (gambling, crypto, etc.)
if (isHighRiskMerchant(merchant)) {
return 30.0;
}
return 10.0; // New merchant
}
/**
* Assess country risk
*/
private double assessCountryRisk(String country, UserBehaviorProfile profile) {
if (profile.getFrequentCountries().contains(country)) {
return -5.0;
}
// High-risk countries (sanctioned, high fraud)
if (isHighRiskCountry(country)) {
return 40.0;
}
return 15.0;
}
/**
* Assess time-based risk
*/
private double assessTimeRisk(LocalDateTime timestamp) {
int hour = timestamp.getHour();
if (hour >= BUSINESS_HOURS_START && hour <= BUSINESS_HOURS_END) {
return 0.0; // Business hours
} else if (hour >= 22 || hour <= 5) {
return 20.0; // Late night
} else {
return 10.0; // Evening
}
}
/**
* Assess user behavior risk
*/
private double assessUserBehaviorRisk(UserBehaviorProfile profile) {
double risk = 0.0;
// Failed SCA attempts
if (profile.getFailedSCACount() > 3) {
risk += 30.0;
}
// Fraud score from external system
risk += profile.getFraudScore() * 100;
return risk;
}
/**
* Assess transaction velocity
*/
private double assessTransactionVelocity(String userId) {
UserBehaviorProfile profile = getUserProfile(userId);
int recentTransactions = profile.getTransactionCount30d();
if (recentTransactions > 100) {
return 20.0;
} else if (recentTransactions > 50) {
return 10.0;
}
return 0.0;
}
/**
* Determine risk level based on score
*/
private RiskLevel determineRiskLevel(double score) {
if (score >= 70) {
return RiskLevel.CRITICAL;
} else if (score >= 40) {
return RiskLevel.HIGH;
} else if (score >= 15) {
return RiskLevel.MEDIUM;
} else {
return RiskLevel.LOW;
}
}
/**
* Determine if SCA is required
*/
private boolean isSCARequired(RiskLevel riskLevel, BigDecimal amount) {
// PSD2: Always required for amount > 30 EUR
if (amount.compareTo(new BigDecimal("30.00")) > 0) {
return true;
}
// Based on risk level
return riskLevel == RiskLevel.MEDIUM || 
riskLevel == RiskLevel.HIGH || 
riskLevel == RiskLevel.CRITICAL;
}
/**
* Determine required SCA factors based on risk
*/
private List<SCACoreService.AuthenticationFactor> determineSCAFactors(RiskLevel riskLevel) {
List<SCACoreService.AuthenticationFactor> factors = new ArrayList<>();
// First factor - always knowledge
factors.add(SCACoreService.AuthenticationFactor.KNOWLEDGE_PIN);
// Second factor based on risk
switch (riskLevel) {
case CRITICAL:
factors.add(SCACoreService.AuthenticationFactor.POSSESSION_HARDWARE_TOKEN);
factors.add(SCACoreService.AuthenticationFactor.INHERENCE_FINGERPRINT);
break;
case HIGH:
factors.add(SCACoreService.AuthenticationFactor.POSSESSION_OTP_APP);
factors.add(SCACoreService.AuthenticationFactor.INHERENCE_FINGERPRINT);
break;
case MEDIUM:
factors.add(SCACoreService.AuthenticationFactor.POSSESSION_OTP_SMS);
break;
default:
factors.add(SCACoreService.AuthenticationFactor.POSSESSION_OTP_SMS);
}
return factors;
}
/**
* Determine exemption reason
*/
private String determineExemptionReason(BigDecimal amount, 
String merchant,
UserBehaviorProfile profile) {
if (amount.compareTo(new BigDecimal("30.00")) <= 0) {
return "LOW_VALUE_TRANSACTION";
}
if (profile.getTrustedBeneficiaries().contains(merchant)) {
return "TRUSTED_BENEFICIARY";
}
return null;
}
/**
* Get or create user profile
*/
private UserBehaviorProfile getUserProfile(String userId) {
return userProfiles.computeIfAbsent(userId, this::createNewProfile);
}
/**
* Create new user profile
*/
private UserBehaviorProfile createNewProfile(String userId) {
UserBehaviorProfile profile = new UserBehaviorProfile();
profile.setUserId(userId);
profile.setAverageTransactionAmount(BigDecimal.ZERO);
profile.setMaxTransactionAmount(BigDecimal.ZERO);
profile.setFrequentMerchants(new ArrayList<>());
profile.setFrequentCountries(new ArrayList<>());
profile.setFrequentTransactionTimes(new ArrayList<>());
profile.setTrustedBeneficiaries(new HashSet<>());
profile.setTransactionCount7d(0);
profile.setTransactionCount30d(0);
profile.setFailedSCACount(0);
profile.setFraudScore(0.0);
return profile;
}
private boolean isHighRiskMerchant(String merchant) {
Set<String> highRiskMerchants = Set.of(
"GAMBLING", "CRYPTO", "ADULT", "WEAPONS"
);
return highRiskMerchants.contains(merchant.toUpperCase());
}
private boolean isHighRiskCountry(String country) {
Set<String> highRiskCountries = Set.of(
"XX", "YY", "ZZ" // Placeholder for high-risk countries
);
return highRiskCountries.contains(country.toUpperCase());
}
@Data
public static class TransactionResult {
private BigDecimal amount;
private String merchant;
private boolean successful;
// other fields
}
}

9. SCA REST Controllers

package com.sca.controller;
import com.sca.core.SCACoreService;
import com.sca.factors.biometric.BiometricAuthenticationService;
import com.sca.factors.push.PushNotificationService;
import com.sca.factors.sms.SMSOTPService;
import com.sca.factors.totp.TOTPService;
import com.sca.risk.SCARiskAnalysisEngine;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.math.BigDecimal;
import java.util.Map;
@Slf4j
@RestController
@RequestMapping("/api/sca")
@RequiredArgsConstructor
public class SCAController {
private final SCACoreService scaCoreService;
private final SMSOTPService smsOtpService;
private final PushNotificationService pushService;
private final BiometricAuthenticationService biometricService;
private final TOTPService totpService;
private final SCARiskAnalysisEngine riskEngine;
/**
* Initiate SCA for transaction
*/
@PostMapping("/initiate")
public ResponseEntity<?> initiateSCA(@RequestBody SCAInitiateRequest request) {
try {
// Assess risk
SCARiskAnalysisEngine.RiskAssessment risk = riskEngine.assessTransactionRisk(
request.getUserId(),
request.getAmount(),
request.getMerchant(),
request.getCountry()
);
if (!risk.isScaRequired()) {
return ResponseEntity.ok(Map.of(
"scaRequired", false,
"exemptionReason", risk.getExemptionReason()
));
}
// Create SCA request
SCACoreService.SCARequest scaRequest = SCACoreService.SCARequest.builder()
.userId(request.getUserId())
.transactionId(request.getTransactionId())
.amount(request.getAmount())
.currency(request.getCurrency())
.merchantName(request.getMerchant())
.requiredFactors(risk.getRecommendedFactors())
.riskScore(String.valueOf(risk.getRiskScore()))
.build();
// Initiate SCA session
SCACoreService.SCASession session = scaCoreService.initiateSCA(scaRequest);
return ResponseEntity.ok(Map.of(
"scaId", session.getScaId(),
"requiredFactors", session.getRequiredFactors(),
"expiresAt", session.getExpiresAt(),
"riskLevel", risk.getRiskLevel()
));
} catch (Exception e) {
log.error("SCA initiation failed", e);
return ResponseEntity.badRequest()
.body(Map.of("error", "SCA initiation failed: " + e.getMessage()));
}
}
/**
* Send SMS OTP
*/
@PostMapping("/{scaId}/sms/send")
public ResponseEntity<?> sendSmsOtp(@PathVariable String scaId,
@RequestParam String phoneNumber) {
boolean sent = smsOtpService.sendOTP(scaId, phoneNumber);
if (sent) {
return ResponseEntity.ok(Map.of("message", "OTP sent successfully"));
} else {
return ResponseEntity.badRequest()
.body(Map.of("error", "Failed to send OTP"));
}
}
/**
* Verify SMS OTP
*/
@PostMapping("/{scaId}/sms/verify")
public ResponseEntity<?> verifySmsOtp(@PathVariable String scaId,
@RequestParam String otp) {
boolean verified = smsOtpService.verifyOTP(scaId, otp);
if (verified) {
return ResponseEntity.ok(Map.of(
"verified", true,
"message", "OTP verified successfully"
));
} else {
return ResponseEntity.badRequest()
.body(Map.of("verified", false, "error", "Invalid OTP"));
}
}
/**
* Send push notification
*/
@PostMapping("/{scaId}/push/send")
public ResponseEntity<?> sendPush(@PathVariable String scaId,
@RequestParam String userId) {
var future = pushService.sendPushAuth(scaId, userId);
return ResponseEntity.accepted().body(Map.of(
"message", "Push notification sent",
"expiresIn", 120
));
}
/**
* Handle push response (from mobile app)
*/
@PostMapping("/push/response")
public ResponseEntity<?> handlePushResponse(@RequestBody PushResponse request) {
boolean handled = pushService.handlePushResponse(
request.getPushId(),
request.getAction()
);
if (handled) {
return ResponseEntity.ok(Map.of("message", "Response processed"));
} else {
return ResponseEntity.badRequest()
.body(Map.of("error", "Failed to process response"));
}
}
/**
* Generate TOTP secret
*/
@PostMapping("/totp/register")
public ResponseEntity<?> registerTOTP(@RequestParam String userId,
@RequestParam String issuer) {
TOTPService.TOTPRegistration registration = totpService.generateSecret(userId, issuer);
try {
byte[] qrCode = totpService.generateQRCode(registration.getQrCodeData(), 200, 200);
return ResponseEntity.ok()
.header("Content-Type", "image/png")
.body(qrCode);
} catch (Exception e) {
return ResponseEntity.ok(Map.of(
"secret", registration.getSecret(),
"qrData", registration.getQrCodeData()
));
}
}
/**
* Verify TOTP
*/
@PostMapping("/totp/verify")
public ResponseEntity<?> verifyTOTP(@RequestParam String userId,
@RequestParam String otp) {
boolean verified = totpService.verifyTOTP(userId, otp, 1);
if (verified) {
return ResponseEntity.ok(Map.of("verified", true));
} else {
return ResponseEntity.badRequest()
.body(Map.of("verified", false, "error", "Invalid TOTP"));
}
}
/**
* Get SCA session status
*/
@GetMapping("/{scaId}/status")
public ResponseEntity<?> getStatus(@PathVariable String scaId) {
SCACoreService.SCASession session = scaCoreService.getSession(scaId);
if (session == null) {
return ResponseEntity.notFound().build();
}
return ResponseEntity.ok(Map.of(
"scaId", session.getScaId(),
"status", session.getStatus(),
"completedFactors", session.getCompletedFactors(),
"retryCount", session.getRetryCount(),
"expiresAt", session.getExpiresAt()
));
}
/**
* Request classes
*/
public static class SCAInitiateRequest {
private String userId;
private String transactionId;
private BigDecimal amount;
private String currency;
private String merchant;
private String country;
// getters and setters
}
public static class PushResponse {
private String pushId;
private String action; // APPROVE or REJECT
// getters and setters
}
}

10. SCA Configuration

package com.sca.config;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import org.springframework.scheduling.annotation.EnableScheduling;
@Configuration
@EnableScheduling
@ConfigurationProperties(prefix = "sca")
public class SCAConfig {
private OtpConfig otp = new OtpConfig();
private SessionConfig session = new SessionConfig();
private RiskConfig risk = new RiskConfig();
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(connectionFactory);
template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
template.setHashKeySerializer(new StringRedisSerializer());
template.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());
return template;
}
@Bean
public SCACleanupService scaCleanupService() {
return new SCACleanupService();
}
/**
* OTP configuration
*/
public static class OtpConfig {
private int length = 6;
private int expirySeconds = 300;
private int maxRetries = 3;
private String algorithm = "HmacSHA1";
private int timeStepSeconds = 30;
// getters and setters
}
/**
* Session configuration
*/
public static class SessionConfig {
private int defaultExpiryMinutes = 10;
private int maxActiveSessions = 1000;
private boolean enablePersistence = true;
// getters and setters
}
/**
* Risk configuration
*/
public static class RiskConfig {
private BigDecimal lowValueThreshold = new BigDecimal("30.00");
private BigDecimal highRiskThreshold = new BigDecimal("500.00");
private boolean enableMachineLearning = false;
private String fraudServiceUrl;
// getters and setters
}
// getters and setters for main config
}
/**
* Scheduled cleanup service
*/
@Component
class SCACleanupService {
@Scheduled(fixedRate = 300000) // Every 5 minutes
public void cleanupExpiredSessions() {
// Cleanup logic
}
}

Security Best Practices

1. Dynamic Linking (PSD2 Requirement)

public class DynamicLinkingService {
public String createDynamicLink(String transactionId, 
BigDecimal amount,
String creditorAccount) {
String data = transactionId + "|" + amount + "|" + creditorAccount;
return hashData(data);
}
public boolean verifyDynamicLink(String link, String transactionId,
BigDecimal amount, String creditorAccount) {
String computed = createDynamicLink(transactionId, amount, creditorAccount);
return link.equals(computed);
}
}

2. Rate Limiting

@Component
public class SCARateLimiter {
private final RateLimiter rateLimiter = RateLimiter.create(5); // 5 attempts per minute
public boolean allowRequest(String userId) {
return rateLimiter.tryAcquire();
}
}

3. Audit Logging

@Aspect
@Component
public class SCAAuditAspect {
@Around("@annotation(Audited)")
public Object auditSCA(ProceedingJoinPoint pjp) throws Throwable {
// Log all SCA attempts
// Include userId, factor type, success/failure, IP, timestamp
// Store in secure audit log for compliance
}
}

Configuration Example

application.yml

sca:
otp:
length: 6
expiry-seconds: 300
max-retries: 3
algorithm: HmacSHA1
time-step-seconds: 30
session:
default-expiry-minutes: 10
max-active-sessions: 1000
enable-persistence: true
risk:
low-value-threshold: 30.00
high-risk-threshold: 500.00
enable-machine-learning: false
factors:
sms-enabled: true
push-enabled: true
biometric-enabled: true
hardware-token-enabled: true
security:
rate-limit: 5
rate-limit-period: 60 # seconds
max-retries: 3
block-duration-minutes: 30

PSD2 Compliance Matrix

RequirementImplementationVerification
Two-factor authenticationMultiple factor typesAll transactions > 30 EUR
Dynamic linkingTransaction hashLinked to specific amount/beneficiary
Independence of factorsDifferent categoriesKnowledge, Possession, Inherence
SCA exemptionsRisk-based engineLow-value, trusted beneficiaries
Audit trailComprehensive loggingAll SCA attempts logged
Transaction monitoringReal-time risk analysisSuspicious activity detection

Conclusion

This comprehensive Strong Customer Authentication implementation provides:

Key Features

  • Multi-factor authentication with knowledge, possession, and inherence factors
  • Multiple delivery methods (SMS, Push, TOTP, Biometric, Hardware tokens)
  • Risk-based authentication with dynamic factor selection
  • PSD2 compliance with dynamic linking and exemptions
  • Session management with Redis persistence
  • Rate limiting and brute force protection
  • Comprehensive audit logging for regulatory compliance

Security Benefits

  • Defense in depth with multiple authentication layers
  • Adaptive authentication based on transaction risk
  • Replay prevention with time-limited challenges
  • Dynamic linking prevents transaction tampering
  • Biometric verification for high-security transactions

Compliance

  • Full PSD2 SCA requirements
  • EBA guidelines on authentication
  • GDPR compliance for biometric data
  • Audit trails for regulatory reporting

This implementation provides a production-ready Strong Customer Authentication system suitable for financial institutions and payment service providers requiring PSD2 compliance.

Java Programming Basics – Variables, Loops, Methods, Classes, Files & Exception Handling (Related to Java Programming)


Variables and Data Types in Java:
This topic explains how variables store data in Java and how data types define the kind of values a variable can hold, such as numbers, characters, or text. Java includes primitive types like int, double, and boolean, which are essential for storing and managing data in programs. (GeeksforGeeks)
Read more: https://macronepal.com/blog/variables-and-data-types-in-java/


Basic Input and Output in Java:
This lesson covers how Java programs receive input from users and display output using tools like Scanner for input and System.out.println() for output. These operations allow interaction between the program and the user.
Read more: https://macronepal.com/blog/basic-input-output-in-java/


Arithmetic Operations in Java:
This guide explains mathematical operations such as addition, subtraction, multiplication, and division using operators like +, -, *, and /. These operations are used to perform calculations in Java programs.
Read more: https://macronepal.com/blog/arithmetic-operations-in-java/


If-Else Statement in Java:
The if-else statement allows programs to make decisions based on conditions. It helps control program flow by executing different blocks of code depending on whether a condition is true or false.
Read more: https://macronepal.com/blog/if-else-statement-in-java/


For Loop in Java:
A for loop is used to repeat a block of code a specific number of times. It is commonly used when the number of repetitions is known in advance.
Read more: https://macronepal.com/blog/for-loop-in-java/


Method Overloading in Java:
Method overloading allows multiple methods to have the same name but different parameters. It improves code readability and flexibility by allowing similar tasks to be handled using one method name.
Read more: https://macronepal.com/blog/method-overloading-in-java-a-complete-guide/


Basic Inheritance in Java:
Inheritance is an object-oriented concept that allows one class to inherit properties and methods from another class. It promotes code reuse and helps build hierarchical class structures.
Read more: https://macronepal.com/blog/basic-inheritance-in-java-a-complete-guide/


File Writing in Java:
This topic explains how to create and write data into files using Java. File writing is commonly used to store program data permanently.
Read more: https://macronepal.com/blog/file-writing-in-java-a-complete-guide/


File Reading in Java:
File reading allows Java programs to read stored data from files. It is useful for retrieving saved information and processing it inside applications.
Read more: https://macronepal.com/blog/file-reading-in-java-a-complete-guide/


Exception Handling in Java:
Exception handling helps manage runtime errors using tools like try, catch, and finally. It prevents programs from crashing and allows safe error handling.
Read more: https://macronepal.com/blog/exception-handling-in-java-a-complete-guide/


Constructors in Java:
Constructors are special methods used to initialize objects when they are created. They help assign initial values to object variables automatically.
Read more: https://macronepal.com/blog/constructors-in-java/


Classes and Objects in Java:
Classes are blueprints used to create objects, while objects are instances of classes. These concepts form the foundation of object-oriented programming in Java.
Read more: https://macronepal.com/blog/classes-and-object-in-java/


Methods in Java:
Methods are blocks of code that perform specific tasks. They help organize programs into smaller reusable sections and improve code readability.
Read more: https://macronepal.com/blog/methods-in-java/


Arrays in Java:
Arrays store multiple values of the same type in a single variable. They are useful for handling lists of data such as numbers or names.
Read more: https://macronepal.com/blog/arrays-in-java/


While Loop in Java:
A while loop repeats a block of code as long as a given condition remains true. It is useful when the number of repetitions is not known beforehand.
Read more: https://macronepal.com/blog/while-loop-in-java/

Leave a Reply

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


Macro Nepal Helper