
In today's hyper-connected enterprise landscape, integrations with banking APIs represent one of the most critical security challenges. Financial institutions maintain extremely stringent security requirements when exposing their services via APIs, often beyond what standard integration platforms offer out-of-the-box.
In this article, I'll share technical insights based on a real implementation with the API of one of the world's largest American banks using SAP Integration Suite (formerly known as SAP Cloud Platform Integration). This implementation required a sophisticated multi-layer security architecture that goes well beyond traditional integration approaches.
What makes this solution particularly valuable is how it implements a true "defense in depth" strategy that:
While this implementation focuses specifically on obtaining an authentication token, the same security patterns and techniques can be applied to consume any banking API services that follow similar standards.
The JOSE (JavaScript Object Signing and Encryption) framework serves as the foundation for our security architecture. JOSE consists of a collection of specifications that standardize methods for secure data exchange:
Our banking integration relies heavily on JWS for signing and JWE for encryption, with both working together to create a comprehensive security solution.
Understanding the structure of JWT tokens is crucial for implementing this security architecture correctly:
Base64UrlEncode(Header) + "." + Base64UrlEncode(Payload) + "." + Base64UrlEncode(Signature)
eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsIng1YyI6WyJNSUlELi4uIl19.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIn0.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
Base64UrlEncode(Header) + "." +
Base64UrlEncode(EncryptedKey) + "." +
Base64UrlEncode(IV) + "." +
Base64UrlEncode(Ciphertext) + "." +
Base64UrlEncode(AuthenticationTag)
eyJhbGciOiJSU0EtT0FFUC0yNTYiLCJlbmMiOiJBMjU2R0NNIn0.a4OJ4Me9Qqz2y1DbJvEfH0M6U6BtM6CipNjFIgZ3W8M.yZW18nJiHMrRfJaR.xLI9vX-TFG_OchACM-MZZg.DNdcijA0zxgtgWKT7vn7Vw
Our approach implements a three-layer security pyramid that provides comprehensive protection:
The foundation of any banking security architecture is mutual TLS authentication:
Note on Certificate Separation: Most banking institutions require using separate certificates for different security functions - one certificate exclusively for establishing the mTLS connection, and another for JWS signing and JWE decryption operations. This separation of concerns is a security best practice that prevents compromise of one function from affecting the others.
The second layer implements digital signatures using the JWS standard (RFC 7515):
// JWS Header Example
{
"alg": "RS256",
"typ": "JWT",
"x5c": ["MIID...base64 encoded certificate..."]
}
Implementation insight: Standard Integration Suite components such as PKCS7/CMS Signer are not suitable for this scenario, as they generate signatures in PKCS#7 format incompatible with JWS. Instead, we implemented custom Groovy scripts using the Nimbus JOSE+JWT library, which perfectly handles the JWS standard.
The third layer implements hybrid encryption using JWE (RFC 7516):
// JWE Header Example
{
"alg": "RSA-OAEP-256",
"enc": "A256GCM"
}
The critical component: AAD (Additional Authenticated Data)
A sophisticated aspect of JWE is the handling of AAD in GCM encryption:
The Nimbus JOSE+JWT library handles this complexity automatically, ensuring that the AAD is correctly applied according to the JWE standard. This was a critical factor in choosing Nimbus for our implementation, as it eliminates a common source of errors in manual implementations.
Before beginning implementation, it's necessary to ensure the following elements:
Before implementing the integration flow, a critical step is the exchange of certificates between your organization and the bank:
This bidirectional certificate exchange establishes the mutual trust necessary for the first layer of our security pyramid (mTLS). Without this exchange properly configured, API communication will fail at the transport level, regardless of the correctness of the message content.
Many banking institutions provide specific portals or processes for certificate registration and management, often requiring additional verification steps to ensure certificate authenticity.
For this implementation, we generated the necessary key pairs using OpenSSL and then imported them into the SAP Integration Suite keystore. Here's the general approach we followed (with anonymized names):
# Generate CSR for signing and encryption certificate
openssl req -newkey rsa:2048 -keyout sign_encrypt.key -out sign_encrypt.csr
# Submit CSR to Certificate Authority (CA) for OV validation
# After approval, the CA provides you with certificate.crt (signed with OV validation):
# Package as PKCS12 (.p12) for import into SAP Integration Suite
openssl pkcs12 -export \
-out sign_encrypt.p12 \
-inkey sign_encrypt.key \
-in certificate.crt \
-name "sign-encrypt.company.com"
The same process was followed for both required certificates: one for mTLS connection and another for JWS signing and JWE decryption operations. It's important to note that certificate.crt is the signed certificate provided by a Certificate Authority (CA) with Organization Validation (OV) after submitting the CSR.
The integration flow follows this structure:
Configure the flow start:
Add Groovy Scripts in sequence:
Configure HTTPS connection:
Process the response:
Below is a detailed representation of the integration flow showing the transformations that occur at each step:
[Original JSON Payload]
↓
[Groovy Script: JWS Signature]
↓ Transformation: JSON → JWS (header.payload.signature)
↓ Algorithm: RS256
↓
[Signed JWS]
↓
[Groovy Script: JWE Encryption]
↓ Transformation: JWS → JWE (header.encrypted_key.iv.ciphertext.tag)
↓ Algorithms: RSA-OAEP-256 (key) + A256GCM (content) with AAD
↓
[Encrypted JWE]
↓
[Send to Bank API]
↓
[JWE Response from Bank]
↓
[Groovy Script: JWE Decryption]
↓ Transformation: JWE → JSON/JWT
↓ Verification: AAD + GCM tag
↓
[Decrypted Response]
↓
[Groovy Script: Token Extraction]
↓ Extraction: access_token, token_type, expires_in
↓
[Token and Metadata]
Originally, we implemented the JWE decryption manually, handling components like Base64URL decoding, RSA transformation, AAD configuration, and GCM tag validation. However, this approach was complex and error-prone.
By leveraging the Nimbus JOSE+JWT library, we've significantly simplified the implementation while maintaining the same high security standards. Let's look at each of the three key scripts:
import com.nimbusds.jose.JWSAlgorithm
import com.nimbusds.jose.JWSHeader
import com.nimbusds.jose.JWSSigner
import com.nimbusds.jose.JWSObject
import com.nimbusds.jose.Payload
import com.nimbusds.jose.crypto.RSASSASigner
import com.nimbusds.jose.util.Base64
import com.sap.gateway.ip.core.customdev.util.Message
import java.security.KeyPair
import java.security.interfaces.RSAPrivateKey
import java.security.cert.X509Certificate
def Message processData(Message message) {
def messageLog = messageLogFactory.getMessageLog(message)
final String SIGNING_KEY_ALIAS = "sign_n_encrypt_cert"
// Enable/disable logging (change to true for debugging)
final boolean DEBUG = false
try {
// 1. Get the payload to sign
String payload = message.getBody(String.class)
// 2. Load private key and certificate using KeystoreService
def keystoreService = com.sap.it.api.ITApiFactory.getService(com.sap.it.api.keystore.KeystoreService.class, null)
if (keystoreService == null) throw new IllegalStateException("Could not obtain keystore service")
KeyPair keyPair = keystoreService.getKeyPair(SIGNING_KEY_ALIAS)
if (keyPair == null) throw new IllegalStateException("Could not retrieve KeyPair with alias: " + SIGNING_KEY_ALIAS)
RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate()
X509Certificate certificate = (X509Certificate) keystoreService.getCertificate(SIGNING_KEY_ALIAS)
if (certificate == null) throw new IllegalStateException("Could not retrieve certificate with alias: " + SIGNING_KEY_ALIAS)
// 3. Prepare certificate for x5c (Base64 of DER)
String certB64 = Base64.encode(certificate.getEncoded()).toString()
// 4. Create JWS header with x5c
JWSHeader jwsHeader = new JWSHeader.Builder(JWSAlgorithm.RS256)
.x509CertChain(Collections.singletonList(new Base64(certB64)))
.build()
if (DEBUG) {
messageLog?.addAttachmentAsString("JWS-Debug",
"Signing process initialized:\n" +
"- Certificate: " + SIGNING_KEY_ALIAS + "\n" +
"- Algorithm: RS256\n" +
"- Payload preview: " + payload.substring(0, Math.min(50, payload.length())) + "...\n" +
"- JWS Header: " + jwsHeader.toString(),
"text/plain")
}
// 5. Create signer with private key
JWSSigner signer = new RSASSASigner(privateKey)
// 6. Create JWS object and sign it
JWSObject jwsObject = new JWSObject(jwsHeader, new Payload(payload))
jwsObject.sign(signer)
// 7. Get compact serialization of JWS
String jwsEncoded = jwsObject.serialize()
// 8. Set JWS as the new message body
message.setBody(jwsEncoded)
return message
} catch (Exception e) {
messageLog?.addAttachmentAsString("Error", "JWS signing failed: " + e.getMessage(), "text/plain")
throw e
}
}
import com.nimbusds.jose.EncryptionMethod
import com.nimbusds.jose.JWEAlgorithm
import com.nimbusds.jose.JWEHeader
import com.nimbusds.jose.JWEObject
import com.nimbusds.jose.Payload
import com.nimbusds.jose.crypto.RSAEncrypter
import com.sap.gateway.ip.core.customdev.util.Message
import java.security.interfaces.RSAPublicKey
def Message processData(Message message) {
def messageLog = messageLogFactory.getMessageLog(message)
// Certificate alias for encryption
final String BANK_CERT_ALIAS = "bank_encryption_cert"
// Enable/disable logging (change to true for debugging)
final boolean DEBUG = false
try {
// 1. Get JWS payload from message body
String jwsPayload = message.getBody(String.class)
// 2. Load certificate using KeystoreService API
def keystoreService = com.sap.it.api.ITApiFactory.getApi(com.sap.it.api.keystore.KeystoreService.class, null)
if (keystoreService == null) throw new IllegalStateException("Could not obtain keystore service")
def certificate = keystoreService.getCertificate(BANK_CERT_ALIAS)
if (certificate == null) throw new IllegalStateException("Could not retrieve certificate with alias: " + BANK_CERT_ALIAS)
RSAPublicKey publicKey = (RSAPublicKey) certificate.getPublicKey()
// 3. Create JWE header with RSA-OAEP-256 and A256GCM
JWEHeader jweHeader = new JWEHeader.Builder(JWEAlgorithm.RSA_OAEP_256, EncryptionMethod.A256GCM)
.build()
// 4. Create encrypter and JWE object with JWS payload
RSAEncrypter encrypter = new RSAEncrypter(publicKey)
JWEObject jweObject = new JWEObject(jweHeader, new Payload(jwsPayload))
// 5. Perform encryption
jweObject.encrypt(encrypter)
// 6. Get serialized JWE and set as message body
String jweEncoded = jweObject.serialize()
message.setBody(jweEncoded)
if (DEBUG) {
// Log basic info about input and output
messageLog?.addAttachmentAsString("JWE-Debug",
"Encryption completed:\n" +
"- Input (JWS) length: " + jwsPayload.length() + " chars\n" +
"- Output (JWE) length: " + jweEncoded.length() + " chars\n" +
"- JWE preview: " + jweEncoded.substring(0, Math.min(50, jweEncoded.length())) + "...",
"text/plain")
}
return message
} catch (Exception e) {
messageLog?.addAttachmentAsString("Error", "JWE encryption error: " + e.getMessage(), "text/plain")
throw e
}
}
This script represents our biggest improvement. Instead of manually handling the complex JWE structure and GCM decryption with AAD, we leverage Nimbus to handle these details automatically:
import com.nimbusds.jose.JWEDecrypter
import com.nimbusds.jose.JWEObject
import com.nimbusds.jose.Payload
import com.nimbusds.jose.crypto.RSADecrypter
import com.sap.gateway.ip.core.customdev.util.Message
import java.security.KeyPair
import java.security.interfaces.RSAPrivateKey
import groovy.json.JsonOutput
import groovy.json.JsonSlurper
import java.util.Base64
def Message processData(Message message) {
def messageLog = messageLogFactory.getMessageLog(message)
final String DECRYPT_KEY_ALIAS = "sign_n_encrypt_cert"
// Enable/disable logging (change to true for debugging)
final boolean DEBUG = false
try {
// 1. Get and validate the JWE token
String jweToken = message.getBody(String.class)
if (jweToken == null || jweToken.count('.') != 4) {
messageLog?.addAttachmentAsString("Warning", "Invalid JWE token format", "text/plain")
return message
}
// 2. Get the private key from keystore
def keystoreService = com.sap.it.api.ITApiFactory.getService(com.sap.it.api.keystore.KeystoreService.class, null)
if (keystoreService == null) throw new IllegalStateException("KeystoreService not available")
KeyPair keyPair = keystoreService.getKeyPair(DECRYPT_KEY_ALIAS)
if (keyPair == null) throw new IllegalStateException("KeyPair not found for alias: " + DECRYPT_KEY_ALIAS)
RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate()
// 3. Decrypt the JWE using Nimbus
JWEObject jweObject = JWEObject.parse(jweToken)
JWEDecrypter decrypter = new RSADecrypter(privateKey)
jweObject.decrypt(decrypter)
// 4. Get the decrypted payload
Payload payload = jweObject.getPayload()
String decryptedContent = payload.toString()
// Only one strategic debug point
if (DEBUG) {
messageLog?.addAttachmentAsString("Decrypted-Content",
"JWE successfully decrypted.\n" +
"Content type: " + (decryptedContent.startsWith("eyJ") ? "JWT/JWS token" : "Raw data") + "\n" +
"Preview: " + decryptedContent.substring(0, Math.min(50, decryptedContent.length())) + "...",
"text/plain")
}
// 5. Process the content based on its type
String finalOutput
JsonSlurper jsonSlurper = new JsonSlurper()
// Handle JWT/JWS content
if (decryptedContent.startsWith("eyJ") && decryptedContent.contains(".")) {
// Extract payload from JWT
String[] parts = decryptedContent.split("\\.")
if (parts.length >= 2) {
String payloadBase64 = parts[1]
byte[] payloadBytes = Base64.getUrlDecoder().decode(payloadBase64)
String payloadJson = new String(payloadBytes, "UTF-8")
// Parse and format JSON
def jsonObject = jsonSlurper.parseText(payloadJson)
finalOutput = JsonOutput.prettyPrint(JsonOutput.toJson(jsonObject))
} else {
finalOutput = decryptedContent
}
} else {
// Try to parse as regular JSON
try {
def jsonObject = jsonSlurper.parseText(decryptedContent)
finalOutput = JsonOutput.prettyPrint(JsonOutput.toJson(jsonObject))
} catch (Exception e) {
// Not JSON, use as plain text
finalOutput = decryptedContent
}
}
// 6. Set the result as message body
message.setBody(finalOutput)
return message
} catch (Exception e) {
messageLog?.addAttachmentAsString("Error", "Decryption failed: " + e.getMessage(), "text/plain")
throw e
}
}
The optimized implementation using Nimbus JOSE+JWT offers several significant advantages:
For the scripts to work correctly, it's necessary to add the following libraries to the integration package in SAP Integration Suite:
To add these JARs to your SAP Integration Suite package:
Even with our optimized implementation, some issues may still arise:
While this implementation focuses on obtaining an authentication token, the same security patterns can be adapted for any API endpoint that requires this level of security. Once you've obtained the access token:
Each script includes a DEBUG flag (default is false) that can be set to true for troubleshooting:
final boolean DEBUG = true
This provides diagnostic information without excessive logging, making it ideal for implementation and troubleshooting.
Implementing this multi-layer security architecture in SAP Integration Suite requires a deep understanding of cryptographic standards and careful configuration of each component. By leveraging the Nimbus JOSE+JWT library, we've achieved a robust, standards-compliant implementation with significantly less complexity than manual approaches.
The key takeaways from this implementation are:
While this implementation specifically targets the authentication token flow, the same patterns can be applied to any API integration that requires advanced security measures. By following this approach, you can meet the most stringent security requirements while maintaining compatibility with modern API standards.
You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.