Node.JS Client App Tutorial

The purpose of this sample project is to show you how to integrate a machine authentication flow with the FusionFabric.cloud Authorization Server, and call an API from FusionFabric.cloud, with the identity of the app.

You will implement both the standard OAuth2 Client Credentials grant flow and the private key authentication based on asymmetric cryptography.

If you prefer watching a video instead, a recording closely following this tutorial is available at Register a NodeJs app connecting to Finastra APIs.

Get it from GitHub

If you want to go ahead, and you have a GitHub account, clone the following repository, and follow the instructions from ffdc-client-credentials/README.md. The link is: https://github.com/FusionFabric/ffdc-sample-nodejs.

The provided Github repository contains also a sample client application - ffdc-authorization-code, that demonstrates the implementation of the OAuth2 Authorization Code grant flow which is not covered in the current tutorial.

Prerequisites

For this application, you will use Express, a web framework for Node.js. Therefore, you must have Node, a version greater than v8.0.0, and npm installed on your computer.

You must also register an application on FusionCreator that includes the Referential Data API from the API Catalog.

Bootstrap App

  1. Create a working directory and open it in a terminal or a command line:
mkdir ffdc-sample-nodejs && cd ffdc-sample-nodejs
  1. Initialize your Node application:
npm init

You can accept the defaults for all the options that are prompted on the terminal. The package.json configuration file is created.

  1. Install the required dependencies:
npm install --save express ejs openid-client dotenv node-fetch request 
  1. Create your main application file, an empty text file named index.js. In the following sections you will add the required code to this file.
touch index.js
  1. Open package.json and replace the main and scripts sections with the following:
{ 
// ...

  "main": "index.js",
  "scripts": {
    "start": "node -r dotenv/config index.js",
    "watch-node": "nodemon src/*",
    "serve-debug": "nodemon --inspect index.js"
  },

// ...
}

OpenID Service

You need to define a helper module to handle the connection to the FusionFabric.cloud Authorization Server.

To configure the OpenID service

  1. Create a text file, named openIdIssuer.js, and add the following code:
const openIdClient = require("openid-client")
 
module.exports = function () {
    return openIdClient.Issuer.discover(process.env.AUTHORIZATION_WELLKNOWN)
}

This module uses the discover function of openid-client to connect to the Discovery service of FusionCreator.

  1. Create a text file named .env, and add the following code, replacing the tokens with the corresponding values:
# OpenID connection details

CLIENT_ID="<%YOUR-CLIENT-ID%>"
CLIENT_SECRET="<%YOUR-SECRET-KEY%>"
AUTHORIZATION_WELLKNOWN = "https://api.fusionfabric.cloud/login/v1/sandbox/.well-known/openid-configuration"
SCOPE="openid"

# Data conection
BASE_URL="https://api.fusionfabric.cloud"

# Running port
PORT=3000

In the .env configuration file you store the variables that are passed to the openIdClient module to retrieve the authorization token. These are the following:

Configuration Module

To make it easier to configure your application, create and use a configuration module.

Create a text file, named config.js and add the following code:

module.exports = {
    baseUrl : process.env.BASE_URL,
    scope : process.env.SCOPE,
    client_id : process.env.CLIENT_ID,
    client_secret : process.env.CLIENT_SECRET || '',
    port : process.env.PORT
}

The configuration module initializes some variables with the values stored in the .env configuration file.

Main App

In this section you write the main body of your app.

  1. Open index.js and add the following code:
'use strict';

const express = require('express');
const fetch = require('node-fetch');
const config = require('./config.js')
const uuidv1 = require('uuid/v1')

// any non undefined value in param will force manual client configuration
const issuer = require('./openIdIssuer')();
issuer.defaultHttpOptions = { timeout: 3500 }

const app = express();
global.Headers = fetch.Headers;

let client;
let access_token;
  1. Initialize OIDC:
issuer.then(issuer => {
  client = new issuer.Client({
    client_id: config.client_id,
    client_secret: config.client_secret
  })

  app.listen(config.port, () => console.log(`Sample app listening on port ${config.port}!`))
})

With the above code chunks:

  • You instantiate an openid-client client.
  • You pass the CLIENT_ID and CLIENT_SECRET of the application that you created on FusionCreator, to the authorization server.
  • You configure your client application to run on port ${config.port}, which is 3000, in this case.

