Apps

How to create an Open Banking client app in Node.js for FDX

Learn how to create a Data Recipient application for Financial Data Exchange. Read about the authentication flow and see an example implementation in Node.js.

What is Open Banking

The idea behind Open Banking is to give the consumers of financial institutions the ability to exchange their financial information with fintech applications safely. Open Banking is becoming a global phenomenon and is being implemented in an increasing number of jurisdictions worldwide. Each jurisdiction establishes a legal framework that permits the development of a regional Open Banking ecosystem. The ecosystem aims to ensure that Recipients (such as fintech) and Data Holders (such as financial institutions) exchange data safely and efficiently.

About Financial Data Exchange

Financial Data Exchange (FDX) is a non-profit industry standards body that is handling Open Banking adoption in the United States and Canada. FDX is committed to the following five fundamental concepts of user-permitted data sharing: Control, Access, Transparency, Traceability, and Security. Its primary purpose is to promote, improve, and work toward widespread adoption of the FDX API technical standard.

FDX Ecosystem

End users use software applications (also known as Data Recipients) to manage their finances or employ financial services. Data Recipients can either use Data Access Platforms or Data Aggregators to connect to financial institutions (Data Holders) or connect to them directly.

The diagram below illustrates an example flow of a Data Recipient gaining access to a user’s account.

[mermaid-begin]
sequenceDiagram autonumber participant tpp as Data Recipient participant ci as Data Provider participant cp as Custom Consent Page participant bank as Data Holder tpp->>ci: Post request to /par ci->>tpp: Request URI (request_uri=xyz) tpp->>ci: GET request to /authorize?request_uri=xyz ci->>ci: Authenticate user ci->>cp: Redirect cp->>ci: Get FDX Consent request ci-->>cp: Consent details cp->>bank: Get user accounts bank-->>cp: Account details cp->>cp: Render Consent Page cp->>ci: Accept / Reject Consent request ci-->>cp: Redirect details ci->>tpp: Authorization code tpp->>ci: Exchange code ci-->>tpp: Tokens

Data Recipient Application

The implementation assumes the existence of a Data Provider Platform that connects to a financial institution. The article describes the authentication flow and provides details on the implementation in Node.js.

In FDX, the Data Recipient Application initiates the authorization request. For authorization, the Pushed Authorization Request (PAR) grant type can be applied. PAR ensures the highest security level for applications using it. This grant type is among the prerequisites of compliance with the FAPI security profile.

The flow starts with the application sending authorization_details to the PAR endpoint. Such a call requires some information from the OpenID server. Besides the /par endpoint URL, we must also know the /token and /authorize URLs. We can retrieve these URLs from the metadata that the OpenID server publishes through the .well-known endpoint, typically at: /.well-known/openid-configuration.

Since the authentication method for the par and token endpoints is set to “Self-Signed TLS Client Authentication,” we need to generate a Self-Signed Certificate with Private Key (X.509 PEM Format) and provide the JWKS—a Public and Private Keypair Set where the Private Key contains the x5c value—to the Data Access Platform.

