Introduction to PSD2 and Open Banking
The Revised Payment Services Directive (PSD2) is an EU regulation that aims to increase competition and innovation in the banking industry. It requires banks to provide Third Party Providers (TPPs) with access to customer accounts through secure APIs. This guide covers implementing PSD2-compliant Open Banking APIs in Java.
System Architecture Overview
Open Banking PSD2 Architecture ├── Regulatory Components │ ├── eIDAS Certificates (QWAC, QSEAL) │ ├── National Competent Authorities (NCAs) │ ├── Directory Service (TPP Registration) │ └── Consent Management ├── Security Layers │ ├── Mutual TLS (mTLS) │ ├── OAuth 2.0 / OIDC │ ├── Certificate Validation │ ├── Request Signing (JWS) │ └── TPP Authentication ├── API Services │ ├── Account Information Service (AIS) │ ├── Payment Initiation Service (PIS) │ ├── Confirmation of Funds (CoF) │ └── Certificate Validation Service └── Compliance Features ├── SCA (Strong Customer Authentication) ├── Dynamic Linking ├── Transaction Monitoring └── Audit Logging
Core Implementation
1. Maven Dependencies
<properties> <spring.boot.version>3.2.0</spring.boot.version> <nimbus.version>9.37.3</nimbus.version> <bouncycastle.version>1.78</bouncycastle.version> <eidas.version>1.5.0</eidas.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-security</artifactId> <version>${spring.boot.version}</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> <version>${spring.boot.version}</version> </dependency> <!-- Nimbus JOSE + JWT --> <dependency> <groupId>com.nimbusds</groupId> <artifactId>nimbus-jose-jwt</artifactId> <version>${nimbus.version}</version> </dependency> <!-- Bouncy Castle for PKI --> <dependency> <groupId>org.bouncycastle</groupId> <artifactId>bcprov-jdk18on</artifactId> <version>${bouncycastle.version}</version> </dependency> <dependency> <groupId>org.bouncycastle</groupId> <artifactId>bcpkix-jdk18on</artifactId> <version>${bouncycastle.version}</version> </dependency> <!-- eIDAS Certificate Validation --> <dependency> <groupId>eu.eidas</groupId> <artifactId>eidas-commons</artifactId> <version>${eidas.version}</version> </dependency> <!-- HTTP Client --> <dependency> <groupId>org.apache.httpcomponents.client5</groupId> <artifactId>httpclient5</artifactId> <version>5.2.1</version> </dependency> <!-- Database --> <dependency> <groupId>org.postgresql</groupId> <artifactId>postgresql</artifactId> <version>42.7.1</version> </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. eIDAS Certificate Validator
package com.openbanking.psd2.certificate; import org.bouncycastle.asn1.ASN1InputStream; import org.bouncycastle.asn1.ASN1ObjectIdentifier; import org.bouncycastle.asn1.ASN1Sequence; import org.bouncycastle.asn1.DERIA5String; import org.bouncycastle.asn1.x509.*; import org.bouncycastle.cert.X509CertificateHolder; import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter; import org.bouncycastle.jce.provider.BouncyCastleProvider; import org.springframework.stereotype.Service; import java.io.ByteArrayInputStream; import java.security.Security; import java.security.cert.*; import java.util.*; @Service public class EIDASCertificateValidator { static { Security.addProvider(new BouncyCastleProvider()); } // PSD2 Certificate Policies OIDs private static final String OID_PSD2_QWAC = "0.4.0.19495.1.1"; private static final String OID_PSD2_QSEAL = "0.4.0.19495.1.2"; private static final String OID_PSD2_QWAC_QSEAL = "0.4.0.19495.1.3"; // PSD2 Roles OIDs private static final String OID_ROLE_AISP = "0.4.0.19495.1.3.1"; private static final String OID_ROLE_PISP = "0.4.0.19495.1.3.2"; private static final String OID_ROLE_PIISP = "0.4.0.19495.1.3.3"; private static final String OID_ROLE_ASPSP = "0.4.0.19495.1.3.4"; /** * PSD2 Certificate information */ @Data @Builder public static class PSD2CertificateInfo { private String subjectDN; private String issuerDN; private String serialNumber; private Date notBefore; private Date notAfter; private String organizationName; private String organizationIdentifier; private String countryCode; private Set<PSD2Role> roles; private CertificateType certificateType; private String authorizationNumber; private String ncaId; private String ncaName; private boolean isValid; private List<String> validationErrors; } public enum PSD2Role { AISP("Account Information Service Provider"), PISP("Payment Initiation Service Provider"), PIISP("Payment Instrument Issuer Service Provider"), ASPSP("Account Servicing Payment Service Provider"); private final String description; PSD2Role(String description) { this.description = description; } public String getDescription() { return description; } } public enum CertificateType { QWAC("Qualified Website Authentication Certificate"), QSEAL("Qualified Electronic Seal"), QWAC_QSEAL("Combined QWAC and QSEAL"); private final String description; CertificateType(String description) { this.description = description; } public String getDescription() { return description; } } /** * Validate eIDAS PSD2 certificate */ public PSD2CertificateInfo validateCertificate(String certificatePem, List<X509Certificate> trustedCAs) { PSD2CertificateInfo.PSD2CertificateInfoBuilder builder = PSD2CertificateInfo.builder() .validationErrors(new ArrayList<>()); try { // Parse certificate CertificateFactory cf = CertificateFactory.getInstance("X.509"); X509Certificate cert = (X509Certificate) cf.generateCertificate( new ByteArrayInputStream(certificatePem.getBytes()) ); builder.subjectDN(cert.getSubjectX500Principal().getName()) .issuerDN(cert.getIssuerX500Principal().getName()) .serialNumber(cert.getSerialNumber().toString(16)) .notBefore(cert.getNotBefore()) .notAfter(cert.getNotAfter()); // Verify certificate chain if (!verifyCertificateChain(cert, trustedCAs)) { builder.validationError("Certificate chain validation failed"); } // Extract PSD2 specific extensions extractPSD2Extensions(cert, builder); // Check certificate validity period try { cert.checkValidity(); } catch (CertificateExpiredException | CertificateNotYetValidException e) { builder.validationError("Certificate validity: " + e.getMessage()); } // Check key usage boolean[] keyUsage = cert.getKeyUsage(); if (keyUsage != null) { if (!keyUsage[0] && !keyUsage[2]) { // digitalSignature and keyEncipherment builder.validationError("Invalid key usage for PSD2"); } } // Check extended key usage try { List<String> extendedKeyUsage = cert.getExtendedKeyUsage(); if (extendedKeyUsage == null || !extendedKeyUsage.contains("1.3.6.1.5.5.7.3.1") && // serverAuth !extendedKeyUsage.contains("1.3.6.1.5.5.7.3.2")) { // clientAuth builder.validationError("Missing required extended key usage"); } } catch (CertificateParsingException e) { builder.validationError("Could not parse extended key usage"); } builder.isValid(builder.build().getValidationErrors().isEmpty()); } catch (Exception e) { builder.validationError("Certificate parsing failed: " + e.getMessage()); } return builder.build(); } /** * Verify certificate chain */ private boolean verifyCertificateChain(X509Certificate cert, List<X509Certificate> trustedCAs) { try { PKIXParameters params = new PKIXParameters(new TrustAnchor( trustedCAs.get(0), null)); params.setRevocationEnabled(false); // Enable in production CertPath certPath = CertificateFactory.getInstance("X.509") .generateCertPath(Arrays.asList(cert)); CertPathValidator validator = CertPathValidator.getInstance("PKIX"); validator.validate(certPath, params); return true; } catch (Exception e) { return false; } } /** * Extract PSD2 specific extensions from certificate */ private void extractPSD2Extensions(X509Certificate cert, PSD2CertificateInfo.PSD2CertificateInfoBuilder builder) { try { // Get certificate as Bouncy Castle object X509CertificateHolder certHolder = new X509CertificateHolder( cert.getEncoded()); // Extract certificate policies Extensions extensions = certHolder.getExtensions(); if (extensions != null) { // Check certificate type from policy OID CertificateType certType = extractCertificateType(extensions); builder.certificateType(certType); // Extract PSD2 roles from certificate Set<PSD2Role> roles = extractPSDRoles(extensions); builder.roles(roles); // Extract organization identifier String orgIdentifier = extractOrganizationIdentifier(cert); builder.organizationIdentifier(orgIdentifier); // Extract authorization number from NCA extractNCAData(extensions, builder); } // Extract organization name from subject String orgName = extractOrganizationName(cert.getSubjectX500Principal()); builder.organizationName(orgName); // Extract country code String countryCode = extractCountryCode(cert.getSubjectX500Principal()); builder.countryCode(countryCode); } catch (Exception e) { builder.validationError("Failed to extract PSD2 extensions: " + e.getMessage()); } } /** * Extract certificate type from policy OID */ private CertificateType extractCertificateType(Extensions extensions) { ASN1ObjectIdentifier[] policyOIDs = getCertificatePolicyOIDs(extensions); for (ASN1ObjectIdentifier oid : policyOIDs) { String oidStr = oid.getId(); if (OID_PSD2_QWAC_QSEAL.equals(oidStr)) { return CertificateType.QWAC_QSEAL; } else if (OID_PSD2_QWAC.equals(oidStr)) { return CertificateType.QWAC; } else if (OID_PSD2_QSEAL.equals(oidStr)) { return CertificateType.QSEAL; } } return null; } /** * Extract PSD2 roles from certificate extensions */ private Set<PSD2Role> extractPSDRoles(Extensions extensions) { Set<PSD2Role> roles = new HashSet<>(); try { // PSD2 roles are in the certificatePolicies extension ASN1ObjectIdentifier[] policyOIDs = getCertificatePolicyOIDs(extensions); for (ASN1ObjectIdentifier oid : policyOIDs) { String oidStr = oid.getId(); if (OID_ROLE_AISP.equals(oidStr)) { roles.add(PSD2Role.AISP); } else if (OID_ROLE_PISP.equals(oidStr)) { roles.add(PSD2Role.PISP); } else if (OID_ROLE_PIISP.equals(oidStr)) { roles.add(PSD2Role.PIISP); } else if (OID_ROLE_ASPSP.equals(oidStr)) { roles.add(PSD2Role.ASPSP); } } } catch (Exception e) { // Log error } return roles; } /** * Get certificate policy OIDs from extensions */ private ASN1ObjectIdentifier[] getCertificatePolicyOIDs(Extensions extensions) { try { Extension certPoliciesExt = extensions.getExtension( Extension.certificatePolicies); if (certPoliciesExt != null) { ASN1Sequence policies = ASN1Sequence.getInstance( certPoliciesExt.getParsedValue()); ASN1ObjectIdentifier[] oids = new ASN1ObjectIdentifier[policies.size()]; for (int i = 0; i < policies.size(); i++) { oids[i] = ASN1ObjectIdentifier.getInstance(policies.getObjectAt(i)); } return oids; } } catch (Exception e) { // Log error } return new ASN1ObjectIdentifier[0]; } /** * Extract organization identifier (NTR) */ private String extractOrganizationIdentifier(X509Certificate cert) { // Organization identifier is in the subject's organizationIdentifier attribute // Format: NTR{Country}-{Identifier} String subjectDN = cert.getSubjectX500Principal().getName(); for (String part : subjectDN.split(",")) { if (part.trim().startsWith("organizationIdentifier=")) { return part.trim().substring("organizationIdentifier=".length()); } } return null; } /** * Extract organization name from subject DN */ private String extractOrganizationName(X500Principal subject) { String name = subject.getName(); for (String part : name.split(",")) { if (part.trim().startsWith("O=")) { return part.trim().substring(2); } } return null; } /** * Extract country code from subject DN */ private String extractCountryCode(X500Principal subject) { String name = subject.getName(); for (String part : name.split(",")) { if (part.trim().startsWith("C=")) { return part.trim().substring(2); } } return null; } /** * Extract NCA (National Competent Authority) data */ private void extractNCAData(Extensions extensions, PSD2CertificateInfo.PSD2CertificateInfoBuilder builder) { try { // NCA data is in the certificatePolicies extension with qualifiers Extension certPoliciesExt = extensions.getExtension( Extension.certificatePolicies); if (certPoliciesExt != null) { ASN1Sequence policies = ASN1Sequence.getInstance( certPoliciesExt.getParsedValue()); for (int i = 0; i < policies.size(); i++) { PolicyInformation policyInfo = PolicyInformation.getInstance(policies.getObjectAt(i)); ASN1Sequence qualifiers = policyInfo.getPolicyQualifiers(); if (qualifiers != null) { for (int j = 0; j < qualifiers.size(); j++) { PolicyQualifierInfo qualifierInfo = PolicyQualifierInfo.getInstance(qualifiers.getObjectAt(j)); if (qualifierInfo.getPolicyQualifierId().equals( PolicyQualifierId.id_qt_cps)) { DERIA5String cps = DERIA5String.getInstance( qualifierInfo.getQualifier()); parseNCACPS(cps.getString(), builder); } } } } } } catch (Exception e) { builder.validationError("Failed to extract NCA data: " + e.getMessage()); } } /** * Parse NCA CPS (Certification Practice Statement) URI */ private void parseNCACPS(String cpsUri, PSD2CertificateInfo.PSD2CertificateInfoBuilder builder) { // CPS URI typically contains NCA identifier // Example: https://www.bundesbank.de/psd2/nca/DE-BAFIN if (cpsUri != null && cpsUri.contains("/nca/")) { String[] parts = cpsUri.split("/nca/"); if (parts.length > 1) { builder.ncaId(parts[1]); builder.ncaName(parts[1]); // Map to actual NCA name in production } } } } 3. TPP Authentication Service
package com.openbanking.psd2.tpp; import com.openbanking.psd2.certificate.EIDASCertificateValidator; import lombok.Data; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import java.security.cert.X509Certificate; import java.time.LocalDateTime; import java.util.*; import java.util.concurrent.ConcurrentHashMap; @Slf4j @Service public class TPPAuthenticationService { private final EIDASCertificateValidator certificateValidator; private final Map<String, TPPRegistration> tppRegistry = new ConcurrentHashMap<>(); private final List<X509Certificate> trustedCAs; public TPPAuthenticationService(EIDASCertificateValidator certificateValidator) { this.certificateValidator = certificateValidator; this.trustedCAs = loadTrustedCAs(); } /** * TPP Registration information */ @Data @Builder public static class TPPRegistration { private String tppId; private String tppName; private String authorizationNumber; private String countryCode; private Set<EIDASCertificateValidator.PSD2Role> roles; private EIDASCertificateValidator.CertificateType certificateType; private String certificateFingerprint; private LocalDateTime registeredAt; private LocalDateTime lastAccess; private boolean active; private List<String> allowedIPs; private Map<String, Object> metadata; } /** * Authenticate TPP using client certificate */ public Optional<TPPRegistration> authenticateTPP(String certificatePem, String clientIP) { try { // Validate certificate EIDASCertificateValidator.PSD2CertificateInfo certInfo = certificateValidator.validateCertificate(certificatePem, trustedCAs); if (!certInfo.isValid()) { log.warn("Certificate validation failed: {}", certInfo.getValidationErrors()); return Optional.empty(); } // Extract TPP identifier from certificate String tppId = extractTPPId(certInfo); // Check if TPP is registered TPPRegistration registration = tppRegistry.get(tppId); if (registration == null) { // Auto-register if not exists (in production, require manual approval) registration = registerTPP(certInfo); log.info("Auto-registered new TPP: {}", tppId); } // Verify IP whitelist if configured if (!isIPAllowed(registration, clientIP)) { log.warn("IP {} not allowed for TPP: {}", clientIP, tppId); return Optional.empty(); } // Check if TPP is active if (!registration.isActive()) { log.warn("TPP {} is not active", tppId); return Optional.empty(); } // Update last access registration.setLastAccess(LocalDateTime.now()); return Optional.of(registration); } catch (Exception e) { log.error("TPP authentication failed", e); return Optional.empty(); } } /** * Authenticate TPP using OAuth2 client credentials */ public Optional<TPPRegistration> authenticateTPPOAuth(String clientId, String clientSecret) { TPPRegistration registration = tppRegistry.get(clientId); if (registration == null || !registration.isActive()) { return Optional.empty(); } // Verify client secret (in production, use proper password validation) // This is simplified - use BCrypt or similar return Optional.of(registration); } /** * Register TPP manually */ public TPPRegistration registerTPP(EIDASCertificateValidator.PSD2CertificateInfo certInfo) { String tppId = extractTPPId(certInfo); TPPRegistration registration = TPPRegistration.builder() .tppId(tppId) .tppName(certInfo.getOrganizationName()) .authorizationNumber(certInfo.getAuthorizationNumber()) .countryCode(certInfo.getCountryCode()) .roles(certInfo.getRoles()) .certificateType(certInfo.getCertificateType()) .certificateFingerprint(generateFingerprint(certInfo)) .registeredAt(LocalDateTime.now()) .lastAccess(LocalDateTime.now()) .active(true) .allowedIPs(new ArrayList<>()) .metadata(new HashMap<>()) .build(); tppRegistry.put(tppId, registration); return registration; } /** * Check if TPP has required role */ public boolean hasRole(String tppId, EIDASCertificateValidator.PSD2Role role) { TPPRegistration registration = tppRegistry.get(tppId); return registration != null && registration.getRoles().contains(role); } /** * Authorize TPP for specific service */ public boolean authorizeTPP(String tppId, String serviceType) { TPPRegistration registration = tppRegistry.get(tppId); if (registration == null || !registration.isActive()) { return false; } // Check role based on service type switch (serviceType) { case "AIS": return registration.getRoles().contains(EIDASCertificateValidator.PSD2Role.AISP); case "PIS": return registration.getRoles().contains(EIDASCertificateValidator.PSD2Role.PISP); case "COF": return registration.getRoles().contains(EIDASCertificateValidator.PSD2Role.PIISP); default: return false; } } /** * Deactivate TPP */ public void deactivateTPP(String tppId, String reason) { TPPRegistration registration = tppRegistry.get(tppId); if (registration != null) { registration.setActive(false); registration.getMetadata().put("deactivationReason", reason); registration.getMetadata().put("deactivatedAt", LocalDateTime.now()); log.info("TPP {} deactivated: {}", tppId, reason); } } /** * Update TPP IP whitelist */ public void updateIPWhitelist(String tppId, List<String> allowedIPs) { TPPRegistration registration = tppRegistry.get(tppId); if (registration != null) { registration.setAllowedIPs(allowedIPs); } } private String extractTPPId(EIDASCertificateValidator.PSD2CertificateInfo certInfo) { // Format: {countryCode}-{authorizationNumber} return certInfo.getCountryCode() + "-" + certInfo.getAuthorizationNumber(); } private String generateFingerprint(EIDASCertificateValidator.PSD2CertificateInfo certInfo) { // Generate SHA-256 fingerprint return UUID.randomUUID().toString(); // Simplified } private boolean isIPAllowed(TPPRegistration registration, String clientIP) { if (registration.getAllowedIPs().isEmpty()) { return true; // No IP restrictions } // Check exact match or CIDR ranges return registration.getAllowedIPs().contains(clientIP); } private List<X509Certificate> loadTrustedCAs() { // Load trusted CA certificates (e.g., from keystore) return new ArrayList<>(); } } 4. Consent Management Service
package com.openbanking.psd2.consent; import com.openbanking.psd2.tpp.TPPAuthenticationService; import jakarta.persistence.*; import lombok.Data; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import java.time.LocalDateTime; import java.util.*; import java.util.concurrent.ConcurrentHashMap; @Slf4j @Service public class ConsentManagementService { private final Map<String, Consent> consentStore = new ConcurrentHashMap<>(); /** * Consent types per PSD2 */ public enum ConsentType { AIS_ACCOUNT_INFO, // Account Information Service AIS_TRANSACTIONS, // Transaction history AIS_BALANCE, // Balance information PIS_PAYMENT, // Payment initiation PIS_BULK_PAYMENT, // Bulk payments PIS_PERIODIC, // Periodic payments COF_FUNDS_CONFIRMATION // Confirmation of Funds } /** * Consent status */ public enum ConsentStatus { RECEIVED, ACCEPTED, REJECTED, REVOKED, EXPIRED } /** * Consent entity */ @Data @Builder @Entity @Table(name = "psd2_consents") public static class Consent { @Id private String consentId; private String tppId; private String userId; private String psuId; // Payment Service User ID @Enumerated(EnumType.STRING) private ConsentType consentType; @Enumerated(EnumType.STRING) private ConsentStatus status; private LocalDateTime createdAt; private LocalDateTime expiresAt; private LocalDateTime lastAccessed; @ElementCollection private Set<String> accountIds; // Accounts covered by consent @ElementCollection private Set<String> permissions; // Specific permissions private String recurringIndicator; // true/false private LocalDateTime validUntil; private Integer frequencyPerDay; @Lob private String scaMethod; // SCA method used @Lob private String consentData; // JSON with additional consent data private String psuIpAddress; private String psuUserAgent; @Version private Long version; } /** * Create new consent request */ public Consent createConsent(ConsentRequest request) { String consentId = generateConsentId(); Consent consent = Consent.builder() .consentId(consentId) .tppId(request.getTppId()) .userId(request.getUserId()) .psuId(request.getPsuId()) .consentType(request.getConsentType()) .status(ConsentStatus.RECEIVED) .createdAt(LocalDateTime.now()) .expiresAt(LocalDateTime.now().plusDays(90)) .accountIds(request.getAccountIds()) .permissions(request.getPermissions()) .recurringIndicator(request.getRecurringIndicator()) .validUntil(request.getValidUntil()) .frequencyPerDay(request.getFrequencyPerDay()) .psuIpAddress(request.getPsuIpAddress()) .psuUserAgent(request.getPsuUserAgent()) .build(); consentStore.put(consentId, consent); log.info("Created consent: {} for TPP: {}", consentId, request.getTppId()); return consent; } /** * Authorize consent (after SCA) */ public boolean authorizeConsent(String consentId, String scaMethod) { Consent consent = consentStore.get(consentId); if (consent == null) { return false; } consent.setStatus(ConsentStatus.ACCEPTED); consent.setScaMethod(scaMethod); consent.setLastAccessed(LocalDateTime.now()); log.info("Consent authorized: {}", consentId); return true; } /** * Validate consent for access */ public ValidationResult validateConsent(String consentId, String tppId, String accountId, String permission) { Consent consent = consentStore.get(consentId); if (consent == null) { return ValidationResult.failure("Consent not found"); } // Check TPP ownership if (!consent.getTppId().equals(tppId)) { return ValidationResult.failure("TPP not authorized for this consent"); } // Check status if (consent.getStatus() != ConsentStatus.ACCEPTED) { return ValidationResult.failure("Consent not in ACCEPTED state"); } // Check expiration if (consent.getExpiresAt().isBefore(LocalDateTime.now())) { return ValidationResult.failure("Consent expired"); } // Check valid until date if (consent.getValidUntil() != null && consent.getValidUntil().isBefore(LocalDateTime.now())) { return ValidationResult.failure("Consent validity period ended"); } // Check account coverage if (!consent.getAccountIds().contains(accountId)) { return ValidationResult.failure("Account not covered by consent"); } // Check permission if (!consent.getPermissions().contains(permission)) { return ValidationResult.failure("Permission not granted"); } // Update last accessed consent.setLastAccessed(LocalDateTime.now()); return ValidationResult.success(consent); } /** * Revoke consent */ public boolean revokeConsent(String consentId, String tppId) { Consent consent = consentStore.get(consentId); if (consent == null || !consent.getTppId().equals(tppId)) { return false; } consent.setStatus(ConsentStatus.REVOKED); log.info("Consent revoked: {}", consentId); return true; } /** * List consents for TPP */ public List<Consent> listConsents(String tppId, ConsentStatus status) { return consentStore.values().stream() .filter(c -> c.getTppId().equals(tppId)) .filter(c -> status == null || c.getStatus() == status) .toList(); } /** * List consents for PSU (Payment Service User) */ public List<Consent> listUserConsents(String psuId) { return consentStore.values().stream() .filter(c -> c.getPsuId().equals(psuId)) .toList(); } /** * Clean up expired consents */ @Scheduled(cron = "0 0 2 * * *") // Daily at 2 AM public void cleanupExpiredConsents() { LocalDateTime now = LocalDateTime.now(); consentStore.values().stream() .filter(c -> c.getStatus() == ConsentStatus.ACCEPTED) .filter(c -> c.getExpiresAt().isBefore(now)) .forEach(c -> { c.setStatus(ConsentStatus.EXPIRED); log.info("Consent expired: {}", c.getConsentId()); }); } private String generateConsentId() { return "CN-" + UUID.randomUUID().toString().substring(0, 8).toUpperCase(); } /** * Consent request DTO */ @Data public static class ConsentRequest { private String tppId; private String userId; private String psuId; private ConsentType consentType; private Set<String> accountIds; private Set<String> permissions; private String recurringIndicator; private LocalDateTime validUntil; private Integer frequencyPerDay; private String psuIpAddress; private String psuUserAgent; } /** * Validation result */ public static class ValidationResult { private final boolean valid; private final Consent consent; private final String error; private ValidationResult(boolean valid, Consent consent, String error) { this.valid = valid; this.consent = consent; this.error = error; } public static ValidationResult success(Consent consent) { return new ValidationResult(true, consent, null); } public static ValidationResult failure(String error) { return new ValidationResult(false, null, error); } public boolean isValid() { return valid; } public Consent getConsent() { return consent; } public String getError() { return error; } } } 5. Account Information Service (AIS)
package com.openbanking.psd2.ais; import com.openbanking.psd2.consent.ConsentManagementService; import com.openbanking.psd2.tpp.TPPAuthenticationService; import lombok.Data; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import java.math.BigDecimal; import java.time.LocalDateTime; import java.util.*; @Slf4j @Service public class AccountInformationService { private final TPPAuthenticationService tppAuthService; private final ConsentManagementService consentService; public AccountInformationService(TPPAuthenticationService tppAuthService, ConsentManagementService consentService) { this.tppAuthService = tppAuthService; this.consentService = consentService; } /** * Account information */ @Data @Builder public static class Account { private String accountId; private String iban; private String bban; private String pan; private String currency; private String product; private String accountType; private String accountStatus; private String bic; private String name; private String description; private LocalDateTime openingDate; private LocalDateTime closingDate; private BigDecimal balance; private LocalDateTime balanceTimestamp; } /** * Transaction */ @Data @Builder public static class Transaction { private String transactionId; private String accountId; private String entryReference; private BigDecimal amount; private String currency; private String creditDebitIndicator; private String status; private LocalDateTime bookingDate; private LocalDateTime valueDate; private LocalDateTime transactionDate; private String remittanceInformation; private String additionalInformation; private String creditorName; private String creditorAccount; private String debtorName; private String debtorAccount; private String bankTransactionCode; private String proprietaryBankTransactionCode; private String purposeCode; private Map<String, Object> additionalData; } /** * Balance information */ @Data @Builder public static class Balance { private String accountId; private BigDecimal amount; private String currency; private BalanceType balanceType; private LocalDateTime lastChangeDateTime; private LocalDateTime referenceDate; private String lastCommittedTransaction; } public enum BalanceType { CLOSING_BOOKED, EXPECTED, AUTHORISED, OPENING_BOOKED, INTERIM_AVAILABLE, FORWARD_AVAILABLE } /** * Get account list */ public AccountListResponse getAccounts(String tppId, String consentId, String psuIpAddress, boolean withBalance) { // Validate consent ConsentManagementService.ValidationResult validation = consentService.validateConsent(consentId, tppId, "*", "AIS_ACCOUNTS"); if (!validation.isValid()) { return AccountListResponse.failure(validation.getError()); } // Check TPP role if (!tppAuthService.hasRole(tppId, EIDASCertificateValidator.PSD2Role.AISP)) { return AccountListResponse.failure("TPP not authorized for AIS"); } // Fetch accounts from core banking system List<Account> accounts = fetchAccounts( validation.getConsent().getUserId(), validation.getConsent().getAccountIds() ); // Include balances if requested and authorized if (withBalance && validation.getConsent().getPermissions().contains("AIS_BALANCE")) { accounts.forEach(account -> account.setBalance(fetchBalance(account.getAccountId())) ); } log.info("AIS accounts fetched for TPP: {}, consent: {}", tppId, consentId); return AccountListResponse.success(accounts); } /** * Get account details */ public AccountResponse getAccount(String tppId, String consentId, String accountId, String psuIpAddress) { // Validate consent ConsentManagementService.ValidationResult validation = consentService.validateConsent(consentId, tppId, accountId, "AIS_ACCOUNTS"); if (!validation.isValid()) { return AccountResponse.failure(validation.getError()); } // Fetch account Account account = fetchAccount(accountId); if (account == null) { return AccountResponse.failure("Account not found"); } return AccountResponse.success(account); } /** * Get transactions */ public TransactionListResponse getTransactions(String tppId, String consentId, String accountId, LocalDateTime dateFrom, LocalDateTime dateTo, String psuIpAddress) { // Validate consent ConsentManagementService.ValidationResult validation = consentService.validateConsent(consentId, tppId, accountId, "AIS_TRANSACTIONS"); if (!validation.isValid()) { return TransactionListResponse.failure(validation.getError()); } // Fetch transactions List<Transaction> transactions = fetchTransactions( accountId, dateFrom, dateTo ); log.info("AIS transactions fetched for TPP: {}, account: {}", tppId, accountId); return TransactionListResponse.success(transactions); } /** * Get balances */ public BalanceListResponse getBalances(String tppId, String consentId, String accountId, String psuIpAddress) { // Validate consent ConsentManagementService.ValidationResult validation = consentService.validateConsent(consentId, tppId, accountId, "AIS_BALANCE"); if (!validation.isValid()) { return BalanceListResponse.failure(validation.getError()); } // Fetch balances List<Balance> balances = fetchBalances(accountId); return BalanceListResponse.success(balances); } // Mock methods - in production, integrate with core banking system private List<Account> fetchAccounts(String userId, Set<String> accountIds) { List<Account> accounts = new ArrayList<>(); for (String accountId : accountIds) { accounts.add(Account.builder() .accountId(accountId) .iban("DE89370400440532013000") .currency("EUR") .name("Girokonto") .product("Current Account") .accountType("CACC") .accountStatus("enabled") .openingDate(LocalDateTime.now().minusYears(2)) .build()); } return accounts; } private Account fetchAccount(String accountId) { return Account.builder() .accountId(accountId) .iban("DE89370400440532013000") .currency("EUR") .name("Girokonto") .product("Current Account") .accountType("CACC") .accountStatus("enabled") .openingDate(LocalDateTime.now().minusYears(2)) .build(); } private BigDecimal fetchBalance(String accountId) { return new BigDecimal("1234.56"); } private List<Transaction> fetchTransactions(String accountId, LocalDateTime from, LocalDateTime to) { List<Transaction> transactions = new ArrayList<>(); transactions.add(Transaction.builder() .transactionId("TXN-001") .accountId(accountId) .amount(new BigDecimal("100.00")) .currency("EUR") .creditDebitIndicator("DBIT") .status("BOOK") .bookingDate(LocalDateTime.now().minusDays(1)) .remittanceInformation("Amazon.de") .build()); return transactions; } private List<Balance> fetchBalances(String accountId) { List<Balance> balances = new ArrayList<>(); balances.add(Balance.builder() .accountId(accountId) .amount(new BigDecimal("1234.56")) .currency("EUR") .balanceType(BalanceType.CLOSING_BOOKED) .lastChangeDateTime(LocalDateTime.now()) .build()); return balances; } // Response classes @Data public static class AccountListResponse { private boolean success; private List<Account> accounts; private String error; private int totalPages; private int currentPage; public static AccountListResponse success(List<Account> accounts) { AccountListResponse response = new AccountListResponse(); response.setSuccess(true); response.setAccounts(accounts); return response; } public static AccountListResponse failure(String error) { AccountListResponse response = new AccountListResponse(); response.setSuccess(false); response.setError(error); return response; } } @Data public static class AccountResponse { private boolean success; private Account account; private String error; public static AccountResponse success(Account account) { AccountResponse response = new AccountResponse(); response.setSuccess(true); response.setAccount(account); return response; } public static AccountResponse failure(String error) { AccountResponse response = new AccountResponse(); response.setSuccess(false); response.setError(error); return response; } } @Data public static class TransactionListResponse { private boolean success; private List<Transaction> transactions; private String error; public static TransactionListResponse success(List<Transaction> transactions) { TransactionListResponse response = new TransactionListResponse(); response.setSuccess(true); response.setTransactions(transactions); return response; } public static TransactionListResponse failure(String error) { TransactionListResponse response = new TransactionListResponse(); response.setSuccess(false); response.setError(error); return response; } } @Data public static class BalanceListResponse { private boolean success; private List<Balance> balances; private String error; public static BalanceListResponse success(List<Balance> balances) { BalanceListResponse response = new BalanceListResponse(); response.setSuccess(true); response.setBalances(balances); return response; } public static BalanceListResponse failure(String error) { BalanceListResponse response = new BalanceListResponse(); response.setSuccess(false); response.setError(error); return response; } } } 6. Payment Initiation Service (PIS)
package com.openbanking.psd2.pis; import com.openbanking.psd2.consent.ConsentManagementService; import com.openbanking.psd2.tpp.TPPAuthenticationService; import lombok.Data; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import java.math.BigDecimal; import java.time.LocalDateTime; import java.util.*; @Slf4j @Service public class PaymentInitiationService { private final TPPAuthenticationService tppAuthService; private final ConsentManagementService consentService; private final Map<String, Payment> paymentStore = new HashMap<>(); /** * Payment types */ public enum PaymentType { SINGLE, BULK, PERIODIC } /** * Payment status */ public enum PaymentStatus { RCVD, // Received PDNG, // Pending ACCP, // Accepted ACSC, // Accepted Settlement Completed ACSP, // Accepted Settlement In Process ACTC, // Accepted Technical Validation ACWC, // Accepted With Change ACWP, // Accepted Without Posting CANC, // Cancelled RJCT // Rejected } /** * Payment initiation request */ @Data @Builder public static class PaymentRequest { private String paymentId; private String tppId; private String consentId; private String psuId; private PaymentType paymentType; private String debtorAccount; private String creditorAccount; private String creditorName; private String creditorAgent; private BigDecimal instructedAmount; private String currency; private String remittanceInformation; private LocalDateTime requestedExecutionDate; private LocalDateTime requestedExecutionTime; private String endToEndIdentification; private String instructionIdentification; private Map<String, Object> additionalData; } /** * Payment response */ @Data @Builder public static class PaymentResponse { private String paymentId; private String transactionStatus; private List<String> transactionIds; private LocalDateTime creationDateTime; private LocalDateTime statusUpdateDateTime; private Map<String, Object> links; private String error; } /** * Initiate single payment */ public PaymentResponse initiatePayment(PaymentRequest request, String psuIpAddress) { // Validate TPP role if (!tppAuthService.hasRole(request.getTppId(), EIDASCertificateValidator.PSD2Role.PISP)) { return PaymentResponse.builder() .error("TPP not authorized for PIS") .build(); } // Validate consent if provided if (request.getConsentId() != null) { ConsentManagementService.ValidationResult validation = consentService.validateConsent( request.getConsentId(), request.getTppId(), request.getDebtorAccount(), "PIS_PAYMENT" ); if (!validation.isValid()) { return PaymentResponse.builder() .error(validation.getError()) .build(); } } // Generate payment ID String paymentId = "PMT-" + UUID.randomUUID().toString().substring(0, 8).toUpperCase(); request.setPaymentId(paymentId); // Store payment Payment payment = Payment.builder() .paymentId(paymentId) .tppId(request.getTppId()) .consentId(request.getConsentId()) .psuId(request.getPsuId()) .paymentType(request.getPaymentType()) .status(PaymentStatus.RCVD) .createdAt(LocalDateTime.now()) .request(request) .build(); paymentStore.put(paymentId, payment); // Process payment asynchronously processPaymentAsync(paymentId); log.info("Payment initiated: {} by TPP: {}", paymentId, request.getTppId()); return PaymentResponse.builder() .paymentId(paymentId) .transactionStatus(PaymentStatus.RCVD.name()) .creationDateTime(LocalDateTime.now()) .links(Map.of("self", "/v1/payments/" + paymentId)) .build(); } /** * Get payment status */ public PaymentResponse getPaymentStatus(String paymentId, String tppId) { Payment payment = paymentStore.get(paymentId); if (payment == null) { return PaymentResponse.builder() .error("Payment not found") .build(); } // Verify TPP ownership if (!payment.getTppId().equals(tppId)) { return PaymentResponse.builder() .error("Not authorized") .build(); } return PaymentResponse.builder() .paymentId(paymentId) .transactionStatus(payment.getStatus().name()) .statusUpdateDateTime(payment.getUpdatedAt()) .build(); } /** * Cancel payment */ public boolean cancelPayment(String paymentId, String tppId) { Payment payment = paymentStore.get(paymentId); if (payment == null || !payment.getTppId().equals(tppId)) { return false; } if (payment.getStatus() == PaymentStatus.RCVD || payment.getStatus() == PaymentStatus.PDNG) { payment.setStatus(PaymentStatus.CANC); payment.setUpdatedAt(LocalDateTime.now()); log.info("Payment cancelled: {}", paymentId); return true; } return false; } /** * Get payment details */ public Payment getPayment(String paymentId, String tppId) { Payment payment = paymentStore.get(paymentId); if (payment != null && payment.getTppId().equals(tppId)) { return payment; } return null; } /** * Process payment asynchronously */ private void processPaymentAsync(String paymentId) { CompletableFuture.runAsync(() -> { try { Payment payment = paymentStore.get(paymentId); // Update to pending payment.setStatus(PaymentStatus.PDNG); payment.setUpdatedAt(LocalDateTime.now()); // Simulate processing Thread.sleep(2000); // Update to accepted payment.setStatus(PaymentStatus.ACSC); payment.setUpdatedAt(LocalDateTime.now()); log.info("Payment completed: {}", paymentId); } catch (Exception e) { log.error("Payment processing failed: {}", paymentId, e); Payment payment = paymentStore.get(paymentId); payment.setStatus(PaymentStatus.RJCT); payment.setUpdatedAt(LocalDateTime.now()); } }); } /** * Payment entity */ @Data @Builder public static class Payment { private String paymentId; private String tppId; private String consentId; private String psuId; private PaymentType paymentType; private PaymentStatus status; private LocalDateTime createdAt; private LocalDateTime updatedAt; private PaymentRequest request; private List<String> transactionIds; } } 7. Confirmation of Funds Service (CoF)
package com.openbanking.psd2.cof; import com.openbanking.psd2.consent.ConsentManagementService; import com.openbanking.psd2.tpp.TPPAuthenticationService; import lombok.Data; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import java.math.BigDecimal; import java.time.LocalDateTime; @Slf4j @Service public class ConfirmationOfFundsService { private final TPPAuthenticationService tppAuthService; private final ConsentManagementService consentService; public ConfirmationOfFundsService(TPPAuthenticationService tppAuthService, ConsentManagementService consentService) { this.tppAuthService = tppAuthService; this.consentService = consentService; } /** * Funds confirmation request */ @Data @Builder public static class FundsConfirmationRequest { private String tppId; private String consentId; private String psuId; private String accountId; private String referenceNumber; private BigDecimal instructedAmount; private String currency; private String payee; private String remittanceInformation; private LocalDateTime requestedExecutionDate; } /** * Funds confirmation response */ @Data @Builder public static class FundsConfirmationResponse { private boolean fundsAvailable; private BigDecimal availableAmount; private String currency; private LocalDateTime timestamp; private String accountId; private String error; } /** * Check if funds are available */ public FundsConfirmationResponse checkFundsAvailability(FundsConfirmationRequest request) { // Validate TPP role if (!tppAuthService.hasRole(request.getTppId(), EIDASCertificateValidator.PSD2Role.PIISP)) { return FundsConfirmationResponse.builder() .error("TPP not authorized for CoF") .build(); } // Validate consent ConsentManagementService.ValidationResult validation = consentService.validateConsent( request.getConsentId(), request.getTppId(), request.getAccountId(), "COF_FUNDS_CONFIRMATION" ); if (!validation.isValid()) { return FundsConfirmationResponse.builder() .error(validation.getError()) .build(); } // Check funds (mock implementation) boolean fundsAvailable = checkFunds( request.getAccountId(), request.getInstructedAmount(), request.getCurrency() ); BigDecimal availableAmount = getAvailableBalance(request.getAccountId()); log.info("CoF check for TPP: {}, account: {}, result: {}", request.getTppId(), request.getAccountId(), fundsAvailable); return FundsConfirmationResponse.builder() .fundsAvailable(fundsAvailable) .availableAmount(availableAmount) .currency(request.getCurrency()) .timestamp(LocalDateTime.now()) .accountId(request.getAccountId()) .build(); } /** * Check funds (mock implementation) */ private boolean checkFunds(String accountId, BigDecimal amount, String currency) { // In production, check with core banking system BigDecimal availableBalance = getAvailableBalance(accountId); return availableBalance.compareTo(amount) >= 0; } /** * Get available balance (mock) */ private BigDecimal getAvailableBalance(String accountId) { // Mock implementation return new BigDecimal("5000.00"); } } 8. Strong Customer Authentication (SCA) Service
package com.openbanking.psd2.sca; import lombok.Data; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import java.security.SecureRandom; import java.time.LocalDateTime; import java.util.*; import java.util.concurrent.ConcurrentHashMap; @Slf4j @Service public class SCAService { private final Map<String, SCASession> scaSessions = new ConcurrentHashMap<>(); private final SecureRandom secureRandom = new SecureRandom(); /** * SCA methods */ public enum SCAMethod { SMS_OTP, APP_OTP, PUSH_NOTIFICATION, BIOMETRIC, HARDWARE_TOKEN, DECOUPLED } /** * SCA status */ public enum SCAStatus { PENDING, INITIATED, VERIFIED, FAILED, EXPIRED } /** * SCA session */ @Data @Builder public static class SCASession { private String scaId; private String psuId; private String tppId; private String consentId; private String paymentId; private SCAMethod method; private SCAStatus status; private String challenge; private String authenticationCode; private LocalDateTime createdAt; private LocalDateTime expiresAt; private LocalDateTime verifiedAt; private int retryCount; private String decoupledReference; private Map<String, Object> additionalData; } /** * Initiate SCA */ public SCASession initiateSCA(String psuId, String tppId, String consentId, String paymentId, SCAMethod method) { String scaId = "SCA-" + UUID.randomUUID().toString().substring(0, 8).toUpperCase(); String challenge = generateChallenge(method); SCASession session = SCASession.builder() .scaId(scaId) .psuId(psuId) .tppId(tppId) .consentId(consentId) .paymentId(paymentId) .method(method) .status(SCAStatus.PENDING) .challenge(challenge) .createdAt(LocalDateTime.now()) .expiresAt(LocalDateTime.now().plusMinutes(5)) .retryCount(0) .build(); scaSessions.put(scaId, session); // Deliver challenge based on method deliverChallenge(session); log.info("SCA initiated: {} for PSU: {}, method: {}", scaId, psuId, method); return session; } /** * Verify SCA */ public boolean verifySCA(String scaId, String authenticationCode) { SCASession session = scaSessions.get(scaId); if (session == null) { return false; } // Check expiration if (session.getExpiresAt().isBefore(LocalDateTime.now())) { session.setStatus(SCAStatus.EXPIRED); return false; } // Check retry limit if (session.getRetryCount() >= 3) { session.setStatus(SCAStatus.FAILED); return false; } // Verify code (in production, use proper verification) boolean verified = verifyCode(session.getChallenge(), authenticationCode); if (verified) { session.setStatus(SCAStatus.VERIFIED); session.setVerifiedAt(LocalDateTime.now()); session.setAuthenticationCode(authenticationCode); log.info("SCA verified: {}", scaId); } else { session.setRetryCount(session.getRetryCount() + 1); log.warn("SCA verification failed: {}, attempt: {}", scaId, session.getRetryCount()); } return verified; } /** * Get SCA session */ public Optional<SCASession> getSCASession(String scaId) { return Optional.ofNullable(scaSessions.get(scaId)); } /** * Check if SCA is required for transaction */ public boolean isSCARequired(String tppId, BigDecimal amount, String psuId) { // Implement PSD2 SCA requirements: // - Amount threshold (e.g., > 30 EUR) // - Risk-based analysis // - Whitelisted beneficiaries // - Recurring transactions if (amount.compareTo(new BigDecimal("30.00")) > 0) { return true; } // Additional rules based on PSD2 return false; } /** * Generate challenge based on method */ private String generateChallenge(SCAMethod method) { switch (method) { case SMS_OTP: case APP_OTP: // Generate 6-digit OTP return String.format("%06d", secureRandom.nextInt(1000000)); case PUSH_NOTIFICATION: return UUID.randomUUID().toString(); default: return UUID.randomUUID().toString(); } } /** * Deliver challenge to PSU */ private void deliverChallenge(SCASession session) { switch (session.getMethod()) { case SMS_OTP: // Send SMS with OTP log.info("SMS OTP sent to PSU: {}, challenge: {}", session.getPsuId(), session.getChallenge()); break; case APP_OTP: // Generate in-app OTP break; case PUSH_NOTIFICATION: // Send push notification break; case DECOUPLED: // Decoupled authentication (e.g., banking app) session.setDecoupledReference(UUID.randomUUID().toString()); break; } } /** * Verify authentication code (mock) */ private boolean verifyCode(String challenge, String code) { // In production, use proper verification return challenge.equals(code); } /** * Clean up expired sessions */ @Scheduled(cron = "0 */5 * * * *") // Every 5 minutes public void cleanupExpiredSessions() { LocalDateTime now = LocalDateTime.now(); scaSessions.values().stream() .filter(s -> s.getStatus() == SCAStatus.PENDING) .filter(s -> s.getExpiresAt().isBefore(now)) .forEach(s -> s.setStatus(SCAStatus.EXPIRED)); } } 9. Open Banking REST Controllers
package com.openbanking.psd2.controller; import com.openbanking.psd2.ais.AccountInformationService; import com.openbanking.psd2.consent.ConsentManagementService; import com.openbanking.psd2.pis.PaymentInitiationService; import com.openbanking.psd2.tpp.TPPAuthenticationService; import jakarta.servlet.http.HttpServletRequest; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import java.security.cert.X509Certificate; import java.time.LocalDateTime; import java.util.Map; @Slf4j @RestController @RequestMapping("/psd2/v1") @RequiredArgsConstructor public class OpenBankingController { private final TPPAuthenticationService tppAuthService; private final ConsentManagementService consentService; private final AccountInformationService aisService; private final PaymentInitiationService pisService; /** * AIS - Get accounts */ @GetMapping("/accounts") public ResponseEntity<?> getAccounts( @RequestHeader("Consent-ID") String consentId, @RequestParam(required = false) Boolean withBalance, HttpServletRequest request) { return authenticateAndExecute(request, (tpp, cert) -> { AccountInformationService.AccountListResponse response = aisService.getAccounts( tpp.getTppId(), consentId, request.getRemoteAddr(), withBalance != null ? withBalance : false ); if (response.isSuccess()) { return ResponseEntity.ok(response); } else { return ResponseEntity.badRequest().body(Map.of("error", response.getError())); } }); } /** * AIS - Get account details */ @GetMapping("/accounts/{accountId}") public ResponseEntity<?> getAccount( @PathVariable String accountId, @RequestHeader("Consent-ID") String consentId, HttpServletRequest request) { return authenticateAndExecute(request, (tpp, cert) -> { AccountInformationService.AccountResponse response = aisService.getAccount( tpp.getTppId(), consentId, accountId, request.getRemoteAddr() ); if (response.isSuccess()) { return ResponseEntity.ok(response); } else { return ResponseEntity.badRequest().body(Map.of("error", response.getError())); } }); } /** * AIS - Get transactions */ @GetMapping("/accounts/{accountId}/transactions") public ResponseEntity<?> getTransactions( @PathVariable String accountId, @RequestHeader("Consent-ID") String consentId, @RequestParam(required = false) LocalDateTime dateFrom, @RequestParam(required = false) LocalDateTime dateTo, HttpServletRequest request) { return authenticateAndExecute(request, (tpp, cert) -> { AccountInformationService.TransactionListResponse response = aisService.getTransactions( tpp.getTppId(), consentId, accountId, dateFrom != null ? dateFrom : LocalDateTime.now().minusMonths(3), dateTo != null ? dateTo : LocalDateTime.now(), request.getRemoteAddr() ); if (response.isSuccess()) { return ResponseEntity.ok(response); } else { return ResponseEntity.badRequest().body(Map.of("error", response.getError())); } }); } /** * AIS - Get balances */ @GetMapping("/accounts/{accountId}/balances") public ResponseEntity<?> getBalances( @PathVariable String accountId, @RequestHeader("Consent-ID") String consentId, HttpServletRequest request) { return authenticateAndExecute(request, (tpp, cert) -> { AccountInformationService.BalanceListResponse response = aisService.getBalances( tpp.getTppId(), consentId, accountId, request.getRemoteAddr() ); if (response.isSuccess()) { return ResponseEntity.ok(response); } else { return ResponseEntity.badRequest().body(Map.of("error", response.getError())); } }); } /** * PIS - Initiate payment */ @PostMapping("/payments") public ResponseEntity<?> initiatePayment( @RequestBody PaymentInitiationService.PaymentRequest paymentRequest, @RequestHeader(value = "Consent-ID", required = false) String consentId, HttpServletRequest request) { return authenticateAndExecute(request, (tpp, cert) -> { paymentRequest.setTppId(tpp.getTppId()); paymentRequest.setConsentId(consentId); PaymentInitiationService.PaymentResponse response = pisService.initiatePayment(paymentRequest, request.getRemoteAddr()); if (response.getError() == null) { return ResponseEntity.accepted().body(response); } else { return ResponseEntity.badRequest().body(Map.of("error", response.getError())); } }); } /** * PIS - Get payment status */ @GetMapping("/payments/{paymentId}/status") public ResponseEntity<?> getPaymentStatus( @PathVariable String paymentId, HttpServletRequest request) { return authenticateAndExecute(request, (tpp, cert) -> { PaymentInitiationService.PaymentResponse response = pisService.getPaymentStatus(paymentId, tpp.getTppId()); if (response.getError() == null) { return ResponseEntity.ok(response); } else { return ResponseEntity.badRequest().body(Map.of("error", response.getError())); } }); } /** * Consent - Create consent */ @PostMapping("/consents") public ResponseEntity<?> createConsent( @RequestBody ConsentManagementService.ConsentRequest consentRequest, HttpServletRequest request) { return authenticateAndExecute(request, (tpp, cert) -> { consentRequest.setTppId(tpp.getTppId()); consentRequest.setPsuIpAddress(request.getRemoteAddr()); consentRequest.setPsuUserAgent(request.getHeader("User-Agent")); ConsentManagementService.Consent consent = consentService.createConsent(consentRequest); return ResponseEntity.ok(Map.of( "consentId", consent.getConsentId(), "status", consent.getStatus(), "scaRequired", true )); }); } /** * Consent - Get consent */ @GetMapping("/consents/{consentId}") public ResponseEntity<?> getConsent( @PathVariable String consentId, HttpServletRequest request) { return authenticateAndExecute(request, (tpp, cert) -> { ConsentManagementService.ValidationResult result = consentService.validateConsent(consentId, tpp.getTppId(), "*", "VIEW"); if (result.isValid()) { return ResponseEntity.ok(result.getConsent()); } else { return ResponseEntity.badRequest().body(Map.of("error", result.getError())); } }); } /** * Consent - Revoke consent */ @DeleteMapping("/consents/{consentId}") public ResponseEntity<?> revokeConsent( @PathVariable String consentId, HttpServletRequest request) { return authenticateAndExecute(request, (tpp, cert) -> { boolean revoked = consentService.revokeConsent(consentId, tpp.getTppId()); if (revoked) { return ResponseEntity.ok(Map.of("message", "Consent revoked")); } else { return ResponseEntity.badRequest().body(Map.of("error", "Revocation failed")); } }); } /** * Authenticate TPP using client certificate */ private ResponseEntity<?> authenticateAndExecute( HttpServletRequest request, AuthenticatedAction action) { try { // Extract client certificate X509Certificate[] certs = (X509Certificate[]) request.getAttribute("javax.servlet.request.X509Certificate"); if (certs == null || certs.length == 0) { return ResponseEntity.status(401) .body(Map.of("error", "No client certificate provided")); } // Convert certificate to PEM String certPem = convertToPEM(certs[0]); // Authenticate TPP var tpp = tppAuthService.authenticateTPP(certPem, request.getRemoteAddr()); if (tpp.isEmpty()) { return ResponseEntity.status(401) .body(Map.of("error", "TPP authentication failed")); } // Execute the action return action.execute(tpp.get(), certs[0]); } catch (Exception e) { log.error("Open Banking API error", e); return ResponseEntity.status(500) .body(Map.of("error", "Internal server error")); } } /** * Convert X509Certificate to PEM format */ private String convertToPEM(X509Certificate cert) throws Exception { byte[] encoded = cert.getEncoded(); String base64 = Base64.getEncoder().encodeToString(encoded); StringBuilder pem = new StringBuilder(); pem.append("-----BEGIN CERTIFICATE-----\n"); // Split into 64-character lines for (int i = 0; i < base64.length(); i += 64) { int end = Math.min(i + 64, base64.length()); pem.append(base64, i, end).append("\n"); } pem.append("-----END CERTIFICATE-----"); return pem.toString(); } /** * Functional interface for authenticated actions */ @FunctionalInterface private interface AuthenticatedAction { ResponseEntity<?> execute(TPPAuthenticationService.TPPRegistration tpp, X509Certificate cert) throws Exception; } } 10. Security Configuration
package com.openbanking.psd2.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.www.BasicAuthenticationFilter; @Configuration @EnableWebSecurity public class SecurityConfig { @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .csrf().disable() .authorizeHttpRequests(authz -> authz .requestMatchers("/psd2/v1/**").authenticated() .anyRequest().permitAll() ) .addFilterBefore(new ClientCertificateFilter(), BasicAuthenticationFilter.class) .httpBasic().disable() .formLogin().disable(); return http.build(); } } /** * Client certificate authentication filter */ class ClientCertificateFilter extends OncePerRequestFilter { @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException { // Client certificate is already validated by TLS // We just need to make it available to the application chain.doFilter(request, response); } } Security Best Practices
1. Certificate Validation
public class CertificateSecurity { public void validateCertificate(X509Certificate cert) { // Check revocation status via OCSP/CRL checkRevocationStatus(cert); // Validate certificate chain validateCertificateChain(cert); // Check PSD2 specific extensions validatePSD2Extensions(cert); } } 2. Request Signing
public class RequestSigningService { public boolean verifyRequestSignature(String request, String signature, X509Certificate cert) { // Verify JWS signature // Verify nonce to prevent replay // Verify timestamp within acceptable window } } 3. Rate Limiting
@Component public class RateLimitingService { private final RateLimiter rateLimiter = RateLimiter.create(10); // 10 requests per second public boolean allowRequest(String tppId) { return rateLimiter.tryAcquire(); } } 4. Audit Logging
@Aspect @Component public class AuditAspect { @Around("@annotation(Audited)") public Object audit(ProceedingJoinPoint pjp) throws Throwable { // Log all Open Banking operations // Include TPP ID, timestamp, operation, result // Store in secure audit log } } Configuration Examples
application.yml
psd2: aspsp: name: "Demo Bank" bic: "DEMODE00XXX" base-url: "https://api.bank.com/psd2/v1" tpp: certificate-validation: enabled: true ocsp-enabled: true crl-enabled: true trusted-cas: - "classpath:certs/trusted-ca-1.pem" - "classpath:certs/trusted-ca-2.pem" consent: default-expiry-days: 90 max-frequency-per-day: 100 sca: methods: SMS_OTP,APP_OTP,PUSH_NOTIFICATION otp-length: 6 otp-expiry-seconds: 300 max-retries: 3 security: rate-limit: 100 rate-limit-period: 60 # seconds audit-log-enabled: true server: port: 8443 ssl: key-store: classpath:keystore.p12 key-store-password: changeit key-store-type: PKCS12 client-auth: need trust-store: classpath:truststore.p12 trust-store-password: changeit trust-store-type: PKCS12
Testing
@SpringBootTest @AutoConfigureMockMvc public class OpenBankingIntegrationTest { @Autowired private MockMvc mockMvc; @Test void testGetAccountsWithValidCertificate() throws Exception { // Create test certificate X509Certificate testCert = createTestCertificate(); mockMvc.perform(get("/psd2/v1/accounts") .header("Consent-ID", "CN-123456") .with(certificate(testCert))) .andExpect(status().isOk()) .andExpect(jsonPath("$.success").value(true)); } } Conclusion
This comprehensive Open Banking PSD2 implementation provides:
Key Features
- eIDAS certificate validation with QWAC/QSEAL support
- TPP authentication using mutual TLS
- Consent management per PSD2 requirements
- Account Information Service (AIS) for account access
- Payment Initiation Service (PIS) for payments
- Confirmation of Funds (CoF) for fund checks
- Strong Customer Authentication (SCA) with multiple methods
- Comprehensive audit logging for compliance
Security Benefits
- Mutual TLS with client certificate authentication
- Certificate validation with OCSP/CRL checks
- Role-based access control based on PSD2 roles
- Consent enforcement for all operations
- SCA for sensitive operations
- Rate limiting and monitoring
Compliance
- Full compliance with PSD2 regulatory requirements
- Support for all TPP roles (AISP, PISP, PIISP)
- Berlin Group/NextGenPSD2 compatible
- eIDAS certificate support
- Audit trails for regulatory reporting
This implementation provides a production-ready Open Banking API that meets all PSD2 requirements while maintaining high security and performance standards.
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/