Open Banking API 1.0

Version Date Author Comment
1.0.0 2 May 2018 Open Banking API Development Team This is the baseline version.
1.1.0 7. March 2019 Open Banking API Development Team Simplified and added sample code
1.1.1 11. March 2019 Open Banking API Development Team Added sandbox users
1.2.0 02. May 2019 Open Banking API Development Team Added full Java Payment API example
1.2.1 16. May 2019 Open Banking API Development Team Corrected "TPP is only PISP" explanation
1.2.2 3. Jun 2019 Open Banking API Development Team Removed broken links
1.2.3 14. Jun 2019 Open Banking API Development Team Added known errors to be fixed
1.2.4 26. Jun 2019 Open Banking API Development Team Updated for new release
2.0.0 22. Aug 2019 Open Banking API Development Team Signature Logic Changed and updated Examples

Known Issues

Introduction

This document explains how you as a TPP should integrate with the Open Banking API. It describes what endpoints should be called in what order, how to sign requests and what data to send in different PSU contexts. Details on each endpoint can be found in the Developer Portal, or in the Berlin Group XS2A Framework Implementation Guidelines.

Overview

Prerequisites

Your eIDAS certificate will be validated and your TPP-registration will be checked against central registries when you make requests to the Production APIs.

Basics

This section presents and discusses some of the basic concepts and features found by the Open Banking API

Open Banking Actors