View

In this section you initialize the view by enabling the EJS template engine to your express app.

app.set('view engine', 'ejs');
app.use(express.static(__dirname + '/public'))

You will write the templates that are used to render the pages of your client application in a later section.

Controller

In the controller, you define the call routes for the following endpoints of your client application: the root - /, the login - /login, the results - /results, and the logout - /logout.

To define the call route for the home page

In index.js add the following code:

app.get('/', (req, res) => {
  res.render('pages/index')
})

This route renders the home page, index.html, from the corresponding EJS template.

To define the call route for the login

In index.js add the following code:

app.get('/login', async (req, res, next) => {

  const grant = {
    grant_type: 'client_credentials',
    scope: config.scope
  }

  try {
    const token = await client.grant(grant)
    access_token = token.access_token
  } catch (e) {
    res.render('pages/error', { error: e })
  }

  res.render('pages/auth', {
    token: access_token
  })

})

In the code above, you start by defining an object to store the additional details required to get the access token: the grant type and the scope. The you use the openid-client client that you initialized in a previous section, to get the access token, by calling the grant() method. You then you store the access token in a local variable that you pass to the EJS template that will render the response. In the response page, auth, you display the access token.

To define the call route for the results

In index.js add the following code:

app.get('/results', async (req, res, next) => {

  try {
    const response = await fetch(config.baseUrl + "/referential/v1/countries", {
      method: 'get',
      headers: new Headers({
        Authorization: 'Bearer ' + access_token,
        'Content-Type': 'application/x-www-form-urlencoded'
      })
    });

    if (!response.ok) {
      return res.render('pages/error', { error: response.statusText })
    }

    const results = await response.json();

    res.render('pages/results', {
      results: results.countries,
    })
  } catch (err) {
    res.render('pages/error', { error: err })
  }

})

The code illustrates the call to an API, in this case, the GetCountries endpoint of the Referential Data API. The access token is added to the headers of each subsequent call of the GET Countries endpoint. If the results are successfully fetched, they are passed to the view template to be displayed in the response page results. If an error occurs, the error page template is called.

To define the call route for the logout

In index.js add the following code:

app.get('/logout', (req, res) => {
  // Cleanup access token
  access_token = undefined

  // Back to index file
  res.render('pages/logout', { logout: "You successfully logged out" })
});

In the logout route call, you simply reset the access token and redirect the browser to the logout page.

HTML Templates

In this section you configure the view engine of your client application. You use an HTML templating engine, enabled by the EJS package.

EJS is configured to work with the templates stored in the views subdirectory of your main application directory.

To enable the view templates

  1. At the root of your application, create the views directory with the following subdirectories and empty text files:
  • views/
    • pages/
      • auth.ejs
      • error.ejs
      • index.ejs
      • logout.ejs
      • results.ejs
    • partials/
      • footer.ejs
      • head.ejs
      • header.ejs
  1. Add the following content to auth.ejs:
<html lang="en">

<head>
  <% include ../partials/head %>
</head>

<body class="container-fluid">
  <header>
    <% include ../partials/header %>
  </header>
  <main class="main">
    <div>
      <p><label>You successfully logged in with access token:</label></p>
      <textarea id="textToken" rows="10" cols="100" wrap="soft"><%= token %></textarea>
      <p></p>
      <button type="button" onclick="myFunction()">Copy to clipboard</button>
      <script>
        function myFunction() {
          document.getElementById("textToken").select()
          document.execCommand('copy')
        }
      </script>
    </div>
  </main>

  <footer>
    <% include ../partials/footer %>
  </footer>
</body>

</html>
  1. Add the following content to error.ejs:
<html lang="en">

<head>
  <% include ../partials/head %>
</head>

<body class="container-fluid">
  <header>
    <% include ../partials/header %>
  </header>
  <main class="main">
    <div class="row">

      <div class="col-sm-12">
        <div class="alert alert-danger" role="alert">
          <%= error %>
        </div>
      </div>
  </main>
  <footer>
    <% include ../partials/footer %>
  </footer>
</body>

</html>
  1. Add the following content to index.ejs:
<html lang="en">

