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
- Create a working directory and open it in a terminal or a command line:
mkdir ffdc-sample-nodejs && cd ffdc-sample-nodejs
- 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.
- Install the required dependencies:
npm install --save express ejs openid-client dotenv node-fetch request
- 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
- Open
package.json
and replace themain
andscripts
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
- Create a text file, named
openIdIssuer.js
, and add the following code:
const openIdClient = require("openid-client")
.exports = function () {
modulereturn openIdClient.Issuer.discover(process.env.AUTHORIZATION_WELLKNOWN)
}
This module uses the discover
function of openid-client to connect to the Discovery service of FusionCreator.
- Create a text file named
.env
, and add the following code, replacing the tokens with the corresponding values:
# OpenID connection details
="<%YOUR-CLIENT-ID%>"
CLIENT_ID="<%YOUR-SECRET-KEY%>"
CLIENT_SECRET= "https://api.fusionfabric.cloud/login/v1/sandbox/.well-known/openid-configuration"
AUTHORIZATION_WELLKNOWN ="openid"
SCOPE
# Data conection="https://api.fusionfabric.cloud"
BASE_URL
# Running port=3000 PORT
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:
- The
CLIENT_ID
andCLIENT_SECRET
are the secrets of your application in FusionCreator. - The
AUTHORIZATION_WELLKNOWN
is the URL of the Discovery service.
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:
.exports = {
modulebaseUrl : 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.
- 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')();
.defaultHttpOptions = { timeout: 3500 }
issuer
const app = express();
global.Headers = fetch.Headers;
let client;
let access_token;
- Initialize OIDC:
.then(issuer => {
issuer= new issuer.Client({
client client_id: config.client_id,
client_secret: config.client_secret
})
.listen(config.port, () => console.log(`Sample app listening on port ${config.port}!`))
app })
With the above code chunks:
- You instantiate an openid-client client.
- You pass the
CLIENT_ID
andCLIENT_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 is3000
, in this case.
View
In this section you initialize the view by enabling the EJS template engine to your express app.
.set('view engine', 'ejs');
app.use(express.static(__dirname + '/public')) app
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:
.get('/', (req, res) => {
app.render('pages/index')
res })
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:
.get('/login', async (req, res, next) => {
app
const grant = {
grant_type: 'client_credentials',
scope: config.scope
}
try {
const token = await client.grant(grant)
= token.access_token
access_token catch (e) {
} .render('pages/error', { error: e })
res
}
.render('pages/auth', {
restoken: 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:
.get('/results', async (req, res, next) => {
app
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();
.render('pages/results', {
resresults: results.countries,
})catch (err) {
} .render('pages/error', { error: err })
res
}
})
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:
.get('/logout', (req, res) => {
app// Cleanup access token
= undefined
access_token
// Back to index file
.render('pages/logout', { logout: "You successfully logged out" })
res; })
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
- 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
- pages/
- 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>
- 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>
- 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>
- 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>
- 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>
- Add the following content to footer.ejs:
<!-- views/partials/footer.ejs -->
<p class="text-center text-muted">© 2020 Finastra. All rights reserved.</p>
- 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>
padding-top:50px; }
body { </style> %>
- 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>
- 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.
- From the root of your client app, start it with the command:
$ npm start
...
Sample app listening on port 3000!
- Point your browser to localhost:3000. The list of the countries, retrieved from the Referential Data API is displayed.
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
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.
Install the following required package:
npm install --save jsonwebtoken uuid
In .env remove or comment the line with the secret value of your application. It is stored as
CLIENT_SECRET="<%YOUR-SECRET-KEY%>"
.Add the following configuration variable:
//...
# Private Key Authentication =True
STRONG
= "key"
KEYID
//...
.exports = {
module//...
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.
- Add the package imports in index.js:
const fs = require('fs');
const jwt = require('jsonwebtoken');
- Add a global variable to switch from private key authentication to standard client credentials OAuth2 flow:
global.strong = config.strong
- Add the following conditional statement to the login route call:
.get('/login', async (req, res, next) => {
app//...
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)
.client_assertion_type = 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer'
grant.client_assertion = signature
grant
}
//...
})
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.
- 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>
- Run your application as described in the previous section.
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")
.exports = function () {
modulereturn openIdClient.Issuer.discover(process.env.AUTHORIZATION_WELLKNOWN)
}
.exports = {
modulebaseUrl : 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')();
.defaultHttpOptions = { timeout: 3500 }
issuer
const app = express();
global.Headers = fetch.Headers;
let client;
let access_token;
.then(issuer => {
issuer= new issuer.Client({
client client_id: config.client_id,
client_secret: config.client_secret
})
.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) => {
app.render('pages/index')
res
})
.get('/login', async (req, res, next) => {
app
const grant = {
grant_type: 'client_credentials',
scope: config.scope
}
try {
const token = await client.grant(grant)
= token.access_token
access_token catch (e) {
} .render('pages/error', { error: e })
res
}
.render('pages/auth', {
restoken: access_token
})
})
.get('/results', async (req, res, next) => {
app
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();
.render('pages/results', {
resresults: results.countries,
})
.get('/logout', (req, res) => {
app
= undefined
access_token
.render('pages/logout', { logout: "You successfully logged out" })
res; })
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"
="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">
integrity<link rel="stylesheet" href="style.css">
<%# <style>
-top:50px; }
body { padding</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 {
-bottom: 10px;
margin
}
.btn {
margin: 10px;
}
body {margin: 10px;
}