Open Banking Down Under: Implementing Consumer Data Right (CDR) in Java Applications

The Consumer Data Right (CDR) is Australia's ambitious open banking framework, giving consumers control over their data and enabling secure sharing with accredited third parties. Building on the FAPI security profile, CDR represents one of the world's most comprehensive data rights regimes. For Java developers building financial applications in Australia—or integrating with Australian banks—understanding and implementing CDR requirements is essential for compliance and interoperability.

What is the Consumer Data Right?

The CDR is a regulatory framework that gives consumers the right to:

  1. Access their data: View their banking, energy, and telecommunications data in machine-readable formats
  2. Share their data: Authorize accredited third parties to access their data
  3. Compare and switch: Use their data to find better deals and services

CDR builds on the FAPI security profile with additional Australian-specific requirements:

AspectCDR Requirement
GovernanceAccredited Data Recipients (ADRs) must be registered
SecurityFAPI Advanced Profile with additional CDR constraints
PrivacyStrict consent management and purpose limitation
PerformanceStrict response time requirements (e.g., 2 seconds for API calls)
Availability99.5% uptime requirement for CDR APIs

CDR Architecture Overview

┌─────────────┐ ┌──────────────┐ ┌─────────────┐ │ Consumer │────────▶│ ADR │────────▶│ Bank │ │ (User) │◀────────│ (Third │◀────────│ (Data │ └─────────────┘ │ Party) │ │ Holder) │ └──────────────┘ └─────────────┘ │ │ ▼ ▼ ┌─────────────────────────────────────┐ │ Register (ACC) │ │ Australian Competition Council │ └─────────────────────────────────────┘

Implementing a CDR Data Holder (Bank) in Java

1. Dependencies

<dependencies> <!-- Spring Boot and Security --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-oauth2-resource-server</artifactId> </dependency> <!-- CDR Specific --> <dependency> <groupId>io.cdr</groupId> <artifactId>cdr-java-sdk</artifactId> <version>1.2.0</version> </dependency> <!-- JWT handling --> <dependency> <groupId>com.nimbusds</groupId> <artifactId>nimbus-jose-jwt</artifactId> <version>9.37.3</version> </dependency> <!-- OpenAPI validation --> <dependency> <groupId>org.openapi4j</groupId> <artifactId>openapi-parser</artifactId> <version>1.0.7</version> </dependency> </dependencies>

2. CDR Configuration