<head>
  <% include ../partials/head %>
</head>

<body class="container-fluid">
  <header>
    <% include ../partials/header %>
  </header>
  <footer>
    <% include ../partials/footer %>
  </footer>
</body>

</html>
  1. Add the following content to logout.ejs:
<html lang="en">

<head>
  <% include ../partials/head %>
</head>

<body class="container-fluid">
  <header>
    <% include ../partials/header %>
  </header>
  <main class="main">
    <div class="row">

      <div class="col-sm-12">
        <div class="alert alert-danger" role="alert">
          <%= error %>
        </div>
      </div>
  </main>
  <footer>
    <% include ../partials/footer %>
  </footer>
</body>

</html>
  1. Add the following content to results.ejs:
<html lang="en">

<head>
  <% include ../partials/head %>
</head>

<body class="container-fluid">
  <header>
    <% include ../partials/header %>
  </header>
  <main class="main">
    <div class="row">
      <% results.forEach(function(result) { %>
      <div class="col-lg-2 col-md-4 col-sm-6">
        <div class="card">
          <div class="card-body">
            <h5 class="card-title"><%= result.name %></h5>            
            <h6 class="card-subtitle mb-2 text-muted"><%= result.currency %></h6>
            <h7 class="card-subtitle mb-2 text-muted"><%= result.alpha3code %></h7>
          </div>
        </div>
      </div>
      <% }); %>
    </div>
  </main>
  <footer>
    <% include ../partials/footer %>
  </footer>
</body>

</html>
  1. Add the following content to footer.ejs:
<!-- views/partials/footer.ejs -->
<p class="text-center text-muted">© 2020 Finastra. All rights reserved.</p>
  1. Add the following content to head.ejs:
<!-- views/partials/head.ejs -->
<meta charset="UTF-8">
<title>Finastra Node.js Sample</title>

<!-- CSS (load bootstrap from a CDN) -->
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css"
    integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">
<link rel="stylesheet" href="style.css">

<%# <style>
    body    { padding-top:50px; }
</style> %>
  1. Add the following content to header.ejs:
<!-- views/partials/header.ejs -->

<head>
  <nav class="navbar navbar-expand-lg navbar-light bg-light">    
    <span class="navbar-brand mb-0 h1">Finastra Node.js Client Credentials Code Sample</span> 
  </nav>
</head>

<h6>
 <nav class="navbar navbar-expand-lg navbar-light">   
    Secret Key Authentication
 </nav>
</h6>

<div class="btn-toolbar" role="toolbar">

  <form action="/login">
    <button type="submit" class="btn btn-primary">Login</button>
  </form>

  <form action="/results">
    <button type="submit" class="btn btn-primary">Get Data</button>
  </form>

  <form action="/logout">
    <button type="submit" class="btn btn-danger">Logout</button>
  </form>

</div>

<hr>
  1. At the root of your application, create a subdirectory named public with an empty CSS file - style.css. Add the following content:
.main {
  height: calc(100% - 150px);
}

.navbar {
  margin-bottom: 10px;
}

.btn {
  margin: 10px;
}

body {
  margin: 10px;
}

Run your App

Your are now ready to run your client application.

  1. From the root of your client app, start it with the command:
$ npm start

...
Sample app listening on port 3000!
  1. Point your browser to localhost:3000. The list of the countries, retrieved from the Referential Data API is displayed.

The countries list displayed by calling a FusionFabric.cloud API with the OAuth2 Autorization Client Credentials Grant flow.

Private Key Authentication

Up to this section, you have used the standard OAuth2 client credentials flow, that enabled you to authenticate your client application using a secret value that is associated to your application.

In this section, you learn how to use an enhanced authentication, that relies on asymmetric cryptography to authenticate your client application, without passing the client secret through the network. To learn more about private key authentication, see the dedicated section in the Platform Deep Dive guide.

To enable private key authentication in your client application

  1. Enable private key authentication in your application, as described in Private Key Authentication and store the private RSA key in a file named private.key, at the root of your client application.

  2. Install the following required package:

npm install --save jsonwebtoken uuid
  1. In .env remove or comment the line with the secret value of your application. It is stored as CLIENT_SECRET="<%YOUR-SECRET-KEY%>".

  2. Add the following configuration variable:

