Article
In the world of open banking and financial services, standard OAuth 2.0 and OpenID Connect security measures are insufficient. The Financial-Grade API (FAPI) defines a set of security profiles that extend OAuth and OIDC to meet the rigorous requirements of financial institutions, healthcare providers, and other industries handling highly sensitive data. For Java applications, implementing FAPI ensures compliance with open banking regulations (PSD2, CDR) and provides the highest level of API security.
What is FAPI?
FAPI is a working group within the OpenID Foundation that defines a security profile for APIs handling high-value or sensitive data. It builds upon OAuth 2.0 and OpenID Connect with additional requirements:
- Strong Authentication: Mandates high-security authentication methods
- Sender-Constrained Tokens: Prevents token misuse through binding to TLS or signatures
- JWT Secured Authorization Response Mode (JARM): Signed and encrypted authorization responses
- PAR (Pushed Authorization Requests): Prevents authorization request tampering
- Rich Client Registration: Detailed client metadata and validation
- Certificate-Bound Access Tokens: Tokens bound to TLS client certificates
FAPI Security Profiles
FAPI defines several conformance levels:
| Profile | Description | Use Case |
|---|---|---|
| FAPI 1.0 Baseline | Basic security profile | Read-only financial data |
| FAPI 1.0 Advanced | High-security with sender constraints | Write operations, payments |
| FAPI 2.0 | Next generation with improved security | All financial APIs |
| CIBA Profile | Decoupled authentication | Mobile approvals |
Why FAPI for Java Applications?
- Regulatory Compliance: Meet PSD2, Open Banking, CDR requirements
- High-Value Transactions: Secure payment initiation and account access
- Liability Shifting: Compliance with security standards reduces liability
- Interoperability: Works across different financial institutions
- Future-Proof: Alignment with evolving financial regulations
Implementing FAPI in Java with Spring Security
1. Maven Dependencies
<dependencies> <!-- Spring Security OAuth2 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-oauth2-resource-server</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-oauth2-client</artifactId> </dependency> <!-- Nimbus JOSE + JWT for advanced JWT operations --> <dependency> <groupId>com.nimbusds</groupId> <artifactId>nimbus-jose-jwt</artifactId> <version>9.37</version> </dependency> <!-- Bouncy Castle for crypto operations --> <dependency> <groupId>org.bouncycastle</groupId> <artifactId>bcprov-jdk15on</artifactId> <version>1.70</version> </dependency> <dependency> <groupId>org.bouncycastle</groupId> <artifactId>bcpkix-jdk15on</artifactId> <version>1.70</version> </dependency> </dependencies>
2. FAPI-Compliant Security Configuration
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.config.http.SessionCreationPolicy;
import org.springframework.security.oauth2.core.DelegatingOAuth2TokenValidator;
import org.springframework.security.oauth2.core.OAuth2TokenValidator;
import org.springframework.security.oauth2.jwt.*;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
@EnableWebSecurity
public class FAPISecurityConfig {
@Value("${spring.security.oauth2.resourceserver.jwt.issuer-uri}")
private String issuerUri;
@Value("${fapi.require-certificate-binding:true}")
private boolean requireCertificateBinding;
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(authz -> authz
.antMatchers("/.well-known/**").permitAll()
.antMatchers("/fapi/**").authenticated()
.anyRequest().authenticated()
)
.oauth2ResourceServer(oauth2 -> oauth2
.jwt(jwt -> jwt
.decoder(jwtDecoder())
.jwtAuthenticationConverter(jwtAuthenticationConverter())
)
.authenticationEntryPoint(new FapiAuthenticationEntryPoint())
)
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
// FAPI requires MTLS
.requiresChannel(channel -> channel
.anyRequest().requiresSecure()
);
return http.build();
}
@Bean
public JwtDecoder jwtDecoder() {
NimbusJwtDecoder jwtDecoder = JwtDecoders.fromIssuerLocation(issuerUri);
OAuth2TokenValidator<Jwt> audienceValidator = new AudienceValidator();
OAuth2TokenValidator<Jwt> issuerValidator = JwtValidators.createDefaultWithIssuer(issuerUri);
OAuth2TokenValidator<Jwt> cnfValidator = new CertificateBindingValidator(requireCertificateBinding);
OAuth2TokenValidator<Jwt> delegatingValidator = new DelegatingOAuth2TokenValidator<>(
issuerValidator, audienceValidator, cnfValidator
);
jwtDecoder.setJwtValidator(delegatingValidator);
return jwtDecoder;
}
private Converter<Jwt, AbstractAuthenticationToken> jwtAuthenticationConverter() {
JwtGrantedAuthoritiesConverter authoritiesConverter = new JwtGrantedAuthoritiesConverter();
authoritiesConverter.setAuthorityPrefix("");
return new JwtAuthenticationConverter() {
{
setJwtGrantedAuthoritiesConverter(authoritiesConverter);
}
};
}
}
3. Certificate-Bound Token Validator
import org.springframework.security.oauth2.core.OAuth2Error;
import org.springframework.security.oauth2.core.OAuth2TokenValidator;
import org.springframework.security.oauth2.core.OAuth2TokenValidatorResult;
import org.springframework.security.oauth2.jwt.Jwt;
import java.security.cert.X509Certificate;
import java.security.MessageDigest;
import java.util.Base64;
/**
* Validates that access tokens are bound to the client certificate (cnf claim)
* as required by FAPI (RFC 8705)
*/
public class CertificateBindingValidator implements OAuth2TokenValidator<Jwt> {
private static final OAuth2Error MISSING_CNF = new OAuth2Error(
"invalid_token", "Token missing certificate binding (cnf claim)", null
);
private static final OAuth2Error INVALID_CNF = new OAuth2Error(
"invalid_token", "Certificate binding validation failed", null
);
private final boolean requireBinding;
public CertificateBindingValidator(boolean requireBinding) {
this.requireBinding = requireBinding;
}
@Override
public OAuth2TokenValidatorResult validate(Jwt token) {
if (!requireBinding) {
return OAuth2TokenValidatorResult.success();
}
// Get client certificate from request context
X509Certificate[] certs = getClientCertificates();
if (certs == null || certs.length == 0) {
return OAuth2TokenValidatorResult.failure(MISSING_CNF);
}
X509Certificate clientCert = certs[0];
// Extract cnf claim from token
Map<String, Object> cnf = token.getClaim("cnf");
if (cnf == null) {
return OAuth2TokenValidatorResult.failure(MISSING_CNF);
}
// Check certificate thumbprint (x5t#S256)
String certThumbprint = cnf.get("x5t#S256");
if (certThumbprint != null) {
String calculated = calculateThumbprint(clientCert);
if (!certThumbprint.equals(calculated)) {
return OAuth2TokenValidatorResult.failure(INVALID_CNF);
}
}
// Check public key thumbprint (jkt)
String keyThumbprint = cnf.get("jkt");
if (keyThumbprint != null) {
String calculated = calculateKeyThumbprint(clientCert.getPublicKey());
if (!keyThumbprint.equals(calculated)) {
return OAuth2TokenValidatorResult.failure(INVALID_CNF);
}
}
return OAuth2TokenValidatorResult.success();
}
private X509Certificate[] getClientCertificates() {
// Retrieve from request context - implement based on your framework
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
if (requestAttributes instanceof ServletRequestAttributes) {
HttpServletRequest request = ((ServletRequestAttributes) requestAttributes).getRequest();
return (X509Certificate[]) request.getAttribute("javax.servlet.request.X509Certificate");
}
return null;
}
private String calculateThumbprint(X509Certificate cert) {
try {
MessageDigest md = MessageDigest.getInstance("SHA-256");
byte[] digest = md.digest(cert.getEncoded());
return Base64.getUrlEncoder().withoutPadding().encodeToString(digest);
} catch (Exception e) {
throw new RuntimeException("Failed to calculate certificate thumbprint", e);
}
}
private String calculateKeyThumbprint(PublicKey publicKey) {
try {
MessageDigest md = MessageDigest.getInstance("SHA-256");
byte[] digest = md.digest(publicKey.getEncoded());
return Base64.getUrlEncoder().withoutPadding().encodeToString(digest);
} catch (Exception e) {
throw new RuntimeException("Failed to calculate key thumbprint", e);
}
}
}
4. Pushed Authorization Requests (PAR) Implementation
import org.springframework.web.bind.annotation.*;
import org.springframework.http.ResponseEntity;
import java.time.Instant;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* FAPI Pushed Authorization Requests (RFC 9126)
* Clients push authorization parameters directly to the server
*/
@RestController
@RequestMapping("/fapi/par")
public class PushedAuthorizationRequestEndpoint {
private final Map<String, ParRequest> requestStore = new ConcurrentHashMap<>();
private final ClientValidator clientValidator;
@PostMapping
public ResponseEntity<ParResponse> pushAuthorizationRequest(
@RequestBody ParRequestParams params,
@RequestHeader("Authorization") String clientAuth) {
// Validate client authentication (must use MTLS or private_key_jwt)
Client client = clientValidator.validateAndAuthenticate(clientAuth);
// Validate request parameters per FAPI requirements
validateRequest(params, client);
// Generate request URI
String requestUri = generateRequestUri();
// Store request with expiration
ParRequest storedRequest = ParRequest.builder()
.requestUri(requestUri)
.params(params)
.clientId(client.getClientId())
.expiresAt(Instant.now().plusSeconds(90)) // FAPI requires 90s
.build();
requestStore.put(requestUri, storedRequest);
return ResponseEntity.ok(ParResponse.builder()
.requestUri(requestUri)
.expiresIn(90)
.build());
}
@GetMapping("/{requestUri}")
public ParRequestParams getRequest(@PathVariable String requestUri) {
ParRequest request = requestStore.get(requestUri);
if (request == null || request.getExpiresAt().isBefore(Instant.now())) {
throw new ParRequestNotFoundException();
}
return request.getParams();
}
private void validateRequest(ParRequestParams params, Client client) {
// FAPI requires specific validation rules
if (params.getResponseType() == null ||
!"code".equals(params.getResponseType())) {
throw new InvalidRequestException("response_type must be 'code'");
}
if (params.getClientId() == null ||
!params.getClientId().equals(client.getClientId())) {
throw new InvalidRequestException("client_id mismatch");
}
// Validate redirect_uri
if (!client.getRedirectUris().contains(params.getRedirectUri())) {
throw new InvalidRequestException("Invalid redirect_uri");
}
// Validate code challenge for PKCE (required in FAPI)
if (params.getCodeChallenge() == null) {
throw new InvalidRequestException("code_challenge required (PKCE)");
}
// Validate request object if present
if (params.getRequest() != null) {
validateRequestObject(params.getRequest(), client);
}
}
@Data
@Builder
public static class ParResponse {
private String requestUri;
private int expiresIn;
}
@Data
public static class ParRequestParams {
private String responseType;
private String clientId;
private String redirectUri;
private String scope;
private String state;
private String codeChallenge;
private String codeChallengeMethod;
private String request;
}
}
5. JWT Secured Authorization Response Mode (JARM)
import com.nimbusds.jose.*;
import com.nimbusds.jose.crypto.RSASSASigner;
import com.nimbusds.jwt.JWTClaimsSet;
import com.nimbusds.jwt.SignedJWT;
/**
* FAPI JARM - JWT Secured Authorization Response Mode
* Authorization responses are returned as signed and optionally encrypted JWTs
*/
@Service
public class JarmResponseService {
@Autowired
private JWKSource<SecurityContext> jwkSource;
@Autowired
private ClientCertificateService certService;
public String createJarmResponse(AuthorizationResult result, Client client) throws Exception {
// Build JWT claims
JWTClaimsSet.Builder claimsBuilder = new JWTClaimsSet.Builder()
.issuer("https://auth-server.example.com")
.audience(client.getClientId())
.issueTime(new Date())
.expirationTime(new Date(System.currentTimeMillis() + 60000))
.claim("code", result.getCode())
.claim("state", result.getState());
if (result.getError() != null) {
claimsBuilder.claim("error", result.getError());
claimsBuilder.claim("error_description", result.getErrorDescription());
}
JWTClaimsSet claims = claimsBuilder.build();
// Sign JWT
JWSHeader header = new JWSHeader.Builder(JWSAlgorithm.RS256)
.keyID(client.getJarmKeyId())
.build();
SignedJWT signedJWT = new SignedJWT(header, claims);
// Get client's signing key
JWK signingKey = jwkSource.getJWK(JWKSelector.fromJWSHeader(header), null)
.get(0);
RSASSASigner signer = new RSASSASigner((RSAKey) signingKey);
signedJWT.sign(signer);
// Optionally encrypt
if (client.requiresJarmEncryption()) {
return encryptJwt(signedJWT.serialize(), client);
}
return signedJWT.serialize();
}
private String encryptJwt(String signedJwt, Client client) throws Exception {
JWEHeader header = new JWEHeader.Builder(JWEAlgorithm.RSA_OAEP_256,
EncryptionMethod.A256GCM)
.contentType("JWT")
.build();
// Encrypt using client's public key
JWEEncrypter encrypter = new RSAEncrypter(client.getEncryptionKey());
JWEObject jweObject = new JWEObject(header, new Payload(signedJwt));
jweObject.encrypt(encrypter);
return jweObject.serialize();
}
}
6. FAPI-Compliant Token Endpoint
@RestController
@RequestMapping("/fapi/token")
public class FapiTokenEndpoint {
@Autowired
private TokenService tokenService;
@Autowired
private ClientAuthenticationService clientAuthService;
@Autowired
private CertificateBindingService bindingService;
@PostMapping
public ResponseEntity<TokenResponse> token(
@RequestParam("grant_type") String grantType,
@RequestParam(value = "code", required = false) String code,
@RequestParam(value = "redirect_uri", required = false) String redirectUri,
@RequestParam(value = "client_id", required = false) String clientId,
@RequestParam(value = "code_verifier", required = false) String codeVerifier,
@RequestHeader("Authorization") String clientAuth,
HttpServletRequest request) {
// Extract client certificate (required for FAPI)
X509Certificate[] certs = (X509Certificate[]) request.getAttribute(
"javax.servlet.request.X509Certificate");
// Authenticate client
Client client = clientAuthService.authenticate(clientAuth, certs);
if ("authorization_code".equals(grantType)) {
return handleAuthorizationCode(code, redirectUri, codeVerifier, client);
} else if ("refresh_token".equals(grantType)) {
return handleRefreshToken(codeVerifier, client);
} else {
throw new UnsupportedGrantTypeException();
}
}
private ResponseEntity<TokenResponse> handleAuthorizationCode(
String code, String redirectUri, String codeVerifier, Client client) {
// Validate authorization code
AuthorizationCode authCode = tokenService.validateAuthorizationCode(code, client);
// Verify PKCE (required in FAPI)
if (!validateCodeVerifier(authCode, codeVerifier)) {
throw new InvalidGrantException("PKCE validation failed");
}
// Create certificate-bound tokens
TokenResponse tokens = tokenService.createCertificateBoundTokens(
client,
authCode.getUser(),
getClientCertificate()
);
return ResponseEntity.ok(tokens);
}
private TokenResponse createCertificateBoundTokens(Client client, User user, X509Certificate cert) {
// Calculate certificate thumbprint
String thumbprint = calculateThumbprint(cert);
// Create access token with cnf claim
Map<String, Object> cnf = Map.of("x5t#S256", thumbprint);
String accessToken = Jwts.builder()
.setSubject(user.getUserId())
.setIssuer("https://auth-server.example.com")
.setAudience(client.getClientId())
.setExpiration(new Date(System.currentTimeMillis() + 3600000))
.claim("scope", "accounts payments")
.claim("cnf", cnf)
.signWith(SignatureAlgorithm.RS256, signingKey)
.compact();
// Create refresh token
String refreshToken = tokenService.createRefreshToken(client, user);
return TokenResponse.builder()
.accessToken(accessToken)
.tokenType("Bearer")
.expiresIn(3600)
.refreshToken(refreshToken)
.cnf(cnf)
.build();
}
}
7. FAPI Client Registration
@RestController
@RequestMapping("/fapi/register")
public class FapiClientRegistrationEndpoint {
@PostMapping
public ResponseEntity<ClientRegistrationResponse> registerClient(
@RequestBody ClientRegistrationRequest request) {
// Validate FAPI-specific client metadata
validateFapiMetadata(request);
// Generate client ID
String clientId = generateClientId();
// Store client with FAPI settings
FapiClient client = FapiClient.builder()
.clientId(clientId)
.clientName(request.getClientName())
.redirectUris(request.getRedirectUris())
.jwksUri(request.getJwksUri())
.jwks(request.getJwks())
.tokenEndpointAuthMethod(request.getTokenEndpointAuthMethod())
.tokenEndpointAuthSigningAlg(request.getTokenEndpointAuthSigningAlg())
.grantTypes(request.getGrantTypes())
.responseTypes(request.getResponseTypes())
.scope(request.getScope())
.softwareStatement(request.getSoftwareStatement())
.fapiProfile(request.getFapiProfile())
.requireSignedRequests(true)
.requirePushedAuthorizationRequests(true)
.requireJarm(true)
.requireCertificateBinding(true)
.createdAt(Instant.now())
.build();
clientRepository.save(client);
return ResponseEntity.ok(ClientRegistrationResponse.builder()
.clientId(clientId)
.clientSecret(null) // Not used in FAPI
.clientIdIssuedAt(Instant.now().getEpochSecond())
.build());
}
private void validateFapiMetadata(ClientRegistrationRequest request) {
// Validate JWKS URI or embedded JWKS
if (request.getJwksUri() == null && request.getJwks() == null) {
throw new InvalidClientMetadataException("jwks or jwks_uri required for FAPI");
}
// Validate token endpoint authentication method
String authMethod = request.getTokenEndpointAuthMethod();
if (!List.of("tls_client_auth", "private_key_jwt").contains(authMethod)) {
throw new InvalidClientMetadataException(
"FAPI requires tls_client_auth or private_key_jwt");
}
// Validate grant types
if (!request.getGrantTypes().contains("authorization_code")) {
throw new InvalidClientMetadataException(
"authorization_code grant required for FAPI");
}
// Validate response types
if (!request.getResponseTypes().contains("code")) {
throw new InvalidClientMetadataException("code response type required");
}
// Validate request object signing algorithm
String signingAlg = request.getRequestObjectSigningAlg();
if (!List.of("PS256", "ES256", "RS256").contains(signingAlg)) {
throw new InvalidClientMetadataException(
"FAPI requires PS256, ES256, or RS256 for request signing");
}
}
@Data
public static class ClientRegistrationRequest {
private String clientName;
private List<String> redirectUris;
private String jwksUri;
private JSONObject jwks;
private String tokenEndpointAuthMethod;
private String tokenEndpointAuthSigningAlg;
private List<String> grantTypes;
private List<String> responseTypes;
private String scope;
private String softwareStatement;
private FapiProfile fapiProfile;
}
public enum FapiProfile {
FAPI_1_BASELINE,
FAPI_1_ADVANCED,
FAPI_2
}
}
8. FAPI Request Object Validator
@Component
public class FapiRequestObjectValidator {
public void validateRequestObject(String requestObjectJwt, Client client) {
try {
// Parse and verify signature
SignedJWT signedJWT = SignedJWT.parse(requestObjectJwt);
// Verify signature using client's JWKS
JWKMatcher matcher = new JWKMatcher.Builder()
.keyID(signedJWT.getHeader().getKeyID())
.build();
List<JWK> matchingKeys = JWKSelector.select(matcher, client.getJwks());
if (matchingKeys.isEmpty()) {
throw new InvalidRequestException("No matching key for request object");
}
JWK jwk = matchingKeys.get(0);
JWSVerifier verifier = new RSASSAVerifier((RSAKey) jwk);
if (!signedJWT.verify(verifier)) {
throw new InvalidRequestException("Request object signature invalid");
}
// Validate claims
JWTClaimsSet claims = signedJWT.getJWTClaimsSet();
// Check expiration
if (claims.getExpirationTime().before(new Date())) {
throw new InvalidRequestException("Request object expired");
}
// Validate audience
if (!claims.getAudience().contains("https://auth-server.example.com")) {
throw new InvalidRequestException("Invalid audience");
}
// Validate response_type (must be 'code')
if (!"code".equals(claims.getStringClaim("response_type"))) {
throw new InvalidRequestException("response_type must be 'code'");
}
// Validate PKCE presence
if (claims.getStringClaim("code_challenge") == null) {
throw new InvalidRequestException("code_challenge required");
}
// Validate scope
String scope = claims.getStringClaim("scope");
if (scope == null || !scope.contains("openid")) {
throw new InvalidRequestException("openid scope required");
}
} catch (Exception e) {
throw new InvalidRequestException("Request object validation failed: " + e.getMessage());
}
}
}
9. FAPI Error Handling
@ControllerAdvice
public class FapiExceptionHandler {
@ExceptionHandler(InvalidRequestException.class)
public ResponseEntity<FapiError> handleInvalidRequest(InvalidRequestException e) {
return ResponseEntity
.status(HttpStatus.BAD_REQUEST)
.body(FapiError.builder()
.error("invalid_request")
.errorDescription(e.getMessage())
.build());
}
@ExceptionHandler(InvalidGrantException.class)
public ResponseEntity<FapiError> handleInvalidGrant(InvalidGrantException e) {
return ResponseEntity
.status(HttpStatus.BAD_REQUEST)
.body(FapiError.builder()
.error("invalid_grant")
.errorDescription(e.getMessage())
.build());
}
@ExceptionHandler(InvalidClientException.class)
public ResponseEntity<FapiError> handleInvalidClient(InvalidClientException e) {
return ResponseEntity
.status(HttpStatus.UNAUTHORIZED)
.header("WWW-Authenticate", "error=\"invalid_client\"")
.body(FapiError.builder()
.error("invalid_client")
.errorDescription(e.getMessage())
.build());
}
@ExceptionHandler(AccessDeniedException.class)
public ResponseEntity<FapiError> handleAccessDenied(AccessDeniedException e) {
return ResponseEntity
.status(HttpStatus.FORBIDDEN)
.body(FapiError.builder()
.error("access_denied")
.errorDescription(e.getMessage())
.build());
}
@Data
@Builder
public static class FapiError {
private String error;
private String errorDescription;
private String errorUri;
}
}
10. FAPI Compliance Testing
@SpringBootTest
@TestPropertySource(properties = "fapi.require-certificate-binding=true")
class FapiComplianceTest {
@Autowired
private TestRestTemplate restTemplate;
@Autowired
private FapiTestClient testClient;
@Test
void testCompleteFapiFlow() throws Exception {
// 1. Client registration
ClientRegistrationRequest registration = createTestClientRegistration();
ResponseEntity<ClientRegistrationResponse> registrationResponse =
restTemplate.postForEntity("/fapi/register", registration,
ClientRegistrationResponse.class);
String clientId = registrationResponse.getBody().getClientId();
// 2. Pushed Authorization Request
ParRequestParams parParams = createParRequest(clientId);
ResponseEntity<ParResponse> parResponse =
restTemplate.postForEntity("/fapi/par", parParams, ParResponse.class);
String requestUri = parResponse.getBody().getRequestUri();
// 3. Authorization request (using request_uri)
String authzUrl = String.format(
"/authorize?client_id=%s&request_uri=%s&response_type=code",
clientId, requestUri);
// 4. User authenticates and consents (simulated)
String authCode = simulateUserAuthz(authzUrl);
// 5. Token request with MTLS
HttpHeaders headers = new HttpHeaders();
headers.set("Authorization", "Bearer " + getClientAssertion(clientId));
MultiValueMap<String, String> tokenParams = new LinkedMultiValueMap<>();
tokenParams.add("grant_type", "authorization_code");
tokenParams.add("code", authCode);
tokenParams.add("redirect_uri", "https://client.example.com/callback");
tokenParams.add("code_verifier", parParams.getCodeVerifier());
HttpEntity<MultiValueMap<String, String>> tokenRequest =
new HttpEntity<>(tokenParams, headers);
ResponseEntity<TokenResponse> tokenResponse =
restTemplate.postForEntity("/fapi/token", tokenRequest,
TokenResponse.class);
// 6. Validate token binding
assertNotNull(tokenResponse.getBody().getCnf());
// 7. Access resource with MTLS-bound token
HttpHeaders resourceHeaders = new HttpHeaders();
resourceHeaders.setBearerAuth(tokenResponse.getBody().getAccessToken());
HttpEntity<?> resourceRequest = new HttpEntity<>(resourceHeaders);
ResponseEntity<String> resourceResponse =
restTemplate.exchange("/fapi/accounts", HttpMethod.GET,
resourceRequest, String.class);
assertEquals(200, resourceResponse.getStatusCodeValue());
}
}
11. FAPI Configuration Properties
# application-fapi.yml fapi: profile: FAPI_1_ADVANCED # Token configuration token: certificate-binding: enabled: true require-cnf-claim: true thumbprint-algorithm: SHA-256 access-token: expiry-seconds: 900 signing-alg: RS256 refresh-token: expiry-seconds: 86400 rotation-enabled: true # PAR configuration par: enabled: true request-uri-expiry: 90 require-signed-requests: true # JARM configuration jarm: enabled: true require-encryption: true signing-alg: PS256 encryption-alg: RSA-OAEP-256 encryption-enc: A256GCM # Client authentication client-auth: allowed-methods: - tls_client_auth - private_key_jwt require-mtls: true require-client-assertion: true client-assertion-expiry: 60 # PKCE requirements pkce: required: true allowed-methods: - S256 min-code-verifier-length: 43 max-code-verifier-length: 128 # Request object request-object: required: true allowed-signing-algs: - PS256 - ES256 - RS256 max-age-seconds: 60 # TLS configuration tls: min-version: TLSv1.2 required-cipher-suites: - TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 - TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 require-client-cert: true certificate-validation: check-revocation: true require-stapling: true
FAPI Implementation Checklist
- [ ] MTLS required for all endpoints
- [ ] Certificate-bound access tokens (cnf claim)
- [ ] Pushed Authorization Requests (PAR) implemented
- [ ] JWT Secured Authorization Response Mode (JARM)
- [ ] PKCE required with S256 method
- [ ] Request objects signed and validated
- [ ] Strong client authentication (private_key_jwt or tls_client_auth)
- [ ] Short-lived tokens (max 15 minutes for access tokens)
- [ ] Refresh token rotation
- [ ] Proper error responses with OAuth2 error codes
- [ ] OpenID Connect Discovery document
- [ ] JWKS endpoint for key distribution
- [ ] Software statement validation
- [ ] FAPI conformance testing passed
Best Practices
- Always use MTLS: FAPI requires client certificate authentication
- Bind tokens to certificates: Prevent token replay across clients
- Short token lifetimes: Access tokens should expire in minutes
- Use PAR for all requests: Prevents parameter tampering
- Sign request objects: Provides integrity and non-repudiation
- Encrypt sensitive responses: JARM with encryption for authorization responses
- Validate everything: Every parameter, signature, and claim
- Regular security testing: Use FAPI conformance test suites
- Monitor and audit: Log all FAPI operations for compliance
Conclusion
Financial-Grade API (FAPI) represents the highest standard of OAuth 2.0 and OpenID Connect security, specifically designed for the demanding requirements of financial services. For Java applications handling sensitive financial data, implementing FAPI provides:
- Regulatory compliance with open banking standards worldwide
- Strong security through certificate binding and signed requests
- Interoperability across financial institutions and third parties
- User protection through multi-factor authentication requirements
- Liability shifting by meeting security baseline requirements
Implementing FAPI in Java requires attention to detail across multiple security dimensions—from client authentication to token binding to response encryption. However, the effort is justified by the security guarantees it provides and the regulatory requirements it satisfies. As open banking continues to expand globally, FAPI will become the baseline for all financial APIs, making its implementation a critical investment for any organization in the financial sector.
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/