@Configuration @EnableConfigurationProperties(CdrProperties.class) public class CdrConfiguration { @Bean public CdrValidator cdrValidator() { return new CdrValidator(); } @Bean public CdrMetricsCollector cdrMetricsCollector() { return new CdrMetricsCollector(); } @Bean @ConditionalOnMissingBean public Clock cdrClock() { return Clock.systemUTC(); } } @ConfigurationProperties(prefix = "cdr") @Data public class CdrProperties { private String dataHolderBrandId; private String version = "1.30.0"; // Latest CDR version private Duration maxResponseTime = Duration.ofSeconds(2); private List<String> supportedVersions = List.of("1.29.0", "1.30.0"); private String registerBaseUrl = "https://api.cdr.gov.au"; private boolean strictValidation = true; }

3. CDR Authentication Filter

@Component public class CdrAuthenticationFilter extends OncePerRequestFilter { private static final Logger logger = LoggerFactory.getLogger(CdrAuthenticationFilter.class); @Autowired private CdrTokenValidator tokenValidator; @Autowired private CdrMetricsCollector metricsCollector; @Override protected void doFilterInternal( HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException { long startTime = System.nanoTime(); String requestId = UUID.randomUUID().toString(); try { // Set CDR headers response.setHeader("x-v", determineVersion(request)); response.setHeader("x-fapi-interaction-id", requestId); // Validate FAPI interaction ID (must be present) String fapiInteractionId = request.getHeader("x-fapi-interaction-id"); if (fapiInteractionId == null || !isValidUuid(fapiInteractionId)) { throw new CdrValidationException("x-fapi-interaction-id", "Invalid or missing"); } // Validate CDR version header String version = request.getHeader("x-v"); if (version == null) { version = "1"; } // Extract and validate token String authHeader = request.getHeader("Authorization"); if (authHeader != null && authHeader.startsWith("Bearer ")) { String token = authHeader.substring(7); // Validate token (FAPI-compliant, sender-constrained) CdrTokenValidationResult result = tokenValidator.validateToken(token, request); if (!result.isValid()) { metricsCollector.recordAuthFailure(result.getFailureReason()); sendCdrError(response, HttpStatus.UNAUTHORIZED, "Invalid token: " + result.getFailureReason()); return; } // Set authentication in context CdrAuthentication auth = new CdrAuthentication(result); SecurityContextHolder.getContext().setAuthentication(auth); // Record consent details for audit recordConsentUsage(result); } chain.doFilter(request, response); } catch (CdrValidationException e) { sendCdrError(response, HttpStatus.BAD_REQUEST, e.getMessage()); } finally { long duration = System.nanoTime() - startTime; metricsCollector.recordRequestDuration(duration, request.getRequestURI()); // Check response time SLA (2 seconds for CDR) if (duration > TimeUnit.SECONDS.toNanos(2)) { logger.warn("CDR response time exceeded 2 seconds: {} ms", duration / 1_000_000); metricsCollector.recordSlaViolation(request.getRequestURI()); } } } private void sendCdrError(HttpServletResponse response, HttpStatus status, String message) throws IOException { response.setStatus(status.value()); response.setContentType("application/json"); CdrErrorResponse error = CdrErrorResponse.builder() .code(status.value()) .title(status.getReasonPhrase()) .detail(message) .build(); response.getWriter().write(new ObjectMapper().writeValueAsString(error)); } private String determineVersion(HttpServletRequest request) { String requestedVersion = request.getHeader("x-v"); if (requestedVersion != null && supportedVersions.contains(requestedVersion)) { return requestedVersion; } return "1"; // Default to oldest supported version } }

4. CDR Token Validator

@Component public class CdrTokenValidator { @Autowired private CdrRegisterClient registerClient; @Autowired private CdrProperties cdrProperties; public CdrTokenValidationResult validateToken(String token, HttpServletRequest request) { try { // Parse JWT SignedJWT signedJWT = SignedJWT.parse(token); // 1. Validate signature against ADR's JWKS if (!validateSignature(signedJWT)) { return CdrTokenValidationResult.invalid("Invalid signature"); } // 2. Validate issuer (must be accredited ADR) String issuer = signedJWT.getJWTClaimsSet().getIssuer(); if (!isAccreditedAdr(issuer)) { return CdrTokenValidationResult.invalid("Unauthorized issuer: " + issuer); } // 3. Validate CDR-specific claims JWTClaimsSet claims = signedJWT.getJWTClaimsSet(); // Check CDR arrangement ID String cdrArrangementId = claims.getStringClaim("cdr_arrangement_id"); if (cdrArrangementId == null) { return CdrTokenValidationResult.invalid("Missing cdr_arrangement_id"); } // Validate consent is still active if (!isConsentActive(cdrArrangementId)) { return CdrTokenValidationResult.invalid("Consent expired or revoked"); } // 4. Validate scope matches consent String scope = claims.getStringClaim("scope"); if (!validateScopeAgainstConsent(scope, cdrArrangementId)) { return CdrTokenValidationResult.invalid("Scope exceeds consented permissions"); } // 5. Validate sender constraint (MTLS) X509Certificate clientCert = extractClientCertificate(request); if (clientCert == null) { return CdrTokenValidationResult.invalid("MTLS client certificate required"); } // Check certificate binding (cnf claim) Map<String, Object> cnf = (Map<String, Object>) claims.getClaim("cnf"); if (cnf == null || !cnf.containsKey("x5t#S256")) { return CdrTokenValidationResult.invalid("Missing certificate binding (cnf)"); } String expectedThumbprint = (String) cnf.get("x5t#S256"); String actualThumbprint = calculateThumbprint(clientCert); if (!expectedThumbprint.equals(actualThumbprint)) { return CdrTokenValidationResult.invalid("Certificate binding mismatch"); } // 6. Validate token expiry Date now = new Date(); if (claims.getExpirationTime().before(now)) { return CdrTokenValidationResult.invalid("Token expired"); } return CdrTokenValidationResult.valid( issuer, cdrArrangementId, scope, claims.getStringClaim("client_id") ); } catch (ParseException | JOSEException e) { logger.error("Failed to validate CDR token", e); return CdrTokenValidationResult.invalid("Malformed token"); } } private boolean validateSignature(SignedJWT signedJWT) throws JOSEException { // Fetch ADR's JWKS from CDR Register String issuer = signedJWT.getJWTClaimsSet().getIssuer(); JWKSet jwkSet = registerClient.getAdrJwks(issuer); // Find matching key JWK jwk = jwkSet.getKeyByKeyId(signedJWT.getHeader().getKeyID()); if (jwk == null) { return false; } // Create verifier JWSVerifier verifier = new RSASSAVerifier((RSAPublicKey) jwk.toRSAKey().toPublicKey()); return signedJWT.verify(verifier); } private boolean isAccreditedAdr(String issuer) { // Check CDR Register for accreditation status return registerClient.isAccredited(issuer); } private boolean isConsentActive(String cdrArrangementId) { // Check database for valid consent Consent consent = consentRepository.findByArrangementId(cdrArrangementId); return consent != null && consent.isActive(); } private boolean validateScopeAgainstConsent(String scope, String arrangementId) { Consent consent = consentRepository.findByArrangementId(arrangementId); Set<String> requestedScopes = Set.of(scope.split(" ")); Set<String> consentedScopes = consent.getScopes(); return consentedScopes.containsAll(requestedScopes); } }

5. CDR API Controllers

@RestController @RequestMapping("/cds-au/v1") @Validated public class CdrBankingController { private static final Logger logger = LoggerFactory.getLogger(CdrBankingController.class); @Autowired private CdrService cdrService; @Autowired private CdrResponseBuilder responseBuilder; @Autowired private CdrMetricsCollector metricsCollector; @GetMapping("/banking/accounts") public ResponseEntity<ResponseBankingAccountListV2> getAccounts( @RequestHeader("x-v") String version, @RequestHeader("x-fapi-interaction-id") String fapiInteractionId, @RequestParam(required = false) String page, @RequestParam(required = false) @Min(1) @Max(1000) Integer pageSize, @RequestParam(required = false) String openStatus, @RequestParam(required = false) String productCategory, @AuthenticationPrincipal CdrAuthentication auth) { long startTime = System.nanoTime(); try { logger.debug("Getting accounts for arrangement: {}", auth.getCdrArrangementId()); // Validate consent includes accounts scope if (!auth.hasScope("bank:accounts.basic:read")) { throw new CdrAuthorizationException("Insufficient scope for accounts access"); } // Fetch accounts based on consent List<BankingAccount> accounts = cdrService.getAccounts( auth.getCdrArrangementId(), openStatus, productCategory ); // Apply pagination Pageable pageable = createPageable(page, pageSize); Page<BankingAccount> page = paginate(accounts, pageable); // Build response ResponseBankingAccountListV2 response = responseBuilder.buildAccountListResponse( page, version ); // Set response headers HttpHeaders headers = new HttpHeaders(); headers.set("x-v", version); headers.set("x-fapi-interaction-id", fapiInteractionId); headers.set("x-min-v", "1"); return ResponseEntity.ok() .headers(headers) .body(response); } catch (Exception e) { metricsCollector.recordError("getAccounts", e.getClass().getSimpleName()); throw e; } finally { long duration = System.nanoTime() - startTime; metricsCollector.recordEndpointDuration("getAccounts", duration); // CDR requires response times under 2 seconds if (duration > TimeUnit.SECONDS.toNanos(2)) { logger.warn("Slow accounts response: {} ms", duration / 1_000_000); } } } @GetMapping("/banking/accounts/{accountId}/transactions") public ResponseEntity<ResponseBankingTransactionList> getTransactions( @PathVariable String accountId, @RequestHeader("x-v") String version, @RequestHeader("x-fapi-interaction-id") String fapiInteractionId, @RequestParam(required = false) String oldestTime, @RequestParam(required = false) String newestTime, @RequestParam(required = false) @Min(1) @Max(1000) Integer limit, @AuthenticationPrincipal CdrAuthentication auth) { // Validate account access if (!cdrService.canAccessAccount(auth.getCdrArrangementId(), accountId)) { throw new CdrAuthorizationException("Account not authorized"); } // Validate consent includes transaction scope if (!auth.hasScope("bank:transactions:read")) { throw new CdrAuthorizationException("Insufficient scope for transaction access"); } // Fetch transactions List<BankingTransaction> transactions = cdrService.getTransactions( accountId, parseDateTime(oldestTime), parseDateTime(newestTime), limit ); // Build response ResponseBankingTransactionList response = responseBuilder.buildTransactionListResponse( transactions, version ); HttpHeaders headers = new HttpHeaders(); headers.set("x-v", version); headers.set("x-fapi-interaction-id", fapiInteractionId); return ResponseEntity.ok() .headers(headers) .body(response); } @GetMapping("/banking/products") public ResponseEntity<ResponseBankingProductListV2> getProducts( @RequestHeader("x-v") String version, @RequestHeader("x-fapi-interaction-id") String fapiInteractionId, @RequestParam(required = false) String effective, @RequestParam(required = false) String updatedSince, @RequestParam(required = false) String brand, @RequestParam(required = false) String productCategory, @RequestParam(required = false) String page, @RequestParam(required = false) @Min(1) @Max(1000) Integer pageSize) { // Products API doesn't require authentication (public data) List<BankingProduct> products = cdrService.getProducts( effective, updatedSince, brand, productCategory ); Page<BankingProduct> page = paginate(products, createPageable(page, pageSize)); ResponseBankingProductListV2 response = responseBuilder.buildProductListResponse( page, version ); HttpHeaders headers = new HttpHeaders(); headers.set("x-v", version); headers.set("x-fapi-interaction-id", fapiInteractionId); return ResponseEntity.ok() .headers(headers) .body(response); } }

6. Consent Management

@Service public class CdrConsentService { @Autowired private ConsentRepository consentRepository; @Autowired private CdrMetricsCollector metricsCollector; /** * Create a new consent arrangement */ @Transactional public Consent createConsent(CreateConsentRequest request, String adrId) { // Validate request validateConsentRequest(request); // Generate CDR arrangement ID String arrangementId = generateCdrArrangementId(); // Create consent Consent consent = Consent.builder() .arrangementId(arrangementId) .adrId(adrId) .customerId(request.getCustomerId()) .scopes(request.getScopes()) .accounts(request.getAccountIds()) .duration(request.getDuration()) .status(ConsentStatus.PENDING) .createdAt(Instant.now()) .expiryTime(Instant.now().plus(request.getDuration())) .purpose(request.getPurpose()) .build(); consentRepository.save(consent); metricsCollector.recordConsentCreated(adrId); return consent; } /** * Authorize a consent (customer approval) */ @Transactional public Consent authorizeConsent(String arrangementId, String authCode) { Consent consent = consentRepository.findByArrangementId(arrangementId) .orElseThrow(() -> new ConsentNotFoundException(arrangementId)); // Validate auth code (from customer authentication) if (!validateAuthCode(authCode, consent.getCustomerId())) { throw new CdrValidationException("Invalid authorization code"); } consent.setStatus(ConsentStatus.ACTIVE); consent.setAuthorizedAt(Instant.now()); consentRepository.save(consent); metricsCollector.recordConsentAuthorized(); return consent; } /** * Revoke consent (customer or ADR initiated) */ @Transactional public void revokeConsent(String arrangementId, RevocationReason reason) { Consent consent = consentRepository.findByArrangementId(arrangementId) .orElseThrow(() -> new ConsentNotFoundException(arrangementId)); consent.setStatus(ConsentStatus.REVOKED); consent.setRevokedAt(Instant.now()); consent.setRevocationReason(reason); consentRepository.save(consent); // Invalidate all tokens for this arrangement tokenService.invalidateTokensForArrangement(arrangementId); metricsCollector.recordConsentRevoked(reason); // Notify ADR of revocation notifyAdrOfRevocation(consent); } /** * Validate consent is active and sufficient for requested scope */ public void validateConsentForScope(String arrangementId, String requiredScope) { Consent consent = consentRepository.findByArrangementId(arrangementId) .orElseThrow(() -> new ConsentNotFoundException(arrangementId)); if (consent.getStatus() != ConsentStatus.ACTIVE) { throw new ConsentInactiveException("Consent is not active"); } if (consent.getExpiryTime().isBefore(Instant.now())) { throw new ConsentExpiredException("Consent has expired"); } if (!consent.getScopes().contains(requiredScope)) { throw new InsufficientScopeException( "Consent does not include required scope: " + requiredScope ); } } private String generateCdrArrangementId() { return "urn:cds:arrangement:" + UUID.randomUUID().toString(); } @Data @Builder public static class Consent { private String arrangementId; private String adrId; private String customerId; private Set<String> scopes; private List<String> accounts; private Duration duration; private ConsentStatus status; private Instant createdAt; private Instant authorizedAt; private Instant expiryTime; private Instant revokedAt; private RevocationReason revocationReason; private String purpose; } public enum ConsentStatus { PENDING, ACTIVE, REVOKED, EXPIRED } public enum RevocationReason { CUSTOMER_REQUESTED, ADR_REQUESTED, DATA_HOLDER_INITIATED, CONSENT_EXPIRED } }

7. CDR Register Integration

@Component public class CdrRegisterClient { private static final Logger logger = LoggerFactory.getLogger(CdrRegisterClient.class); @Autowired private RestTemplate restTemplate; @Autowired private CdrProperties cdrProperties; @Autowired private CacheManager cacheManager; /** * Validate ADR accreditation status */ public boolean isAccredited(String adrId) { Cache cache = cacheManager.getCache("adrAccreditation"); Boolean cached = cache.get(adrId, Boolean.class); if (cached != null) { return cached; } try { String url = cdrProperties.getRegisterBaseUrl() + "/accreditation/" + adrId; AccreditationResponse response = restTemplate.getForObject(url, AccreditationResponse.class); boolean isAccredited = response != null && response.isAccredited(); cache.put(adrId, isAccredited); return isAccredited; } catch (Exception e) { logger.error("Failed to check accreditation for ADR: {}", adrId, e); return false; } } /** * Fetch ADR's JWKS for token validation */ public JWKSet getAdrJwks(String adrId) { Cache cache = cacheManager.getCache("adrJwks"); JWKSet cached = cache.get(adrId, JWKSet.class); if (cached != null) { return cached; } try { String url = cdrProperties.getRegisterBaseUrl() + "/jwks/" + adrId; String jwksJson = restTemplate.getForObject(url, String.class); JWKSet jwkSet = JWKSet.parse(jwksJson); cache.put(adrId, jwkSet); return jwkSet; } catch (Exception e) { logger.error("Failed to fetch JWKS for ADR: {}", adrId, e); throw new CdrRegisterException("Failed to fetch ADR JWKS", e); } } /** * Get data holder brand details */ public DataHolderBrand getDataHolderBrand() { try { String url = cdrProperties.getRegisterBaseUrl() + "/data-holders/" + cdrProperties.getDataHolderBrandId(); return restTemplate.getForObject(url, DataHolderBrand.class); } catch (Exception e) { logger.error("Failed to fetch data holder brand", e); throw new CdrRegisterException("Failed to fetch brand details", e); } } @Data public static class AccreditationResponse { private String adrId; private String legalName; private boolean accredited; private List<String> accreditedDataRecipientTypes; private Instant accreditationDate; private Instant revocationDate; } }

8. CDR Response Builder

@Component public class CdrResponseBuilder { private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ISO_OFFSET_DATE_TIME; public ResponseBankingAccountListV2 buildAccountListResponse( Page<BankingAccount> accounts, String version) { ResponseBankingAccountListV2 response = new ResponseBankingAccountListV2(); ResponseBankingAccountListV2Data data = new ResponseBankingAccountListV2Data(); List<BankingAccountV2> accountList = accounts.getContent().stream() .map(this::convertToV2) .collect(Collectors.toList()); data.setAccounts(accountList); response.setData(data); // Add links and meta ResponseLinks links = buildLinks(accounts); ResponseMeta meta = buildMeta(accounts); response.setLinks(links); response.setMeta(meta); return response; } public ResponseBankingTransactionList buildTransactionListResponse( List<BankingTransaction> transactions, String version) { ResponseBankingTransactionList response = new ResponseBankingTransactionList(); ResponseBankingTransactionListData data = new ResponseBankingTransactionListData(); List<BankingTransactionV2> transactionList = transactions.stream() .map(this::convertToV2) .collect(Collectors.toList()); data.setTransactions(transactionList); response.setData(data); return response; } public ErrorListResponse buildErrorResponse( List<CdrError> errors, String fapiInteractionId) { ErrorListResponse response = new ErrorListResponse(); List<Error> errorList = errors.stream() .map(this::convertToError) .collect(Collectors.toList()); response.setErrors(errorList); return response; } private BankingAccountV2 convertToV2(BankingAccount account) { BankingAccountV2 v2 = new BankingAccountV2(); v2.setAccountId(account.getAccountId()); v2.setDisplayName(account.getDisplayName()); v2.setNickname(account.getNickname()); v2.setAccountNumber(account.getAccountNumber()); v2.setBsb(account.getBsb()); v2.setProductName(account.getProductName()); v2.setProductCategory(account.getProductCategory()); v2.setBalance(account.getBalance()); v2.setCurrency(account.getCurrency()); v2.setOpenStatus(account.isOpen() ? "OPEN" : "CLOSED"); v2.setCreationDate(account.getCreationDate().format(DATE_FORMATTER)); return v2; } private BankingTransactionV2 convertToV2(BankingTransaction transaction) { BankingTransactionV2 v2 = new BankingTransactionV2(); v2.setTransactionId(transaction.getTransactionId()); v2.setAccountId(transaction.getAccountId()); v2.setStatus(transaction.getStatus()); v2.setDescription(transaction.getDescription()); v2.setPostingDateTime(transaction.getPostingDateTime().format(DATE_FORMATTER)); v2.setValueDateTime(transaction.getValueDateTime().format(DATE_FORMATTER)); v2.setExecutionDateTime(transaction.getExecutionDateTime().format(DATE_FORMATTER)); v2.setAmount(transaction.getAmount()); v2.setCurrency(transaction.getCurrency()); v2.setReference(transaction.getReference()); v2.setMerchantName(transaction.getMerchantName()); v2.setMerchantCategoryCode(transaction.getMerchantCategoryCode()); return v2; } private ResponseLinks buildLinks(Page<?> page) { ResponseLinks links = new ResponseLinks(); links.setSelf(page.getCurrentUrl()); links.setFirst(page.getFirstUrl()); links.setPrev(page.hasPrevious() ? page.getPrevUrl() : null); links.setNext(page.hasNext() ? page.getNextUrl() : null); links.setLast(page.getLastUrl()); return links; } private ResponseMeta buildMeta(Page<?> page) { ResponseMeta meta = new ResponseMeta(); PaginationMeta pagination = new PaginationMeta(); pagination.setTotalRecords((int) page.getTotalElements()); pagination.setTotalPages(page.getTotalPages()); meta.setPagination(pagination); return meta; } private Error convertToError(CdrError cdrError) { Error error = new Error(); error.setCode(cdrError.getCode()); error.setTitle(cdrError.getTitle()); error.setDetail(cdrError.getDetail()); error.setMeta(cdrError.getMeta()); return error; } }

9. CDR Metrics and Monitoring

@Component public class CdrMetricsCollector { private final MeterRegistry meterRegistry; private final Map<String, Timer> endpointTimers = new ConcurrentHashMap<>(); private final AtomicLong activeConsents = new AtomicLong(); public CdrMetricsCollector(MeterRegistry meterRegistry) { this.meterRegistry = meterRegistry; initializeMetrics(); } private void initializeMetrics() { // CDR-specific metrics meterRegistry.gauge("cdr.consents.active", activeConsents); // Response time metrics Timer.builder("cdr.api.response.time") .description("CDR API response times") .publishPercentiles(0.5, 0.95, 0.99) .register(meterRegistry); // Error rates by endpoint Counter.builder("cdr.api.errors") .description("CDR API errors by type") .tag("endpoint", "all") .register(meterRegistry); } public void recordRequestDuration(long durationNanos, String endpoint) { Timer timer = endpointTimers.computeIfAbsent(endpoint, e -> Timer.builder("cdr.api.response.time") .tag("endpoint", e) .register(meterRegistry) ); timer.record(durationNanos, TimeUnit.NANOSECONDS); // Check SLA compliance (2 seconds) if (durationNanos > TimeUnit.SECONDS.toNanos(2)) { meterRegistry.counter("cdr.api.sla.violations", "endpoint", endpoint ).increment(); } } public void recordError(String endpoint, String errorType) { meterRegistry.counter("cdr.api.errors", "endpoint", endpoint, "errorType", errorType ).increment(); } public void recordConsentCreated(String adrId) { meterRegistry.counter("cdr.consents.created", "adr", adrId ).increment(); activeConsents.incrementAndGet(); } public void recordConsentRevoked(CdrConsentService.RevocationReason reason) { meterRegistry.counter("cdr.consents.revoked", "reason", reason.name() ).increment(); activeConsents.decrementAndGet(); } public void recordAuthFailure(String reason) { meterRegistry.counter("cdr.auth.failures", "reason", reason ).increment(); } public void recordTokenValidation(String result) { meterRegistry.counter("cdr.token.validation", "result", result ).increment(); } public void recordDataTransfer(String adrId, String dataType, long sizeBytes) { meterRegistry.counter("cdr.data.transferred", "adr", adrId, "dataType", dataType ).increment(sizeBytes); } }

10. CDR Compliance Testing

@SpringBootTest @AutoConfigureMockMvc @TestPropertySource(properties = { "cdr.strict-validation=true", "cdr.max-response-time=2s" }) public class CdrComplianceTest { @Autowired private MockMvc mockMvc; @Autowired private CdrTestHelper testHelper; @Test public void testFapiInteractionIdRequired() throws Exception { // Missing x-fapi-interaction-id mockMvc.perform(get("/cds-au/v1/banking/accounts") .header("Authorization", "Bearer " + testHelper.getValidToken())) .andExpect(status().isBadRequest()) .andExpect(jsonPath("$.errors[0].code").value("invalid_header")); } @Test public void testValidInteractionId() throws Exception { String interactionId = UUID.randomUUID().toString(); mockMvc.perform(get("/cds-au/v1/banking/accounts") .header("x-fapi-interaction-id", interactionId) .header("Authorization", "Bearer " + testHelper.getValidToken())) .andExpect(status().isOk()) .andExpect(header().string("x-fapi-interaction-id", interactionId)); } @Test public void testVersionHeader() throws Exception { // Test with valid version mockMvc.perform(get("/cds-au/v1/banking/accounts") .header("x-v", "1.30.0") .header("x-fapi-interaction-id", UUID.randomUUID().toString()) .header("Authorization", "Bearer " + testHelper.getValidToken())) .andExpect(status().isOk()) .andExpect(header().string("x-v", "1.30.0")); // Test with unsupported version mockMvc.perform(get("/cds-au/v1/banking/accounts") .header("x-v", "999.0.0") .header("x-fapi-interaction-id", UUID.randomUUID().toString()) .header("Authorization", "Bearer " + testHelper.getValidToken())) .andExpect(status().isOk()) .andExpect(header().string("x-v", "1")); // Defaults to oldest supported } @Test public void testSenderConstrainedToken() throws Exception { // Get token with certificate binding String token = testHelper.obtainTokenWithCertificate(); // Use with correct certificate mockMvc.perform(get("/cds-au/v1/banking/accounts") .header("x-fapi-interaction-id", UUID.randomUUID().toString()) .header("Authorization", "Bearer " + token) .with(testHelper.withClientCertificate())) .andExpect(status().isOk()); // Use with wrong certificate mockMvc.perform(get("/cds-au/v1/banking/accounts") .header("x-fapi-interaction-id", UUID.randomUUID().toString()) .header("Authorization", "Bearer " + token) .with(testHelper.withWrongCertificate())) .andExpect(status().isUnauthorized()); } @Test public void testResponseTimeSLA() throws Exception { long startTime = System.nanoTime(); mockMvc.perform(get("/cds-au/v1/banking/accounts") .header("x-fapi-interaction-id", UUID.randomUUID().toString()) .header("Authorization", "Bearer " + testHelper.getValidToken())) .andExpect(status().isOk()); long duration = System.nanoTime() - startTime; long durationMs = TimeUnit.NANOSECONDS.toMillis(duration); // Should be under 2 seconds assertTrue(durationMs < 2000, "Response time exceeded 2 seconds: " + durationMs + "ms"); } @Test public void testScopeEnforcement() throws Exception { // Token with basic scope only String basicToken = testHelper.obtainTokenWithScope("bank:accounts.basic:read"); // Should work for accounts endpoint mockMvc.perform(get("/cds-au/v1/banking/accounts") .header("x-fapi-interaction-id", UUID.randomUUID().toString()) .header("Authorization", "Bearer " + basicToken)) .andExpect(status().isOk()); // Should fail for transactions (requires additional scope) mockMvc.perform(get("/cds-au/v1/banking/accounts/123/transactions") .header("x-fapi-interaction-id", UUID.randomUUID().toString()) .header("Authorization", "Bearer " + basicToken)) .andExpect(status().isForbidden()); } @Test public void testConsentRevocation() throws Exception { // Create consent String arrangementId = testHelper.createConsent("bank:accounts.basic:read"); // Get token String token = testHelper.obtainTokenForArrangement(arrangementId); // Use token mockMvc.perform(get("/cds-au/v1/banking/accounts") .header("x-fapi-interaction-id", UUID.randomUUID().toString()) .header("Authorization", "Bearer " + token)) .andExpect(status().isOk()); // Revoke consent testHelper.revokeConsent(arrangementId); // Try again - should fail mockMvc.perform(get("/cds-au/v1/banking/accounts") .header("x-fapi-interaction-id", UUID.randomUUID().toString()) .header("Authorization", "Bearer " + token)) .andExpect(status().isUnauthorized()); } @Test public void testPagination() throws Exception { // Request first page MvcResult result = mockMvc.perform(get("/cds-au/v1/banking/accounts") .param("page-size", "2") .header("x-fapi-interaction-id", UUID.randomUUID().toString()) .header("Authorization", "Bearer " + testHelper.getValidToken())) .andExpect(status().isOk()) .andExpect(jsonPath("$.data.accounts.length()").value(2)) .andExpect(jsonPath("$.links.next").exists()) .andReturn(); // Extract next link and follow String nextLink = JsonPath.read(result.getResponse().getContentAsString(), "$.links.next"); mockMvc.perform(get(URI.create(nextLink)) .header("x-fapi-interaction-id", UUID.randomUUID().toString()) .header("Authorization", "Bearer " + testHelper.getValidToken())) .andExpect(status().isOk()) .andExpect(jsonPath("$.data.accounts.length()").value(2)); } }

CDR Implementation Checklist

RequirementImplementationVerified
FAPI Advanced Profile✅ CdrAuthenticationFilter✓
MTLS Client Authentication✅ MutualTlsConfig✓
Sender-Constrained Tokens✅ CdrTokenValidator✓
Consent Management✅ CdrConsentService✓
CDR Register Integration✅ CdrRegisterClient✓
Response Time < 2 seconds✅ CdrMetricsCollector✓
Availability 99.5%✅ Health checks & monitoring✓
Version Headers (x-v)✅ CdrAuthenticationFilter✓
Interaction ID Tracking✅ CdrAuthenticationFilter✓
Error Responses (RFC 7807)✅ CdrResponseBuilder✓
Pagination✅ CdrBankingController✓
Audit Logging✅ CdrAuditLogger✓

Conclusion

The Consumer Data Right represents a fundamental shift in how financial data is accessed and shared in Australia. For Java developers, implementing CDR requires careful attention to security, performance, and regulatory compliance.

The combination of FAPI Advanced Profile, sender-constrained tokens, consent management, and rigorous performance monitoring creates a robust foundation for CDR compliance. By leveraging Spring Security, Nimbus JOSE, and the CDR-specific libraries presented in this guide, teams can build CDR-compliant APIs that meet the stringent requirements of the Australian regulatory framework.

As CDR expands beyond banking to energy and telecommunications, the patterns and practices established for banking will serve as a blueprint for broader data right implementations. Java applications that implement CDR correctly today will be well-positioned to participate in Australia's growing open data economy.

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