Secure Communication
NOTE: In the current version of the API the secure communication is not required and will not be enforced in production.
Nonetheless, it is highly recommended to implement the secure communication, as it will be mandatory in the next API version.
Encryption
All request and response payloads can be encrypted to increase the security for the whole API communication. This can be achieved by signing the payload with JWS and afterwards encrypting it with JWE.
Signing with JWS
The signing of the payload is done with the sender's private key. The used algorithm has to be RSA_USING_SHA512
. The content type of the signed payload is application/json
.
Pseudo code:
yourPrivateKeyForRequest{id, key} = loadYourPrivateKey(id=X)
request = {body: "{...jsonRequest...}", contentType: "application/json"}
jwsToken = jwsSign({
payload: request.body,
// "cty" property for JWS header
header.payloadContentType: request.contentType,
key: yourPrivateKeyForRequest.key,
// "kid" property for JWS header
header.keyId: yourPrivateKeyForRequest.id,
// "alg´" property for JWS header
header.algorithm: RSA_USING_SHA512,
keyValidation: true
})
Encryption with JWE
The encryption is done with the public key of the recipient. The used algorithm has to be RSA_OAEP_256
. The algorithm for the content encryption has to be AES_256_GCM
. The content type of the signed payload is application/jose
.
Pseudo code:
jwsToken = // the JWS Signed Data
recipientPublicKey{id, key} = loadRecipientPublicKey(id=Y)
jweToken = jweEncrypt({
payload: jwsToken,
// "cty" property for JWE header
header.payloadContentType: "application/jose",
key: recipientPublicKey.key,
// "kid" property for JWE header
header.keyId: recipientPublicKey.id,
// "alg" property for JWE header
header.algorithm: RSA_OAEP_256,
// "enc" property for JWE header
header.encryptionAlgorithm: AES_256_GCM,
keyValidation: true
})
Decryption
The decryption has to be executed in reverse order compared to the encryption. First the JWE token is decrypted and then the JSON Web Signature (JWS) is verified.
Decryption of the JWE Token
Pseudo code:
jweToken{header, cipher-text, auth-tag} // the JWE encrypted data
yourPrivateKeyForResponse{id, key} = loadYourPrivateKey(id=jweToken.header.keyId)
jwsToken{header, payload, signature} = jweDecrypt({
payload: jweToken,
key: yourPrivateKeyForResponse.key,
// "alg" property from JWE header
header.algorithm: jweTokenForResponse.header.algorithm,
// "enc" property from JWE header
header.encryptionAlgorithm: jweTokenForResponse.header.encryptionAlgorithm,
keyValidation: true
})
Verification of the JSON Web Signature (JWS)
Pseudo code:
jwsToken = // the JWS Signed Data
theirPublicKeyForResponse{id, key} = loadTheirPublicKey(id=jwsTokenForResponse.header.keyId)
verifiedData{data, contentType} = jwsVerify({
payload: jwsToken,
key: theirPublicKeyForResponse.key,
// "alg" property from JWS header
header.algorithm: jwsToken.header.algorithm,
keyValidation: true
})
// Example for verifiedData
=> {data: "...jsonResponse...", "contentType": "application/json"}
Request/Response Structure
The content type of a request or a response with an encrypted payload must always be application/jose
. The body contains the JWE token.
Here is an example for an encrypted API call:
Request
PUT /xs2a/v1/sessions HTTP/1.1
Content-Type: application/jose
Authorization: Token <Token>
Host: <Host>
eyJhbGciOiJSU0EtT0FFUC0yNTYiLCJlbmMiOiJBMjU2R0NNIiwia2lkIjoiMkFGOTJCMUQifQ.OxHUvzpTq1HMz0EmWVkxAG8SAs14PJUJWIt2XOK4I8IRx-uNxAwqNQLSVx7reWvCra538PLuNmteY2LQsKwttZyMu9AR7hV-a-u8_wqxEKCVxkJHUh4VgGlJ-7aMzmTk1FMon0HI-xtm7okfXI5D62KAv_6VXRMlZUf-pdggDyPHPk7worLgcuOBy0AUOY0yiYN3vQrN8Qpv0m4nEQiHL1wj4fzkfma6MuNS62Q7E_2Pl60uKDYNpm8jdBMmFrsuFQXos3JspZvsIalmghagFQA5YtBzlw-Dry8tjxCezXZzx1M21vf_lVcDDO0BaNjROExdTKfwWlRWnMv30Lx6nA.ApNGrmoYssIbo-in.DNfg73P1u8EN9gOxmKbwmhJtJJlzXbuw89_3XIzRAOg_9-HUm-a9pZJGO1PQjetfW7AY5RxQjUcm8LI6_QbCDuybXDh064Z52zLVS4D1P8VNVRJN4lc0_crs7OYOnfHw77X03WSUaZ_CGvKdHdI-GW8ve3g-_OiVHshWwoC0D6Y5wciURBVTZrQKadfjeJoUHFQEUQJGdZP24O91ZAPYxE8w2_4KRAcH3OwFB-bI5NzeYj72Pr3Oa2D8y4PxIydwsuRSR1ZALddZbk6HomYVTfdWTricrx3RJRH2PtoKUveTYnPObeeYIrU1F7ph8-UgWIKwYeRzoCiwZdy4DVuqlrZuxIHUUWSa07eSp-ZGbqfOx9pm640Cz-3EfYYZgqZW_-WNaZnDyQdtCAH_i_XICuRWzPWI4mDf2Sw917PRO5xsoRkp0rZfT7K2AnFiE6vFblA_QZz5T3D41sqlPG-0zCVSwGwbRfmvt3-VdNewpj82ToHZgCOQYYJFcr9c_M_80oAtyu9fvAPiFJHc9t8GmaUvlR_kFD3BNeCq2q7ynj779ixrK-tKkdW2UECQQe5YeaHdaKlVtewJslwE54Ymd6kNZmBQ_OrSA4nAvNOjpaSAD9a1dyJHYyDddwlYdTrkS9VHiS-xejDIleMf7hfBgHqa0RevlPLamW5rMty9nKOQv1XZg9QQllKpxUEqmrubkHg.4g1a-iY77voFCpDHht2iMw
Response
HTTP/1.1 201 Created
Content-Type: application/jose
eyJhbGciOiJSU0EtT0FFUC0yNTYiLCJlbmMiOiJBMjU2R0NNIiwia2lkIjoiNUYwQzlEMzcifQ.QjfGSGF2HbEH63bZu_0HKvDpILk4JbRZ_LxFh1XJ034fwzbQEFv8KjXLUgVGWT2yb6Wk72M4fnwgVVWtgyYX1ZN0y8aRYHkj58xFOxN4aUKPAquScbjIQUZBjD6d-DQSj9K8bWtRI2WmmWIzH8-1gR3Ykpia5FxE0qGEr7aBSC15a2n0__v3fnLUL821RKnBCW_MQbiFXh2dYMZD54-caz8w_KGuhiHvss1CMl0-DCw_Xr5xHlNsgkDNts8ycyAzuxkLLgK_Au3MFIvtWzKSDfKbj2aNr8tu5gc0mGya25VZ0KaRsuE1iJy_IvMpPQXV_VHdeOVIpAuptKmlv6yyYQ.897L3hsLMz-ftjbo.Ttjc9tGe1HcJsFrUy2vRaNpkIOUSkugE0qCOJ4ZNALQ4DOngmxOpu-4N0J53jk3v_OXqATabWdAQjKiXmwDdosxib-ymwIEyUX5E6jarDNITaZY54JeiMr1hQ0gYuUfvLMLXw5ZwntA-Gmq8xpwONNxBLIasgvD6h6t0xYGA2fQVD8PhtYWbONU-xIkG7UWg27SLPXUh97rbhidcP4zZhIhk3W8db4P91YnLqmiJkiOtcLfU1NEr21iJSBNnH4A9MW-nd3t3BLBJatQEeZVMrp1WPlCyFDyxOjp3HCVV-w_jwrnLH2x_ew7ANY_mgEZhyRBqAfgpcgFekvpegv3LL9lXLln4K_5HrAtgIr26_fmFQO_qvOBD-tSf6wyWKXPmhC9zBeCas3HnquZZCHWxrg_EUMfQMtbSG50VqUQYqZvDY1HKogiDrtq1r_yBnWM_vuTANxNyhjHtLI97CQ6raFDFzw1E-E9I43QyTYPSYIESHdIRavFcBqeSsx75PyMW2dhBu3qV0jUdgpiMywc6MLCjP5lCLbuH0wZjvO1qTc61-Wc3cKSo12_LrMplq-8-gqVmoHvZsKYF3LfpTgX0i0L8I19dmGk5on0T4uD31PWAVDobN34LN7nNmG2GPwmJibl5jsljIY78lUD5Jhj1Qk6vj2_2F91S7iEoxUBUClbORvx--uif1ljVHC_kUPU6FWflRREZEVTfKdkY_S2bhzCuGwg6aYumLem1ddZmcKaDBNNi8Romv1cCV-qVyB0F4uLWZ1IFRbdgxKRvpvoZzN0kApNhWWDiG7vWpbzOkLdHwey_BRMtrbXLfMwtuJ-nLTooK-H7pqun_scmFGt57eMmh7ALW9jKWgawo1nQ0IXMpXJ3aGSMroiYxTqi9APpfHXOvs5Urw1hAqhdl4H2fGxBBLD3WW9DAiUFpuzMe9xQQ1qEFNvfMBIvv2ekjJZIzPnsZ0ahgv5ymaNXxFhx2h9t8KPMLJ7VOZgBPjvf7GYiEfJYf33JTmCBlBKmyh1olWxs5iAvcIQva1moYbiMJAESmQmjAyOcxnxZuu1WsnPX1BYNiJyXs7LsiIpI6Oo39mJVbjh7jh1xRLccFccSUZYk7KZFCC1EGiE0z8NQrr3F1_lHyeaFHd4mEJvY5_1lF1z7FqkkCwydNqcEBH0RJ-Fu_ShEquTffLa5udNrIZBQRpivirHhSVcfw23JSM9FZ2be6W1tcD8_sroUuEeBMGtxCAzRF4Gq9WRR1xe68WmEOHmx6OEHgQIAPf6KTYGIgrx3Aw2VkeofcIeA8gUc4PgZNKq23TAA9m_ZhVVLykDUZyQfljYiNY-RsdjkXw1CCTQP-1TRbBYnQalZlCfnYM1nvdJIJ_JEXix1Y8hMioJ0BsaYGtjDfEM6mGPeMZ-L6RMAcjM62KU6utcJ6zlkl16QitHQJt0uvu1tVRFdpYdjcwFAD__161Yk-JVHPQ3UBFoZ7NOq7IDySkzogulQvH1BahAf7TQJDffj9loNK4g5x4D__0-XaTKSubg3655s24oCDSav3gKoqzrOIMuuDMreKVlFDtGbT9c6ySTqvM3g5hITeqJ34IAa4pTrRM-RdT8Z1g6C4FKhvFHOHR9ArCSyrm2EBPO1RrBMi9xejZrhGRzdYjWeaInV4aoOvJCdiDCkMI4JJKa_vnnqB0wzmlrhuE69_GtLLLCeDfV9RWvjCDVcERpu1fO5pmSnhrWU_n0dgPQjK0KQhX97LnTNcXTbklf11A7-vpmrW1-wJ7zXDmf2N_IWN8-vji4x7hp5o9roUjS5rG0IRSUYAZqMMT3H6uXqtX8M_CA_UzM0fyFyXafCk1IvPdGeqyr8pITPVFdeOlCmiOBCH78aaSPO-dQFAghASP1qFdH4I1h2DP7hHQGNNT8.miLjzfNjBifoGQEqCr0w1g
GET and DELETE Endpoints
There are new POST
endpoints specifically for encryption as it is not common to have a body in GET
and DELETE
requests.
All GET
and DELETE
requests can be converted to POST
by appending the corresponding HTTP method to the URL. With GET
it would be /get
and with DELETE
/delete
.
Example 1:
GET /xs2a/v1/sessions/{session_id}
will become
POST /xs2a/v1/sessions/{session_id}/get
Example 2:
DELETE /xs2a/v1/sessions/{session_id}/flows/{flow_id}
will become
POST /xs2a/v1/sessions/{session_id}/flows/{flow_id}/delete
Replay Attack Prevention
To prevent replay attacks there are two new mandatory fields for every request:
The request_id
has to be a unique string identifier for every request. The length of this identifier has to be between 10 and 100 characters. We recommend using a UUID. Sending two requests with the same identifier in a timeframe of 5 minutes will reject the second request.
The request_timestamp
field should contain a timestamp in UTC milliseconds. Requests with a request_timestamp
older than 2 minutes will be rejected.
This example shows what the unencrypted create session request body could look like:
{
"request_id": "b5c9edb2-319f-4f5a-bef1-9cad8f7e785e",
"request_timestamp": 1585063812156,
"psu": {
"user_agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 10_3_1 like Mac OS X) AppleWebKit/603.1.30 (KHTML, like Gecko) Version/10.0 Mobile/14E304 Safari/602.1",
"ip_address": "136.71.143.70"
}
}
Additionally, all encrypted responses will also contain a new field after decrypting the payload:
The response_timestamp
field contains the response creation timestamp in UTC milliseconds. To further improve security this timestamp should be verified by the recipient of the response to detect manipulation. Responses with a response_timestamp
older than 2 minutes should not be accepted.
This example shows what the decrypted create session response body could look like:
{
"response_timestamp": 1585063812228,
"data": {
"session_id": "<session_id>",
"session_id_short": "<session_id_short>",
"flows": {
...
},
"self": "<self_url"
}
}
Key Creation
A valid key pair can be created like this:
openssl genrsa -out private.pem 2048
openssl rsa -in private.pem -pubout > public.pub
The content of the public.pub
file has to be sent to Klarna to start using the JWE encrypted communication. All other required data will be generated and provided by Klarna.