Below are the certificates and JWKS. We will use them later.

  • Self-Signed Certificate (client.crt)

    -----BEGIN CERTIFICATE-----
    MIIE6jCCAtKgAwIBAgIGAYTjbUsNMA0GCSqGSIb3DQEBCwUAMDYxNDAyBgNVBAMM
    K2VnRzV5em1IVjZob3dVOXN5UHd0T011bGlrUWlnaF9lUE5pTHlJQWJ5NFEwHhcN
    MjIxMjA1MTc1NTAxWhcNMjMxMDAxMTc1NTAxWjA2MTQwMgYDVQQDDCtlZ0c1eXpt
    SFY2aG93VTlzeVB3dE9NdWxpa1FpZ2hfZVBOaUx5SUFieTRRMIICIjANBgkqhkiG
    9w0BAQEFAAOCAg8AMIICCgKCAgEAoXERLEmuaLV6v878SWgZPyatk76sjlhiwMML
    rnEpiYsyGwr1prbB4kLGHgAubW/pI7gQaSQppacjLZurB9OTRzqY3L/j8r1xuW7i
    khzKBrH178SpCkcVHPQtQia0wIQ4vevC1hD3ze8k3eNGm6qR1tFDd2Y6D2cHMaPA
    0OuC6pjsyrAtfXhWLHdtY+nEbDjNR1PwKBXg918yZRZJPy/o4tMAwJw66X74vU6R
    Pxfv9VkAgIS9j7aVbWEsr8LaN7UHVP2BRRhFASqioFeXJA2Mvnxa7Yi7uPeWr4DQ
    HZJPXjSPp9ElFsMvVdge1Q7cJPUzKKcoy+CHAZS6klqITPL0VEXUBoExUhmaEa0c
    p1oSV4cwmaC6MGbNlOR4Tv7BKa0tPOdWvEq3WmEnvVDUSqPd36vS/qnkjcdm1+hy
    BVkmTQPoslj7UR+i24EsVE5rrnIVtMf57AJXTf1BJjrYdYJJmHwQAv7Oybs1arkX
    /Nbujzigi7TLi30Rt/DYvQCQT6xd2HEStQTOKHQihwsNl3o4o4asEsJnoJB1co5h
    5VjfhQU++njGPUIMeMGErnoX3vfls6pw4B9i2VwuwFUzK5nHSFGryXbQGbzWHr/e
    z3QJK601WgmbNxQ3mVCfQmikxM0j0n9G2C4IHdzayk5enAmV3PNbtooilFOAzC3x
    Q6v99mMCAwEAATANBgkqhkiG9w0BAQsFAAOCAgEAbWxcrXpFI3ouTuMoc1srdoEu
    9Vr876gAikDHNklLYus/jRfsRlIbYCDNLCrsANzt3GBp5uvXAJvICDTwhdKdUr5F
    KwNzzIoS+IpOT6FVZBzLiqCqCPZAKV0pii82zSOY/eVTbM095wE0W9vzdUwmNki+
    e/KQFKosMXfMo+NCTRpQVZGVI+sBuuO3zz5QId3nSLDPus+UZKQFqYIEOySEXDWW
    zgkpliiHsGoOZqlGPK04zxCN9smKHvjznyWb+Ew8acf4ccL83Ly0Hk7hWrGM/6qM
    2AG/9W5uuRNdOa9bSZaf9vd24K1wc8mzuKYVDQ7o0v+SomVdXff/gEk/iLs9lSrQ
    2PyEM4C6TUWMWKPHghDnGmy/7f+jXjr3NAUCygRZrUxOc+E2z2Gro6Yu3a/X4Zcf
    LLHj4jr0Ri9uHwXaX1EzMh1zpWoU2+aWer8ZPpqlpkh5mKLOIIKOeEZlxU5pUypd
    CIP/Gx6Fh1qfb5PGt+knpKIU/zaPKAeRdFVbetrNAno3gTXHax4kapJXXbQtz7oL
    m195DlUWYIyljbwY3slig4cMht7NJIBUprTxGOyydHsrxow9wcjyfkBQAcweKltg
    mWwLZ5wahwVz8ehwg6HBV9EyqBKp4ESmdPnSiWADCqojMdH27pDsRFyR3ZggRtPF
    KSfu7p4nc0pbRdOTr8E=
    -----END CERTIFICATE-----
    
  • Private Key (client.key)

    -----BEGIN PRIVATE KEY-----
    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
    57QtKgn2h8dJ/x0WSSdt1li/QmP4
    -----END PRIVATE KEY-----
    
  • JWKS

    {
      "keys": [
        {
          "p": "1rzNLTR00cXCBkdH6SsGwIS8C2D1HNsVk7oyCkshLuN-t2loyfRGMTETWE0a_VHd7MKD5-UqVa0UCgJhY0iUqfd5hyeom1ovCFz220ApQy9n1TxGfo4Q0q9KGJRIUj6yfmTvy8sWPz-4tcV06c3k4duGobHp1JsRT-JtxGglK28GfBwwrbyEMOM9L-bL6sHlPoSN5TjwbXAPEPiehiA-pYPjaUuAZwTnfjsw2tHLe-zsx2nyTPXV8fukQVxFzV3Cs_9zUmLwB9mTPc2_e8OcCUlXGG4oJ8zrPZevr6RYm-LXGDZ0sV7f0R23fEkF_B-AL_arjOS4UBnKFKb4HMO9gw",
          "kty": "RSA",
          "q": "wHaUJI0SpTGGnz25X3w7Q-mb79OsVFvqF8LjUenfF2HfAWs23-CU59Mut4jf14Bt4uwiSTVM0ZormM29Zj8FWzsv7LOglNQsiN1sZuf-Cdmxwujs6NngIYpQsMRy1gSeL1_aKw_hNaY-cOy7fjy_FrFYs4QJHnkoA9qxscRzkUrQ8N9uCW2SBCQOHyoymbrKQLOyvrL8G1Pgcwlac6wqxBO79lGDTmm9tzL37OLqzZKq1el6FDoj9ndFwmQmhPcni-qt-YCb8sBjjD4pq281t6RTaZ8mk2P66dlEHFAK82IXgKmma0ZcrLIzJPh4r57hE3b66bU_XFFUQjTlopFtoQ",
          "d": "YOG6fYpmCN5HoI21ZtP6uqQabFxzrySpdqZV_0RXufv3gLUOABMSvRZ3UA-pzPDoxw-xgXc7KvbnLHqLo7G7Y3Ztv6PERqqbcb1yaF6wel4XieDJ1KodqHd8JYOW2yU96_NsxAwAueu2gxXeDJLK3FBOnXuSgiXA8nAjyPxcXy6Qg35AflhV1m-BDQF_Z7aasiBUMaoXjDvrwTAjmrDEoEIh5HSdAfy5htl8XpGd5e2YWIfQPktC1xYGvB9B10Zi9QUAq5dJ6c5NM838nJ0pBjMYWg3BIdoq6n1x5x0jVhRS1VG0YTmW0XxV3GQDhoZDQjU2TUsbmb5ITIXZ39u49VlofFjATKlW6S8OOZGgYpNKwhxzzvvgdwJv-QFr_XiM06lUWZvO8LU43cF-HCTS_NOSWCl8lSFYdIftBbDQevhSA3qEw15W2mQDHDXQ3GxGaWbF0FbiKEG_7lWZYhqeRD8vczUrg9e_3EpRnuI5ErcsVzgxTQ4aOv5LUTp4ZpLfJ-Dtfevv9yc5z5oblyt4tJQ4VDMI51vqkgoK0I2pqNq7DvdFfwy8tfQOnAMWcJZgHptcPlY4r1IDujS8mL9mib88KRBBTzzt3skt3BjmVaZuyo6dgX7Tf3bKHFD5pBF-DeP5aaLiZP8lRl_b5gCnzMkiIzGlvOSkJOO7KqRgE0E",
          "e": "AQAB",
          "use": "sig",
          "kid": "egG5yzmHV6howU9syPwtOMulikQigh_ePNiLyIAby4Q",
          "qi": "Znp2n7cIuh5LYG6plnF1vSLKrIUxFqs2Cd-dJRfD6m0Pnyvyw3wqPjhOvStN61U5wHthFlmqWia9Edsrdbcg-ngIzR9fxLHHC-ZpCCnBvjVnn7ctW32Bl-P45qeYMQ7xNu47tB4PBJFotno3tro63YVf-mYxGNe1TvFebE-I6LpaTWe6v_66ddwP71pXcEhH_l45h0Rm-KcHrGi4qM1NcrKkU0egSUCG934oKrSHNks2Zlbe5euhr2kEoY9fVF4r6xGFeF0eUKq2177P0Ap3Mvebu4ulE23oLdiJ4t6CIeewaFfF5ftVnPTunOe0LSoJ9ofHSf8dFkknbdZYv0Jj-A",
          "dp": "OtTBFahwdORMMvImdnAuSmJTLn7iyIQRVkbF3OIWdkUne1JqviGpdWKvnRqK0WozdeEYyFiVj6F6gv-B9PR2nlZJukiYPe-oQQ9PJmReqaR8OwRnf9amL0KM90zwv2-j8VLwC5JZRvzLOymczU12bpZ5PSWEHZZaWNt9HTk3Lb4eNbR3dbHXOZmYMt51cZyrlMQ2J93AqQrxZcHnlHGTGD-Y90kTJaZHUeMfkGO37vzLJhsVohYcBk7eAnYoPYp5hFzxGFo4zr3CREVxN5Qw1K5-KRhM8hKHRHmhTWqmSBcOvPh2Yf3PLfiJQ4iyFg__S6S1_UTQZkmafqv7_810xw",
          "alg": "RS256",
          "dq": "FZ07hd9R-LtXIwsLhm2tOjNCD-z1rYOlBoYcgVaCd13GbaJcx0excVeGJ5Z3jPwfWxypvQ7_UYq-2qpAWvs43sYG285QemZZmgVD50qmfPS1we3mrT_Kq_mclj15qA9ctHVPSwa9hAjnekcbx1GLJEFGdOKfI4z3sXcd_ZjSYb7FOEffU6-6djyWWWmjxR_1Fs8Xg9MV_PuueTzTq6nqI5sTwNXmqSe25pwSu8fAFtYh2WQfEaZ143gBnZMhwE0BY_U3FI4c9WYlx9ozU81pevY5Z-M3sdXIl_qOa5-Rm0Oo4AotZl7AiaR5me1NoGt3W3hBNX3lX7jCfazzHQ64AQ",
          "n": "oXERLEmuaLV6v878SWgZPyatk76sjlhiwMMLrnEpiYsyGwr1prbB4kLGHgAubW_pI7gQaSQppacjLZurB9OTRzqY3L_j8r1xuW7ikhzKBrH178SpCkcVHPQtQia0wIQ4vevC1hD3ze8k3eNGm6qR1tFDd2Y6D2cHMaPA0OuC6pjsyrAtfXhWLHdtY-nEbDjNR1PwKBXg918yZRZJPy_o4tMAwJw66X74vU6RPxfv9VkAgIS9j7aVbWEsr8LaN7UHVP2BRRhFASqioFeXJA2Mvnxa7Yi7uPeWr4DQHZJPXjSPp9ElFsMvVdge1Q7cJPUzKKcoy-CHAZS6klqITPL0VEXUBoExUhmaEa0cp1oSV4cwmaC6MGbNlOR4Tv7BKa0tPOdWvEq3WmEnvVDUSqPd36vS_qnkjcdm1-hyBVkmTQPoslj7UR-i24EsVE5rrnIVtMf57AJXTf1BJjrYdYJJmHwQAv7Oybs1arkX_Nbujzigi7TLi30Rt_DYvQCQT6xd2HEStQTOKHQihwsNl3o4o4asEsJnoJB1co5h5VjfhQU--njGPUIMeMGErnoX3vfls6pw4B9i2VwuwFUzK5nHSFGryXbQGbzWHr_ez3QJK601WgmbNxQ3mVCfQmikxM0j0n9G2C4IHdzayk5enAmV3PNbtooilFOAzC3xQ6v99mM",
          "x5c": [
            "MIIE6jCCAtKgAwIBAgIGAYTjbUsNMA0GCSqGSIb3DQEBCwUAMDYxNDAyBgNVBAMMK2VnRzV5em1IVjZob3dVOXN5UHd0T011bGlrUWlnaF9lUE5pTHlJQWJ5NFEwHhcNMjIxMjA1MTc1NTAxWhcNMjMxMDAxMTc1NTAxWjA2MTQwMgYDVQQDDCtlZ0c1eXptSFY2aG93VTlzeVB3dE9NdWxpa1FpZ2hfZVBOaUx5SUFieTRRMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAoXERLEmuaLV6v878SWgZPyatk76sjlhiwMMLrnEpiYsyGwr1prbB4kLGHgAubW/pI7gQaSQppacjLZurB9OTRzqY3L/j8r1xuW7ikhzKBrH178SpCkcVHPQtQia0wIQ4vevC1hD3ze8k3eNGm6qR1tFDd2Y6D2cHMaPA0OuC6pjsyrAtfXhWLHdtY+nEbDjNR1PwKBXg918yZRZJPy/o4tMAwJw66X74vU6RPxfv9VkAgIS9j7aVbWEsr8LaN7UHVP2BRRhFASqioFeXJA2Mvnxa7Yi7uPeWr4DQHZJPXjSPp9ElFsMvVdge1Q7cJPUzKKcoy+CHAZS6klqITPL0VEXUBoExUhmaEa0cp1oSV4cwmaC6MGbNlOR4Tv7BKa0tPOdWvEq3WmEnvVDUSqPd36vS/qnkjcdm1+hyBVkmTQPoslj7UR+i24EsVE5rrnIVtMf57AJXTf1BJjrYdYJJmHwQAv7Oybs1arkX/Nbujzigi7TLi30Rt/DYvQCQT6xd2HEStQTOKHQihwsNl3o4o4asEsJnoJB1co5h5VjfhQU++njGPUIMeMGErnoX3vfls6pw4B9i2VwuwFUzK5nHSFGryXbQGbzWHr/ez3QJK601WgmbNxQ3mVCfQmikxM0j0n9G2C4IHdzayk5enAmV3PNbtooilFOAzC3xQ6v99mMCAwEAATANBgkqhkiG9w0BAQsFAAOCAgEAbWxcrXpFI3ouTuMoc1srdoEu9Vr876gAikDHNklLYus/jRfsRlIbYCDNLCrsANzt3GBp5uvXAJvICDTwhdKdUr5FKwNzzIoS+IpOT6FVZBzLiqCqCPZAKV0pii82zSOY/eVTbM095wE0W9vzdUwmNki+e/KQFKosMXfMo+NCTRpQVZGVI+sBuuO3zz5QId3nSLDPus+UZKQFqYIEOySEXDWWzgkpliiHsGoOZqlGPK04zxCN9smKHvjznyWb+Ew8acf4ccL83Ly0Hk7hWrGM/6qM2AG/9W5uuRNdOa9bSZaf9vd24K1wc8mzuKYVDQ7o0v+SomVdXff/gEk/iLs9lSrQ2PyEM4C6TUWMWKPHghDnGmy/7f+jXjr3NAUCygRZrUxOc+E2z2Gro6Yu3a/X4ZcfLLHj4jr0Ri9uHwXaX1EzMh1zpWoU2+aWer8ZPpqlpkh5mKLOIIKOeEZlxU5pUypdCIP/Gx6Fh1qfb5PGt+knpKIU/zaPKAeRdFVbetrNAno3gTXHax4kapJXXbQtz7oLm195DlUWYIyljbwY3slig4cMht7NJIBUprTxGOyydHsrxow9wcjyfkBQAcweKltgmWwLZ5wahwVz8ehwg6HBV9EyqBKp4ESmdPnSiWADCqojMdH27pDsRFyR3ZggRtPFKSfu7p4nc0pbRdOTr8E="
          ]
        }
      ]
    }
    