//...

# Private Key Authentication 
STRONG=True

KEYID = "key"

//...
module.exports = {
    //...
    key : process.env.KEYID,
    strong : (process.env.STRONG === 'True')
}

This lets you switch between the standard OAuth2 client credentials flow and the private key authentication flow by setting STRONG to True or False, in .env.

  1. Add the package imports in index.js:
const fs = require('fs');
const jwt = require('jsonwebtoken');
  1. Add a global variable to switch from private key authentication to standard client credentials OAuth2 flow:
global.strong = config.strong
  1. Add the following conditional statement to the login route call:
app.get('/login', async (req, res, next) => {
//...

  if (config.strong) {
    // Read private key
    const privateKey = fs.readFileSync('./private.key', 'utf8')

    const payload = {
      jti: uuidv1(),
      exp: Math.floor(Date.now() / 1000) + (30 * 60),
      iss: config.client_id,
      aud: config.baseUrl + '/login/v1',
      sub: config.client_id
    }

    const signOptions = {
      algorithm: "RS256",
      keyid: config.key
    }

    const signature = jwt.sign(payload, privateKey, signOptions)

    grant.client_assertion_type = 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer'
    grant.client_assertion = signature
  }

//...
})

With the above code, you use a private RSA key to sign a JSON Web Token (JWT) that you send to the Authorization Server in exchange for the access token. The Authorization Server verifies your JWT with a public RSA key that you upload to your application.

  1. Update the header.ejs template to account for the type of authentication when displaying the pages:
<h6>
  <nav class="navbar navbar-expand-lg navbar-light">   
<% if(strong){ %>
  JSON Web Key Authentication
<% } else{ %>  
 Secret Key Authentication
<% } %>
 </nav>
</h6>
  1. Run your application as described in the previous section.

The countries list displayed by calling a FusionFabric.cloud API with the private key authentication flow.

Final Code Review

Here are the code files discussed on this page.

{
  "name": "ffdc-sample-nodejs",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "start": "node -r dotenv/config index.js",
    "watch-node": "nodemon src/*",
    "serve-debug": "nodemon --inspect index.js"
  },
  "author": "",
  "license": "ISC",
  "dependencies": {
    "dotenv": "^8.2.0",
    "ejs": "^3.0.2",
    "express": "^4.17.1",
    "node-fetch": "^2.6.0",
    "openid-client": "^3.14.2",
    "request": "^2.88.2"
  }
}


 

const openIdClient = require("openid-client")
 
module.exports = function () {
    return openIdClient.Issuer.discover(process.env.AUTHORIZATION_WELLKNOWN)
}
 

module.exports = {
    baseUrl : process.env.BASE_URL,
    scope : process.env.SCOPE,
    client_id : process.env.CLIENT_ID,
    client_secret : process.env.CLIENT_SECRET || '',
    port : process.env.PORT
}
 

'use strict';

const express = require('express');
const fetch = require('node-fetch');
const config = require('./config.js')
const uuidv1 = require('uuid/v1')
const issuer = require('./openIdIssuer')();
issuer.defaultHttpOptions = { timeout: 3500 }

const app = express();
global.Headers = fetch.Headers;

let client;
let access_token;

issuer.then(issuer => {
  client = new issuer.Client({
    client_id: config.client_id,
    client_secret: config.client_secret
  })

  app.listen(config.port, () => console.log(`Sample app listening on port ${config.port}!`))
})

app.set('view engine', 'ejs');
app.use(express.static(__dirname + '/public'))

app.get('/', (req, res) => {
  res.render('pages/index')
})

app.get('/login', async (req, res, next) => {

  const grant = {
    grant_type: 'client_credentials',
    scope: config.scope
  }

  try {
    const token = await client.grant(grant)
    access_token = token.access_token
  } catch (e) {
    res.render('pages/error', { error: e })
  }

  res.render('pages/auth', {
    token: access_token
  })

})

