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.

Leave a Reply

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


Macro Nepal Helper