In addition, the Data Provider must allow scopes we intend to send and get the redirect_uri address where we want to navigate later during the flow. So we pass the values for the required scopes and redirect_uri as well.

Implementation

Import Required Packages and Start Express Server

const axios = require("axios");
const https = require("https");
const fs = require("fs");
const qs = require("qs");
const express = require("express");

const app = express();
const port = 3005;

app.listen(port, () => {
  console.log(`Example app listening on port ${port}`);
});

Pass Required Information From the Data Provider and Read the CRT and KEY Files

const url = "[DATA_PROVIDER_PAR_URL]";
const client_id = "[DATA_PROVIDER_CLIENT_ID]";
const redirect_uri = `http://localhost:${port}/response-par`;

const httpsAgent = new https.Agent({
  cert: fs.readFileSync("client.crt"),
  key: fs.readFileSync("client.key"),
});

Create First Express Endpoint to Initiate the PAR Flow

app.get("/par", async (_, res) => {
  try {
    const {
      data: { request_uri },
    } = await axios({
      method: "POST",
      url,
      httpsAgent,
      headers: {
        "Content-Type": "application/x-www-form-urlencoded",
      },
      data: qs.stringify({
        client_id,
        redirect_uri,
        response_type: "code",
        authorization_details: JSON.stringify([
          {
            type: "fdx_v1.0",
            consentRequest: {
              durationType: "ONE_TIME",
              lookbackPeriod: 60,
              resources: [
                {
                  resourceType: "ACCOUNT",
                  dataClusters: [
                    "ACCOUNT_DETAILED",
                    "TRANSACTIONS",
                    "STATEMENTS",
                  ],
                },
                {
                  resourceType: "CUSTOMER",
                  dataClusters: ["CUSTOMER_CONTACT"],
                },
              ],
            },
          },
        ]),
      }),
    });

    const authorizeUrl = `[DATA_PROVIDER_AUTHORIZE_URL]?client_id=${client_id}&request_uri=${request_uri}&scope=ACCOUNT_BASIC UPDATE_CONSENTS READ_CONSENTS`;
    res.redirect(authorizeUrl);
  } catch (err) {
    console.log(error);
    res.sendStatus(500);
  }
});
app.get("/response-par", async (req, res) => {
  const { code } = req.query;

  const mtls_token_url = "[DATA_PROVIDER_TOKEN_URL]";
  const data = qs.stringify({
    grant_type: "authorization_code",
    code,
    client_id,
    redirect_uri,
  });

  try {
    const authResponse = await axios({
      method: "POST",
      url: mtls_token_url,
      httpsAgent,
      headers: {
        "Content-Type": "application/x-www-form-urlencoded",
      },
      data,
    });

    const { access_token, grant_id } = authResponse.data;

    const consentsResponse = await axios({
      method: "GET",
      url: `[DATA_PROVIDER_URL]/consents/${grant_id}`,
      httpsAgent,
      headers: {
        Authorization: `Bearer ${access_token}`,
      },
    });

    res.send(`
      <pre>
        ${JSON.stringify(authResponse.data, null, 2)}
      </pre>
      <pre>
        ${JSON.stringify(consentsResponse.data, null, 2)}
      </pre>
    `);
  } catch (err) {
    console.log(err);
    res.sendStatus(500);
  }
});

Run the Application

Launch the application. Once it’s running, a user can visit http://localhost:3005/par.

  1. The application requests a consent. It calls the PAR endpoint and provides a Rich Authorization Request object describing the consent.

  2. The user authenticates with the Data Provider.

  3. The Data Provider redirects the user to the consent page.

  4. The consent page asks for information about the requested consent and user accounts. This info is returned by the Data Holder.

  5. Depending on the user’s choice, the consent page notifies the Data Provider if the user accepted or rejected the request. When accepted, the user is redirected to redirect_uri provided by the application (http://localhost:3005/response-par).

  6. Since the PAR authorization grant with response_type: code is used, the response endpoint receives a code for the subsequent request to obtain an access token.

  7. The token and grant identifier obtained can be used to access FDX consents.

Conclusion

Upon reading this tutorial, you know the prerequisites for implementing FDX with a NodeJS application. When you need more info—our FDX-dedicated how-tos section is just a click away for you to leverage your FDX-compliant app further on.

Like what you see? Register for free to get access to a Cloudentity tenant and start exploring our platform!

Updated: Feb 21, 2023