app.get('/results', async (req, res, next) => {

  try {
    const response = await fetch(config.baseUrl + "/referential/v1/countries", {
      method: 'get',
      headers: new Headers({
        Authorization: 'Bearer ' + access_token,
        'Content-Type': 'application/x-www-form-urlencoded'
      })
    });

    if (!response.ok) {
      return res.render('pages/error', { error: response.statusText })
    }

    const results = await response.json();

    res.render('pages/results', {
      results: results.countries,
    })
    
    app.get('/logout', (req, res) => {
    
    access_token = undefined

    res.render('pages/logout', { logout: "You successfully logged out" })
});

HTML Templates

<html lang="en">

<head>
  <% include ../partials/head %>
</head>

<body class="container-fluid">
  <header>
    <% include ../partials/header %>
  </header>
  <main class="main">
    <div>
      <p><label>You successfully logged in with access token:</label></p>
      <textarea id="textToken" rows="10" cols="100" wrap="soft"><%= token %></textarea>
      <p></p>
      <button type="button" onclick="myFunction()">Copy to clipboard</button>
      <script>
        function myFunction() {
          document.getElementById("textToken").select()
          document.execCommand('copy')
        }
      </script>
    </div>
  </main>

  <footer>
    <% include ../partials/footer %>
  </footer>
</body>

</html>

 
<html lang="en">

<head>
  <% include ../partials/head %>
</head>

<body class="container-fluid">
  <header>
    <% include ../partials/header %>
  </header>
  <main class="main">
    <div class="row">

      <div class="col-sm-12">
        <div class="alert alert-danger" role="alert">
          <%= error %>
        </div>
      </div>
  </main>
  <footer>
    <% include ../partials/footer %>
  </footer>
</body>

</html>

 
<html lang="en">

<head>
  <% include ../partials/head %>
</head>

<body class="container-fluid">
  <header>
    <% include ../partials/header %>
  </header>
  <footer>
    <% include ../partials/footer %>
  </footer>
</body>

</html>

 

<html lang="en">

<head>
  <% include ../partials/head %>
</head>

<body class="container-fluid">
  <header>
    <% include ../partials/header %>
  </header>
  <main class="main">
    <div class="row">

      <div class="col-sm-12">
        <div class="alert alert-danger" role="alert">
          <%= error %>
        </div>
      </div>
  </main>
  <footer>
 

<html lang="en">

<head>
  <% include ../partials/head %>
</head>

<body class="container-fluid">
  <header>
    <% include ../partials/header %>
  </header>
  <main class="main">
    <div class="row">
      <% results.forEach(function(result) { %>
      <div class="col-lg-2 col-md-4 col-sm-6">
        <div class="card">
          <div class="card-body">
            <h5 class="card-title"><%= result.name %></h5>            
            <h6 class="card-subtitle mb-2 text-muted"><%= result.currency %></h6>
            <h7 class="card-subtitle mb-2 text-muted"><%= result.alpha3code %></h7>
          </div>
        </div>
      </div>
      <% }); %>
    </div>
  </main>
  <footer>
    <% include ../partials/footer %>
  </footer>
</body>

</html>
 

<!-- views/partials/footer.ejs -->
<p class="text-center text-muted">© 2020 Finastra. All rights reserved.</p>
 
<!-- views/partials/head.ejs -->
<meta charset="UTF-8">
<title>Finastra Node.js Sample</title>

<!-- CSS (load bootstrap from a CDN) -->
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css"
    integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">
<link rel="stylesheet" href="style.css">

<%# <style>
    body    { padding-top:50px; }
</style> %>
 
<!-- views/partials/header.ejs -->

<head>
  <nav class="navbar navbar-expand-lg navbar-light bg-light">    
    <span class="navbar-brand mb-0 h1">Finastra Node.js Client Credentials Code Sample</span> 
  </nav>
</head>

<h6>
 <nav class="navbar navbar-expand-lg navbar-light">   
    Secret Key Authentication
 </nav>
</h6>

<div class="btn-toolbar" role="toolbar">

  <form action="/login">
    <button type="submit" class="btn btn-primary">Login</button>
  </form>

  <form action="/results">
    <button type="submit" class="btn btn-primary">Get Data</button>
  </form>

  <form action="/logout">
    <button type="submit" class="btn btn-danger">Logout</button>
  </form>

</div>

<hr>

 
.main {
  height: calc(100% - 150px);
}

.navbar {
  margin-bottom: 10px;
}

.btn {
  margin: 10px;
}

body {
  margin: 10px;
}