Actor Abbreviation Type Description
Payment Service User PSU Person A natural or legal person making use of a payment service as a payee, payer or both (PSD2 Article 4(10))
Payment Service Provider PSP Legal Entity A legal entity (and some natural persons) that provide payment services as defined by PSD2 Article 4(11)
Account Servicing Payment Service Provider ASPSP Legal Entity An ASPSP is a PSP that provides and maintains a payment account for a payment services user (PSD 2 Article 4(15).
Third Party Provider TPP Legal Entity A party other than an ASPSP that provides payment related services. The term is not actually defined in PSD2, but is generally deemed to include all payment service providers that are 3rd parties (the ASPSP and the PSU to whom the account belongs being the first two parties).
Payment Initiation Service Provider PISP Legal Entity A TPP that provides Payment Initiation Services. PSD2 does not offer a formal definition. Article 4(18) quite circularly defines a PISP as a PSP that provides Payment Initiation Services.
Account Information Service Provider AISP Legal Entity A TPP that provides Account Information Services. Again, PSD2 defines AISPs in Article 4(19) circularly as a PSP that provides account information services

RESTful Principles

The Open Banking API adheres to RESTful API concepts where possible and sensible to do so. However, the priority is to have an API that is simple to understand and easy to use. In instances where following RESTful principles would be convoluted and complex, the principles have not been followed.

Accepted Data Encoding

The Open Banking API encodes all data as JSON. Different XML formats also mentioned in Berlin Group XS2A Framework Implementation Guidelines is not supported, but could be supported in the future.

Character Encoding

The API requests and responses must use a UTF-8 character encoding, as is the default for JSON text in [RFC 7158 - Section 8.1][53]. However, an ASPSP's downstream system may not accept some UTF-8 characters, such as emojis. If the ASPSP rejects the request with a message that a UTF-8 character cannot be processed, the ASPSP should respond with a 400 (Bad Request).

Date Formats

All dates in the JSON payloads are represented in ISO 8601 date or date time format.

ISO 8601 Date

2018-05-17 

ISO 8601 DateTime

2018-05-17T08:37:12+00:00 

All dates in the HTTP headers are represented as RFC 7231 Full Dates e.g.

Sun, 17 May 2018 08:37:12 UTC

TPP Redirects

The TPP needs to implement some endpoints so they can receive the query parameters in redirects from the ASPSP. Currently the only redirects happens after the PSU has completed the SCA process. The URL for these redirects must always be provided by the TPP in the `TPP-Redirect-URI` header in case SCA is required.

Redirect after successful SCA process

Example:

https://some-aisp-with-callback.com/callback.html?psu-id=dfd07c50-20f5-4703-b5dc-ce141a13ad76&tpp-session-id=76599984&context=PRIVATE
Parameter Name Type Description
psu-id RFC 4122 UID (UUID) Identifies a PSU, it is unique for a given combination of TPP and ASPSP, and does not change. The value here should be provided in the `PSU-ID` header for all subsequent requests, and should be persisted by the TPP for future requests for the same PSU. The value of `PSU-ID` need not be shared with the PSU.
psu-corporate-id (optional) RFC 4122 UID (UUID) If the PSU chooses a corporate context this field will identify one corporate agreement belonging to the PSU (there could be multiple), and should be treated the same as the PSU-ID, but set to the `PSU-CORPORATE-ID` header instead.
tpp-session-id String The value of the `TPP-Session-ID` header used by the request that triggered the SCA process.
context String, one of (PRIVATE, CORPORATE) indicates whether a PSU selected a `PRIVATE` or `CORPORATE` context. See corporate context

Redirect after failed SCA process

Example:

http://some-aisp-with-callback.com/callback.html?system=ERA-PSD2&status=400&code=PAYMENT_FAILED&message=SomeUrlEncodedMessage
Parameter Name type Description
system String The name of the internal system that produced the error in case of a server error
status Number HTTP status code
code String Error code detailing the status
message String URL-encoded message, detailing the error

See Error Codes and Responses for details on error codes.

Security & Access Control

Transport Security

The communication between the TPP and the ASPSP is always secured by using a TLS-connection using TLS version 1.2 or higher.

Non-Repudiation

Digital signatures as described by [draft-cavage-signatures-09][54] are used to facilitate non-repudiation for Open Banking API requests.

In addition digital signatures must adhere to requirements as described in XS2A Framework Implementation Guidelines, Section 11.

Signing Specification

The TPP must sign each API request.

The ASPSP should verify the signature of API requests it receives before carrying out the request. If the signature fails validation, the ASPSP must respond with a 400 (Bad Request).

The ASPSP must reject any API requests that should be signed but do not contain a signature in the HTTP header with a 400 (Bad Request).

The ASPSP may sign the HTTP body of each API response that it produces which has an HTTP body.

The TPP should verify the signature of API responses that it receives.

Signing Process

The process of signing HTTP messages is subject to change when the draft-cavage-http-signatures-09 and ETSI TS 119 495 standards release a final version of their specification.

The target url and the required header fields must be used to sign the request. The signature is contained in its entirety in the "Signature" header field.

The 4 parts of a signature header: |Name|Description| |----|-----------| |keyId |the id of the key used to sign the request (in case of rsa-sha256 this points to a RSA Private Key) | |algorithm | the algorithm used for signing (for example rsa-sha256) | |headers | contains all the required header names separated by space in the same order they will be concatenated for signing. | | target url | which is not a header is indicated with (request-target) | |signature | The signature string, see below |

Requirements for Key ID

It consists of two parts SN= and CA= * SN should contain the hex encoded upper case representation of the serial number of the certificate. * CA should contain The issuer of the certificate. This should be formatted according to RFC 2253.

Required Headers for Signature The headers listed below should be put in the signature if they are used in the request.

Signature Requirements: * If a header is not included in the request it should not be included in the signature * All header names must be lower case. * Concatenate header names and values with a separator of ":" and a space. * Separate each header with a newline * Remember to use the same headers in the same ordering as the "headers" value * Sign this string using the defined by algorithm and the key defined by keyId. * Base64 encode the resulting bytes.

Example of how to concatenate header fields before signing

first-header-name: first-header-value
second-header-name: second-header-value

After this concatenate the 4 parts of the signature header in order: * Format each part: key="value" * Concatenate each part using ',' (comma)

Example of a completed signature header

Assume the `algorithm` value was "rsa-sha256". This would signal to the application that the data associated with the `keyId` is an RSA Private Key, the signature string hashing function is SHA-256, and the signing algorithm is the one defined in Section 8.2.1 of [RFC 3447][55]. The result of the signature creation algorithm should result in a binary string, which is then base 64 encoded and placed into the `signature` value, currently indicated as "base64(rsa-sha256(signing-string))".

Signature: keyId="SN=DCC5CCB85FDAB32A,CA=O=PSDNO-FSA-SOMEASPSP,L=Trondheim,C=NO,CA=", algorithm="rsa-sha256", headers="digest tpp-transaction-id tpp-request-id psu-id date", signature="base64(rsa-sha256(signing-string))"

ISO-8859-1 or UTF-8 encoded headers

If any values in the signature header is ISO-8859-1 or UTF-8 encoded you need to URL encode the signature header according to RFC 2047 which means MIME encoding the signature.

Also the signature must be wrapped using this format: =?charset?encoding?encoded signature?=

Signature example with this encoding:

Signature: =?utf-8?B?a2V5QTQsQ0E9Mi41LjQuOTc9IzB........jMTM1MDUzNDQ0ZTRmMmQ0NjUz?=

Java example of how to implement encoding:

if (charset.equals(StandardCharsets.UTF_8)) {
  signature = String.format("=?utf-8?B?%s?=", Base64.getEncoder().encodeToString(signature.getBytes(StandardCharsets.UTF_8)));
}

Strong Customer Authentication

SCA is mandated by the RTS and supported by the Open Banking API using the SCA redirect flow as detailed in XS2A Framework Implementation Guidelines, Section 5.1.

The message flow used is relatively simple and straight forward;

Note that the `TPP-Redirect-URI` header is required for all requests, as the TPP has no prior knowledge of when a PSU will be required to complete the SCA process.

During the SCA process the ASPSP will automatically gather consent, and the TPP will for subsequent request only be granted access to interact with these consented accounts.

SCA is at most valid for one hour within a session, as governed by `TPP-Session-ID`. Such sessions must be managed in accordance with the PSD2 regulative.

Following a successful SCA process the PSU will be redirected to the `TPP-Redirect-URI` which should be a TPP endpoint as described in TPP Redirects

This API uses the Bank Offered Consent approach described in XS2A Framework Implementation Guidelines, section 6.4 It means that the PSU will give consent during the SCA redirect or directly with the ASPSP, and the POST to /v1/consents will disregard the body and only return a sca redirect. The consent flow is explained further under Account Information API.

A PSU can revoke consent for accessing account information at any point in time.

The PSU can revoke authorization directly with the ASPSP. The mechanisms for this are in the competitive space and are up to each ASPSP to implement in the ASPSP's banking interface. ASPSPs are under no obligation to notify TPPs regarding the revocation of authorisation.

Private and Corporate Context

A PSU can act as a private person or as a representative of a corporation.

The PSU chooses which context it will use during the SCA process. The steps in this document describes the flow for a private person. The corporate context is described here.

Special Handling for Corporate Context

The main difference is that after SCA both `PSU-ID` and `PSU-CORPORATE-ID` headers need to be set for future calls to the API.

The `PSU-CORPORATE-ID` is aquired in two ways:

1. If the TPP has not previously aquired a PSU-ID:

The PSU needs to go through the SCA process and choose corporate context which can be initiated in one of two ways: * Consent SCA flow for AISPs. * GET /v1/user SCA flow in Payment Integration API for PISPs that are not AISPs.

After the SCA process the PSU-CORPORATE-ID will be provided in the TPP redirect.

2. If the TPP has already aquired a PSU-ID.

The set of PSU corporate agreements can be fetched by calling GET /v1/agreements. These agreements can be used by the PSU to choose its corporate context which should be set as the `PSU-CORPORATE-ID` header.

Example:

  1. The TPP calls GET /v1/agreements and presents them to the PSU
  2. The PSU selects a new corporate agreement where no consents are given.
  3. The TPP calls GET /v1/accounts on this context by setting the `PSU-CORPORATE-ID` header.

A new consent flow will be triggered like in Account Information API Steps The PSU would not be required to select corporate agreement in the Consent SCA process, because it is already selected by setting the `PSU-CORPORATE-ID` header in the previous call.

Account Information API

This API allows TPPs to access the accounts of a PSU and see account numbers, balances and transactions. * Every sandbox and production request must be signed, this means the request need to contain the header `Signature`, see Signing Process. * Every request must have the header `TPP-Redirect-URI`. This should point to a endpoint in your application, the TPP Redirect Endpoint * The `TPP-Session-ID` is optional and can be used to correlate a redirect to the original request.

Below you will find detailed steps for doing your first request.

Quick Start Accounts Full Java Example

This is a full example showing how to get accounts, payment uses the same signing algorithm but with extra headers. You need Java 11 for this example to work.

package your.package;

import okhttp3.Headers;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;

import org.bouncycastle.jce.provider.BouncyCastleProvider;


import javax.security.auth.x500.X500Principal;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.invoke.MethodHandles;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.*;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;


/**
 * Check the prerequisites before running this sample.
 *
 * This code requires OkHttp and Bouncy castle as dependencies
 *
 * Update variables with relevant values and run the main method to test.
 *
 * Maven dependencies:
 *  <dependency>
 *      <groupId>org.bouncycastle</groupId>
 *      <artifactId>bcprov-jdk15on</artifactId>
 *      <version>1.61</version>
 *  </dependency>
 *  <dependency>
 *  <groupId>com.squareup.okhttp3</groupId>
 *      <artifactId>okhttp</artifactId>
 *      <version>3.13.1</version>
 *  </dependency>
 */
public class Psd2DEMOClient {

    private static String[] signatureHeaders = {"date", "digest", "x-request-id", "psu-id", "psu-corporate-id", "tpp-redirect-uri"};

    public static void main(String[] args) throws Exception {
        OkHttpClient httpClient = createOkHttpClient();


        /*
        See the getAccount method description for information on the different fields and the sample values set here.
         */
        String certificateFile = "cert.pem";
        String keyFile = "cert.pkcs8.key";

        /*
        set this to a constant value or change it to the tpp-session-id query parameter value after following the redirect.
         */
        String tppSessionId = UUID.randomUUID().toString()+"-codesample";

        /*
        set this one after following the redirect and run it again
         */
        String psuId = null;

        URL url = new URL("----- ASPSP BASE URL WITH TRAILING '/' ----");
        Request request =  Psd2DEMOClient.getAccounts(certificateFile, keyFile, url, tppSessionId, psuId);

        Response response = httpClient.newCall(request).execute();


        System.out.println("---------- Request ----------");
        System.out.println(request.url());
        System.out.println(request.headers());

        System.out.println("---------- Response ----------");
        String body = response.body().string();
        System.out.println(response.code());
        System.out.println(response.headers().toString());
        System.out.println(body);

        System.out.println("\n------------------------------------------\n");

        if(body.contains("accounts")) System.out.println("Yay you got accounts!");
        else if(response.code() == 200) System.out.println("Yay! It seems like it worked, but no accounts were found.");
        else if(body.contains("scaRedirect")) System.out.println("Yay! you got through, now follow the scaRedirect link and then try again using the psu-id and tpp-session-id from the url parameters in the final redirect.");
        else System.out.println("Something went wrong! Check the response message for details.");

    }
    /**
     * Make sure your certificate is registered with the ASPSP.
     *
     * When this method has been executed successfully the body of the response (as a string) should contain a _link with a scaRedirect and a href.
     * 1. Open the url in your browser.
     * 2. Follow the SCA flow in the browser
     * 3. After final redirect you will see a set of query parameters in the url in your browser. (example: https://httpbin.org/?psu-id=98256c61-9cd3-4244-b721-434738c63670&tpp-session-id=8b2d1322-94d7-4aee-998f-c8cdb1021e2a&context=PRIVATE)
     * 4. Use the PSU-ID and Tpp-session-id url parameter values for subsequent requests.
     *
     * @param certificateFile path to the certificate pem file.
     * @param keyFile path to the certificate key file.
     * @param baseUrl ASPSP url with a trailing '/'(url to the sandbox environment excluding the path v1/accounts)
     * @param tppSessionId  any string (for example a UUID string) it identifies the session. The same id should be used for every request in a PSU session
     * @param psuId optional psu id, after
     *
     */
    public static Request getAccounts(String certificateFile, String keyFile, URL baseUrl, String tppSessionId, String psuId) throws Exception {

        /*
        Append the API path to the API base url
         */
        URL url = new URL(baseUrl,"v1/accounts");

        /*
        Request id to identify the request across services. Store this id and its audit trail!
         */
        String requestId = UUID.randomUUID().toString();

        /*
        Read the certificate
         */
        X509Certificate certificate = (X509Certificate) CertificateFactory.getInstance("X.509").generateCertificate(new ByteArrayInputStream(readResourceFile(certificateFile)));

        /*
        Read the certificate key
         */
        PrivateKey privateKey = KeyFactory.getInstance("RSA").generatePrivate(new PKCS8EncodedKeySpec(readResourceFile(keyFile)));
        System.out.println("Private Key > Format: " + privateKey.getFormat());
        System.out.println("Private Key > Algorithm: " + privateKey.getAlgorithm());

        /*
        Base 64 encode the certificate for use in the header
        */
        String encodedCertificate = Base64.getEncoder().encodeToString(certificate.getEncoded());

        /*
        Create headers to use for both the signature and the http request
        */
        Map<String, String> headers = new LinkedHashMap<String, String>(Map.of(
                "X-Request-ID", requestId,
                "Date", ZonedDateTime.now().format(DateTimeFormatter.RFC_1123_DATE_TIME),
                "TPP-Session-ID", tppSessionId,
                "TPP-Redirect-URI", "https://httpbin.org",
                "TPP-Signature-Certificate", encodedCertificate,
                "PSU-IP-Address", "0.0.0.0" //Use an actual PSU ip address here!
                ));

        if(psuId != null) {
            headers.put("PSU-ID", psuId);
        }


        /*
        Make all headers lower case.
         */
        Map<String, String> normalizedHeaders = headers
                .entrySet()
                .stream()
                .collect(Collectors.toMap(entry -> entry.getKey().toLowerCase(), Map.Entry::getValue));


        /*
        Create signature.
         */
        String signature = createSignature(certificate, privateKey, normalizedHeaders);

        /*
        Create a request and set all headers including the signature header.
         */
        Headers.Builder headerBuilder = new Headers.Builder();
        normalizedHeaders.forEach(headerBuilder::add);

        return new Request.Builder().url(url)
                .headers(headerBuilder.build())
                .header("Signature", signature)
                .build();

    }

    /**
     *
     * @param certificate Valid X509 certificate registered on a TPP
     * @param privateKey The Private key of the x509 certificate
     * @param requestHeaders
     * @return The signature
     * @throws Exception
     */
     private static String createSignature(X509Certificate certificate, PrivateKey privateKey, Map<String, String> requestHeaders) throws Exception {

         /*
         Certificate serial number on upper case hex form.

            Example output: 6AEB4444FBAAD267
          */
        String upperCaseHEXSerialNumber = certificate.getSerialNumber().toString(16).toUpperCase().trim();

        /*
        Create the keyId value by Concatenating the serial and issuer name items using SN=***,CA=***
            (Note that "CA=" is not part of the issuer name and "SN=" is not part of the serial)

            Example output:
                SN=6AEB4444FBAAD267,CA=O=PSDNO-FSA-ABCA,L=Trondheim,C=NO
         */
         String keyId = String.format("SN=%s,CA=%s",
                 upperCaseHEXSerialNumber,
                 certificate.getIssuerX500Principal().getName(X500Principal.RFC2253));
         System.out.println("Signature > Key ID: "  + keyId);

        /*
        Filter the headers so only the ones needed for the request are left.
         */
        List<String> headersForSigning = requestHeaders.entrySet().stream()
                .map(Map.Entry::getKey)
                .filter(headerName -> Arrays.asList(signatureHeaders).contains(headerName))
                .collect(Collectors.toList());


        /*
        Create the headers value by concatenating the header names (Concatenate with whitespace *not* comma)

            This is used to communicate the header ordering to the ASPSP for signature validation.
         */
        String headers = String.join(" ", headersForSigning);


        /*
        Concatenate all the headers to make the signing string.

            They are concatenate in this way:
                name1: value1
                name2: value2
         */
        String signingString = headersForSigning.stream()
                .map( headerName -> String.format("%s: %s", headerName, requestHeaders.get(headerName)))
                .collect(Collectors.joining("\n"));


        /*
        Sign the string using RSA SHA256.

            The output is a byte array
         */
        Signature sha256withRSA = Signature.getInstance("SHA256withRSA", new BouncyCastleProvider());
        sha256withRSA.initSign(privateKey);
        sha256withRSA.update(signingString.getBytes(StandardCharsets.UTF_8));
        byte[] signedBytes = sha256withRSA.sign();

        /*
        Validate signature (this is not strictly neccessary, the purpose in this example is to provide error messages
        if there is a problem with the input certificate or key).
         */


        /*
        Base 64 encode the byte result of the signing algorithm to get a string.
         */
        String base64EncodedSignedBytes = Base64.getEncoder().encodeToString(signedBytes);

         sha256withRSA.initVerify(certificate.getPublicKey());
         sha256withRSA.update(signingString.getBytes(StandardCharsets.UTF_8));
         boolean valid = sha256withRSA.verify(signedBytes);
         if (valid) {
             System.out.println(String.format("Signature > VALID when checked against certificate with serial %s", upperCaseHEXSerialNumber));
         } else {
             System.out.println(String.format("Signature > NOT VALID when checked against certificate with serial %s", upperCaseHEXSerialNumber));
             throw new SignatureException(String.format("Signed bytes is signed using a private key not matching the certificate with serial %s", upperCaseHEXSerialNumber));
         }

        /*
        Create the signature header value by concatenating the keyId value, headers value, algorithm used and the base 64 encoded signature.

            Example output: keyId="SN=6AEB4444FBAAD267,CA=O=PSDNO-FSA-ABCA,L=Trondheim,C=NO", algorithm="rsa-sha256", headers="date x-request-id tpp-redirect-uri psu-id", signature="***************"
         */
        String signature = String.format("keyId=\"%s\",algorithm=\"rsa-sha256\",headers=\"%s\",signature=\"%s\"", keyId, headers, base64EncodedSignedBytes);


        return signature;
    }

    private static OkHttpClient createOkHttpClient() {
        return new OkHttpClient.Builder()
                .connectTimeout(60 * 1000, TimeUnit.MILLISECONDS)
                .readTimeout(60 * 1000, TimeUnit.MILLISECONDS)
                .writeTimeout(60 * 1000, TimeUnit.MILLISECONDS)
                .build();
    }

    public static byte[] readResourceFile(String fileName) throws IOException {

        Psd2DEMOClient.class.getResource(fileName);

        try (InputStream inputStream = MethodHandles.lookup().lookupClass().getResourceAsStream(fileName)) {
            return inputStream.readAllBytes();
        }
    }

}

Steps

To get access to account information you need to perform minimum these steps. (These steps are for a private context, but corporate context is very similar. See Private and Corporate Context)

  1. Do a GET to /v1/accounts (or any other resource you want to access)
  2. Redirect the PSU using scaRedirect, or present the link in an iframe to start the SCA flow.
  3. Persist the PSU-ID, it should be used in all future requests for this PSU.
  4. You may now call the same resource as in the first step again (GET /v1/accounts).

After these steps the other account resources can be accessed the same way as GET /v1/accounts in the last step, even after the user session ends (with some limits. See Request Limits).

Sequence Diagram

Sequence diagram
Sequence diagram

Request Limits

The header `PSU-IP-Address` must be present if the PSU has asked for this account access in real-time.

`PSU-IP-Address` should be the ip address of the device the PSU is using to access the TPP service.

When the TPP want to do a request without a PSU present it should omit `PSU-IP-Address`.

There are only two cases where a TPP can make requests and omit the `PSU-IP-Address`, and they have some restrictions:

Payment Initiation API

This API allows TPPs to initiate payments on behalf of a PSU. * Every sandbox and production request must be signed, this means the request need to contain the header `Signature`, see Signing Process. * Every request must have the header `TPP-Redirect-URI`. This should point to a endpoint in your application, see TPP Redirect Endpoint * The `TPP-Session-ID` is optional and can be used to correlate a redirect to the original request.

Below you will find detailed steps for doing your first request.

Quick Start Payment Full Java Example

This is a full example showing how to get accounts, payment uses the same signing algorithm but with extra headers. You need Java 11 for this example to work.

package your.package;

import okhttp3.*;

import org.bouncycastle.jce.provider.BouncyCastleProvider;


import javax.security.auth.x500.X500Principal;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.invoke.MethodHandles;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.*;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.security.spec.PKCS8EncodedKeySpec;
import java.time.LocalDate;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;


/**
 * Check the prerequisites before running this sample.
 *
 * This code requires OkHttp and Bouncy castle as dependencies
 *
 * Update variables with relevant values and run the main method to test.
 *
 * Maven dependencies:
 *  <dependency>
 *      <groupId>org.bouncycastle</groupId>
 *      <artifactId>bcprov-jdk15on</artifactId>
 *      <version>1.61</version>
 *  </dependency>
 *  <dependency>
 *  <groupId>com.squareup.okhttp3</groupId>
 *      <artifactId>okhttp</artifactId>
 *      <version>3.13.1</version>
 *  </dependency>
 */
public class Psd2PaymentDEMOClient {

    private static String[] signatureHeaders = {"date", "digest", "x-request-id", "psu-id", "psu-corporate-id", "tpp-redirect-uri"};

    public static void main(String[] args) throws Exception {
        OkHttpClient httpClient = createOkHttpClient();

        /*
        See the performPaymentFlow method description for information on the different fields and the sample values set here.
         */
        String certificateFile = "cert.pem";
        String keyFile = "cert.pkcs8.key";

        /*
        The account numbers to use in a test payment.
        Remember to change these to accounts in the test data set.
         */
        String debtorAccountBban = "97105048304";
        String creditorAccountBban = "97105048304";

        /*
        Set this to a constant value or change it to the tpp-session-id query parameter value after following the redirect.
         */
        String tppSessionId = UUID.randomUUID().toString()+"-codesample";

        /*
        Set this to an exsisting sca-redirect
         */
        String psuId = null;

        /*
        Set this to the content of the href in the "startAuthorisation" link in the response of a payment initiation
         */
        String startAuthorisationLink = null;

        URL url = new URL("----- ASPSP BASE URL WITH TRAILING '/' ----");

        Request request = Psd2PaymentDEMOClient.performPaymentFlow(certificateFile, keyFile, url, debtorAccountBban, creditorAccountBban, tppSessionId, psuId, startAuthorisationLink);

        Response response = httpClient.newCall(request).execute();



        System.out.println("---------- Request ----------");
        System.out.println(request.url());
        System.out.println(request.headers());

        System.out.println("---------- Response ----------");
        String body = response.body().string();
        System.out.println(response.code());
        System.out.println(response.headers());
        System.out.println(body);

        System.out.println("\n------------------------------------------\n");

        if(body.contains("startAuthorisation")) System.out.println("You have intiated a payment, to authorise it paste the startAuthorisation link in the example");
        else if(body.contains("scaRedirect")) {
            if(startAuthorisationLink == null) {
                System.out.println("You got through, now follow the scaRedirect link and then try again using the psu-id and tpp-session-id from the url parameters in the final redirect.");
            } else {
                System.out.println("Now follow the scaRedirect link, if you get no errors after performing SCA the payment has been successfully authorised.");
            }
        }
        else System.out.println("Something went wrong! Check the response message for details.");
    }


    /**
     * Make sure your certificate is registered with the ASPSP.
     *
     *
     * After this method is run without psuId or with startAuthorisationLink the body of the response (as a string) should contain a _link with a scaRedirect and a href.
     * 1. Open the url in your browser.
     * 2. Follow the SCA flow in the browser
     * 3. After final redirect you will see a set of query parameters in the url in your browser. (example: https://httpbin.org/?psu-id=98256c61-9cd3-4244-b721-434738c63670&tpp-session-id=8b2d1322-94d7-4aee-998f-c8cdb1021e2a&context=PRIVATE)
     * 4.
     *    a. If psuId is not present Use the PSU-ID and Tpp-session-id url parameter values for subsequent requests.
     *    b. if psuId and startAuthorisationLink is present these parameters without any error message indicate a successfully approved payment.
     *
     * @param certificateFile path to the certificate pem file.
     * @param keyFile path to the certificate key file.
     * @param url ASPSP url with a trailing '/'(url to the sandbox environment excluding the path v1/payments).
     * @param debtorAccountBban Debtor account, will be inserted into sample json.
     * @param creditorAccountBban Creditor account, will be inserted into sample json.
     * @param tppSessionId  any string (for example a UUID string) it identifies the session. The same id should be used for every request in a PSU session.
     * @param psuId optional psu id, if present will attempt to initialize payment.
     * @param startAuthorisationLink optional startAuthorisation link to follow, if present will attempt to start autorisation of the payment.
     *
     */
    public static Request performPaymentFlow(String certificateFile, String keyFile, URL url, String debtorAccountBban, String creditorAccountBban, String tppSessionId, String psuId, String startAuthorisationLink) throws Exception {

        /*
        Read the certificate
         */
        X509Certificate certificate = (X509Certificate) CertificateFactory.getInstance("X.509").generateCertificate(new ByteArrayInputStream(readResourceFile(certificateFile)));

        /*
        Read the certificate key
         */
        PrivateKey privateKey = KeyFactory.getInstance("RSA").generatePrivate(new PKCS8EncodedKeySpec(readResourceFile(keyFile)));
        System.out.println("Private Key > Format: " + privateKey.getFormat());
        System.out.println("Private Key > Algorithm: " + privateKey.getAlgorithm());

        /*
        Step 1: If you have no PSU-ID one must be generated first.
         */
        if(psuId == null) {

            return getPSUId(certificate, privateKey, url,tppSessionId);
        }

        /*
        Step 2: Initiate Payment
         */
        else if(startAuthorisationLink == null) {
            return initiatePayment(certificate, privateKey, url, debtorAccountBban, creditorAccountBban, tppSessionId, psuId);
        }

        /*
        Step 3: Start payment authorisation
         */
        else {
            return startAuthorisation(certificate,privateKey, url, tppSessionId, psuId, startAuthorisationLink);
        }

    }

    public static Request getPSUId(X509Certificate certificate, PrivateKey privateKey, URL baseUrl, String tppSessionId) throws Exception {

        /*
        Append the API path to the API base url
         */
        URL url = new URL(baseUrl,"v1/users");

        Request request = makeSignedRequestBuilder(certificate, privateKey, url, null, tppSessionId, null)
                .get()
                .build();

        return request;
    }


    public static Request initiatePayment(X509Certificate certificate, PrivateKey privateKey, URL baseUrl, String debtorAccountBban, String creditorAccountBban, String tppSessionId, String psuId) throws Exception {
        /*
        Append the API path to the API base url
         */
        URL url = new URL(baseUrl,"v1/payments/norwegian-domestic-credit-transfers?allowLogicalDuplicate=true");

        /*
         Generate sample payment data.
         It is important not to have any whitespace in the json structure.
         */
        String isoDate = DateTimeFormatter.ISO_DATE.format(LocalDate.now());

        String requestBody = "{\"instructedAmount\":{\"currency\":\"NOK\",\"amount\":\"100\"},\"debtorAccount\":{\"bban\":\""+debtorAccountBban+"\",\"currency\":\"NOK\"},\"creditorAccount\":{\"bban\":\""+creditorAccountBban+"\",\"currency\":\"NOK\"},\"creditorName\":\"X\",\"creditorAddress\":{\"country\":\"NO\"},\"purposeCode\":\"COST\",\"requestedExecutionDate\":\""+isoDate+"\",\"remittanceInformationUnstructured\":\"0x5f3759df\"}";

        String digest = "SHA-256="+Base64.getEncoder().encodeToString(MessageDigest.getInstance("SHA-256", new BouncyCastleProvider()).digest(requestBody.getBytes(StandardCharsets.UTF_8)));


        Request request = makeSignedRequestBuilder(certificate, privateKey, url, digest, tppSessionId, psuId)
                .post(RequestBody.create(MediaType.parse("application/json"),requestBody))
                .build();


        return request;
    }

    public static Request startAuthorisation(X509Certificate certificate, PrivateKey privateKey, URL baseUrl, String tppSessionId, String psuId, String startAuthorisationLink) throws Exception {

        String startAuthorisationLinkNoLeadingSlash = startAuthorisationLink.startsWith("/") ? startAuthorisationLink.substring(1) : startAuthorisationLink;
        /*
        Append the API path to the API base url
         */
        URL url = new URL(baseUrl,startAuthorisationLinkNoLeadingSlash);

        Request request = makeSignedRequestBuilder(certificate, privateKey, url, null, tppSessionId, psuId)
                .post(RequestBody.create(null, new byte[0]))
                .build();

        return request;
    }

    private static Request.Builder makeSignedRequestBuilder(X509Certificate certificate, PrivateKey privateKey, URL url, String digest, String tppSessionId, String psuId) throws Exception {

        /*
        Request id to identify the request across services. Store this id and its audit trail!
         */
        String requestId = UUID.randomUUID().toString();

        /*
        Base 64 encode the certificate for use in the header
        */
        String encodedCertificate = Base64.getEncoder().encodeToString(certificate.getEncoded());

        /*
        Create headers to use for both the signature and the http request
        */
        Map<String, String> headers = new LinkedHashMap<String, String>(Map.of("X-Request-ID", requestId,
                "Date", ZonedDateTime.now().format(DateTimeFormatter.RFC_1123_DATE_TIME),
                "TPP-Session-ID", tppSessionId,
                "TPP-Redirect-URI", "https://httpbin.org",
                "TPP-Signature-Certificate", encodedCertificate,


                "ASPSP-ID", "9057",

                "PSU-IP-Address", "0.0.0.0" //Use an actual PSU ip address here!
        ));

        if(psuId != null) {
            headers.put("PSU-ID", psuId);
        }

        if(digest != null) {
            headers.put("digest", digest);
        }

        /*
        Make all headers lower case.
         */
        Map<String, String> normalizedHeaders = headers
                .entrySet()
                .stream()
                .collect(Collectors.toMap(entry -> entry.getKey().toLowerCase(), Map.Entry::getValue));


        /*
        Create signature.
         */
        String signature = createSignature(certificate, privateKey, normalizedHeaders);

        /*
        Create a request and set all headers including the signature header.
         */
        Headers.Builder headerBuilder = new Headers.Builder();
        normalizedHeaders.forEach(headerBuilder::add);

        return new Request.Builder().url(url)
                .headers(headerBuilder.build())
                .header("Signature", signature);

    }

    /**
     *
     * @param certificate Valid X509 certificate registered on a TPP
     * @param privateKey The Private key of the x509 certificate
     * @param requestHeaders
     * @return The signature
     * @throws Exception
     */
    private static String createSignature(X509Certificate certificate, PrivateKey privateKey, Map<String, String> requestHeaders) throws Exception {

         /*
         Certificate serial number on upper case hex form.

            Example output: 6AEB4444FBAAD267
          */
        String upperCaseHEXSerialNumber = certificate.getSerialNumber().toString(16).toUpperCase().trim();

        /*
        Create the keyId value by Concatenating the serial and issuer name items using SN=***,CA=***
            (Note that "CA=" is not part of the issuer name and "SN=" is not part of the serial)

            Example output:
                SN=6AEB4444FBAAD267,CA=O=PSDNO-FSA-ABCA,L=Trondheim,C=NO
         */
        String keyId = String.format("SN=%s,CA=%s",
                upperCaseHEXSerialNumber,
                certificate.getIssuerX500Principal().getName(X500Principal.RFC2253));
        System.out.println("Signature > Key ID: "  + keyId);

        /*
        Filter the headers so only the ones needed for the request are left.
         */
        List<String> headersForSigning = requestHeaders.entrySet().stream()
                .map(Map.Entry::getKey)
                .filter(headerName -> Arrays.asList(signatureHeaders).contains(headerName))
                .collect(Collectors.toList());


        /*
        Create the headers value by concatenating the header names (Concatenate with whitespace *not* comma)

            This is used to communicate the header ordering to the ASPSP for signature validation.
         */
        String headers = String.join(" ", headersForSigning);


        /*
        Concatenate all the headers to make the signing string.

            They are concatenate in this way:
                name1: value1
                name2: value2
         */
        String signingString = headersForSigning.stream()
                .map( headerName -> String.format("%s: %s", headerName, requestHeaders.get(headerName)))
                .collect(Collectors.joining("\n"));


        /*
        Sign the string using RSA SHA256.

            The output is a byte array
         */
        Signature sha256withRSA = Signature.getInstance("SHA256withRSA", new BouncyCastleProvider());
        sha256withRSA.initSign(privateKey);
        sha256withRSA.update(signingString.getBytes(StandardCharsets.UTF_8));
        byte[] signedBytes = sha256withRSA.sign();

        /*
        Validate signature (this is not strictly neccessary, the purpose in this example is to provide error messages
        if there is a problem with the input certificate or key).
         */


        /*
        Base 64 encode the byte result of the signing algorithm to get a string.
         */
        String base64EncodedSignedBytes = Base64.getEncoder().encodeToString(signedBytes);

        sha256withRSA.initVerify(certificate.getPublicKey());
        sha256withRSA.update(signingString.getBytes(StandardCharsets.UTF_8));
        boolean valid = sha256withRSA.verify(signedBytes);
        if (valid) {
            System.out.println(String.format("Signature > VALID when checked against certificate with serial %s", upperCaseHEXSerialNumber));
        } else {
            System.out.println(String.format("Signature > NOT VALID when checked against certificate with serial %s", upperCaseHEXSerialNumber));
            throw new SignatureException(String.format("Signed bytes is signed using a private key not matching the certificate with serial %s", upperCaseHEXSerialNumber));
        }

        /*
        Create the signature header value by concatenating the keyId value, headers value, algorithm used and the base 64 encoded signature.

            Example output: keyId="SN=6AEB4444FBAAD267,CA=O=PSDNO-FSA-ABCA,L=Trondheim,C=NO", algorithm="rsa-sha256", headers="date x-request-id tpp-redirect-uri psu-id", signature="***************"
         */
        String signature = String.format("keyId=\"%s\",algorithm=\"rsa-sha256\",headers=\"%s\",signature=\"%s\"", keyId, headers, base64EncodedSignedBytes);


        return signature;
    }

    private static OkHttpClient createOkHttpClient() {
        return new OkHttpClient.Builder()
                .connectTimeout(60 * 1000, TimeUnit.MILLISECONDS)
                .readTimeout(60 * 1000, TimeUnit.MILLISECONDS)
                .writeTimeout(60 * 1000, TimeUnit.MILLISECONDS)
                .build();
    }

    public static byte[] readResourceFile(String fileName) throws IOException {

        Psd2DEMOClient.class.getResource(fileName);

        try (InputStream inputStream = MethodHandles.lookup().lookupClass().getResourceAsStream(fileName)) {
            return inputStream.readAllBytes();
        }
    }

}

Steps

To perform payments the PSU need to be registered with the TPP using SCA once and each payment needs to be signed separately using SCA. (These steps are for a private context, but corporate context is very similar. See Private and Corporate Context)

Register a PSU with the TPP

To register a PSU with the TPP means getting a PSU-ID to use when calling API endpoints on behalf of that PSU. The way a PSU registers with the TPP is different if the TPP is only PISP or also AISP.

If TPP is only PISP

  1. Do a GET to /v1/users
  2. Redirect the PSU using scaRedirect, or present the link in an iframe to start the SCA flow.
  3. Persist the PSU-ID, it should be used in all future requests for this PSU.

If TPP is both PISP and AISP

In this case the PSU gets registered by following the flow in Account Information API Steps.

Perform a Single Payment

After the previous steps you should have a PSU-ID to put in the headers for the rest of the calls.

  1. Do a POST to /v1/payments/:product
  1. Follow the startAuthorization link to get a scaRedirect link.
  2. Redirect the PSU using scaRedirect, or present the link in an iframe to start the SCA flow.

The transaction status can be checked using the status link from the first POST to /v1/payment/:product.

Sequence Diagram

This shows a flow for a PISP who is not an AISP, the part from "InitiatePayment" is the same for both.

Sequence diagram
Sequence diagram

Clarifications

Logical Duplicate Error

Two payments can not have the same date, amount, debit account and credit account. This will lead to a Logical Duplicate error. This is not part of the standard, but the ASPSP implementing this API will not support duplicate payments, future implementations may make it possible to disable this check.

SEPA Credit Transfers

To send payments by SEPA Credit Transfers (SCT) the payment must: * be in Euros * be sent to a bank in a SEPA country which is a member of the SCT scheme * include a valid IBAN for the beneficiary's account * include a SWIFTBIC if the payment is to a non-EEA country

DELETE Payment not Implemented

It is not possible to delete payments using the Open Banking API, this may be implemented in the future. To cancel payments a PSU must do this directly with the ASPSP

Error Codes and Responses

See XS2A Framework Implementation Guidelines, Section 14.11 for the permitted error codes and Developer Portal for details on wich error codes that are implemented.

References

Appendix

Javascript Request Signing Code Sample

//signingExample.js
const crypto = require('crypto'); // https://nodejs.org/api/crypto.html

function sign(key, headers) {
  var headerStrings = Object.keys(headers).map(function(header) {
    return header.toLowerCase() + ": " + headers[header];
  }).join('\n');

  var signature = crypto.createSign('RSA-SHA256');
  signature.update(headerStrings);
  return signature.sign(key, 'base64');
}

function format(parameters) {
  var supportedParameters = ['keyId', 'algorithm', 'headers', 'signature'];
  var parameterStrings = supportedParameters.map(function(param) {
    return parameters[param] ? param + '="' + parameters[param] + '"' :
    '';
  });
  return parameterStrings.join(",");
}


createSignature = function(certificate, headers) {
  const signingHeaders = ['date', 'digest', 'x-request-id', 'psu-id', 'psucorporate-
  id', 'tpp-redirect-uri'];
  const headerCopy = {};
  Object.keys(headers).forEach(function(key) {
    if (signingHeaders.indexOf(key.toLocaleLowerCase()) > -1) {
      headerCopy[key] = headers[key];
    }
  })
  try {
    const signature = sign(certificate.key, headerCopy);
    return format({
      keyId: certificate.issuer,
      algorithm: certificate.algorithm,
      headers: Object.keys(headerCopy).join(' '),
      signature: signature
    });
  } catch (e) {
    console.log('failed to create signature', e);
    return null;
  }
};

createDigest = function(body) {
  if (body) {
    const digest = crypto.createHash('sha256').update(body,
    'utf8').digest('base64');
    return 'SHA-256=' + digest;
  } else {
    return null;
  }
}

main = function() {
  certificate = { // No blank spaces in the key
    // key: content of key.der file that starts with -----BEGIN PRIVATE
    KEY----- and end with -----END PRIVATE KEY-----
    key=`-----BEGIN PRIVATE KEY-----
    private key content
    -----END PRIVATE KEY-----
    `,
    issuer: 'SN=D9ED5432AA92D251,CA=O=PSDNO-FSASOMENAME,
    L=Trondheim,C=NO',
    algorithm: 'rsa-sha256'
  },
  htmlBody = "{someJsonContent: 1234}"; // content of body that is sent
  in POST requests
  headers = {
    digest: createDigest(JSON.stringify(htmlBody)),
    'x-request-id': '1234567',
    'psu-id': 'some-id',
    'tpp-redirect-uri': 'https://mydomain.com/callbacksite',
    date: 'Fri, 25 Jan 2019 13:42:20 GMT'
  }
  const signature = createSignature(certificate, headers);
  console.log(signature);
}
main();