Authenticating End-Users With Node.js and Oracle Identity Cloud Service
Authenticate end-users step-by-step.
Join the DZone community and get the full member experience.
Join For FreeI once wrote about authenticating users using database tables and JWTs. While functional, custom solutions like that put the onus on you to protect the user’s credentials, they’re a far cry from a full-blown platform for security and identity. Oracle’s solution for such a platform is called Oracle Identity Cloud Service (IDCS). In this tutorial, I’ll show you how to authenticate end-users using OpenID Connect 1.0 with IDCS and protect your Node.js-based APIs.
Overview
From the OpenID Connect 1.0 spec’s abstract:
OpenID Connect 1.0 is a simple identity layer on top of the OAuth 2.0 protocol. It enables Clients to verify the identity of an end-user based on the authentication performed by an Authorization Server. It also allows Clients to obtain basic profile information about the end-user in an interoperable and REST-like manner.
To demonstrate how to add IDCS based authentication to a Node.js app, I’ll pick up the code where the series on creating a REST API left off. If you wish to follow along in this tutorial, I recommend that you clone the oracle-db-examples repo from GitHub and start with the files in javascript/rest-api/part-5-manual-pagination-sorting-and-filtering/hr_app. To run this application before making any changes, you will need to run npm install, set the environment variables referenced in config/database.js, and then run the node.
To access a completed version of this tutorial, see the javascript/idcs-authentication directory of the oracle-db-examples repo.
Note: This tutorial focuses on authenticating end-users that want to consume a Node.js-based API. Mid-tier authentication to the database is not covered and will continue to work as it did in the previous series.
The IDCS team provides many SDKs, including a Node.js module, to integrate with various platforms. The Node.js module includes a Passport strategy, so I’ll bring in Passport for authentication with Express. Finally, to avoid sending any potentially sensitive information to the client, I’ll use the express-session module combined with Passport’s session support for cookie-based sessions.
The front-end demo app is implemented using Bootstrap and Vanilla JavaScript. This should make it easy enough to follow the logic and later integrate the concepts into your front-end JavaScript framework of choice.
You will need an Oracle Cloud account to work with IDCS and complete this tutorial. If you don’t already have an account, you can request a free trial account at oracle.com/tryit.
Register the Demo App With IDCS
To allow an app to authenticate users, you need to register it with IDCS first. Follow these steps to register the Node.js demo app with IDCS.
- Log in to your Oracle Cloud account.
- Click the Navigation Drawer icon in the upper left-hand corner and select My Services Dashboard.
My services dashboard - In the My Services Dashboard page, click the Navigation Drawer icon, and select any option under Users.
Users - In the User Management page, click the Identity Console button.
Identity Console - In the Identity Console page, click the Navigation Drawer icon, and select Applications.
Selecting applications - In the Applications page, click the Add button.
Clicking add button - In the Add Application dialog, select the Confidential Application option. This will take you to the Add Confidential Application wizard.
Add confidential application wizard - In the Details pane, set Name to Node.js Demo App, then click Next >.
App details - In the Client pane, select Configure this application as a client now and populate the fields as follows:
- Allowed Grant Types: Select Client Credentials and Authorization Code.
- Allow non-HTTPS URLs: Select this option.
- Redirect URL: http://localhost:3000/callback.
- Post Logout Redirect URL: http://localhost:3000.
- While still in the Client pane, click the Add button under Grant the client access to Identity Cloud Service Admin APIs.
Adding client access to admin APIs - In the Add App Role dialog, select Authenticator Client and Me. Then, click the Add button.
- Click Next in the Client pane and in the following panes until you reach the last pane. Then, click Finish.
- Make a note of the Client ID and Client Secret values displayed in the Application Added dialog. The Node.js application will need these values to work with IDCS. Click Close when finished.
Application added - In the Node.js Demo App page, click the Activate button.
Activating demo app - In the Active Application? dialog, click the Activate Application button.
At this point, the Node.js app is registered with IDCS; you’re ready to start building out the app to authenticate users.
Before leaving the Identity Console, go ahead and download the Node.js SDK. Start by clicking the Navigation Drawer icon. Then, select Settings > Downloads. Locate the Node.js SDK from the list and click the download button. This will download a zip file to your machine that you’ll use in Part Two.
Configure the Demo App to Work With IDCS
As mentioned in the overview, I’ll start enhancing the Node.js app where the REST series left off. If you’ve not done so already, you can clone the oracle-db-examples repo and change directories to the last part of that series with the following commands:
git clone https://github.com/oracle/oracle-db-examples.git
cd oracle-db-examples/javascript/rest-api/part-5-manual-pagination-sorting-and-filtering/hr_app
Create a new configuration file named authentication.js in the config directory. Copy and paste the following code into the file and save your changes.
module.exports = {
idcs: {
classOpts: {
ClientTenant: process.env.IDCS_CLIENT_TENANT,
ClientId: process.env.IDCS_CLIENT_ID,
ClientSecret: process.env.IDCS_CLIENT_SECRET,
IDCSHost: `https://${process.env.IDCS_CLIENT_TENANT}.identity.oraclecloud.com`,
AudienceServiceUrl: `https://${process.env.IDCS_CLIENT_TENANT}.identity.oraclecloud.com`,
TokenIssuer: 'https://identity.oraclecloud.com/',
LogLevel: 'warn'
},
strategyName: 'IDCSOIDC',
authHeaderName: 'idcs_user_assertion',
authCodeScope: 'urn:opc:idm:t.user.me openid',
authResponseType: 'code',
loginRedirectUrl: 'http://localhost:3000/callback',
logoutRedirectUrl: 'http://localhost:3000'
},
sessionOpts: {
secret: '?e4TpH,),Rox9k8LBKH7',
resave: false,
saveUninitialized: false,
cookie: {
maxAge: 1000 * 60 * 60 * 8 // 1000 ms * 60 s * 60 m * 8 h = eight hours in ms
}
}
};
Overview:
- Lines 1-27: The authentication configuration module exports an object with two main properties: idcs (for IDCS specific values) and sessionOpts (for the express-session module).
- Lines 3-11: The classOpts property is used to instantiate two classes in the IDCS SDK for Node.js: OIDCStrategy and IdcsAuthenticationManager. You’ll see how these are used later on.
- Lines 12-17: The rest of the IDCS related configuration attributes will be used in various parts of the authentication module you’ll create in the next part of this tutorial.
- Lines 19-26: The express-session configuration attributes are documented here. I’ve set the session cookie to expire after 8 hours, which matches the default IDCS session expiration (found under Settings > Session Settings).
There are three environment variables used in the configuration file:
IDCS_CLIENT_TENANT.
IDCS_CLIENT_ID.
IDCS_CLIENT_SECRET.
These environment variables need to be set before running the Node.js application.
The IDCS_CLIENT_ID and IDCS_CLIENT_SECRET values were provided when registering the application with IDCS. The IDCS_CLIENT_TENANT value can be obtained from the Identity Console by clicking on your user icon and selecting About.
A dialog with IDCS details specific to your instance will be displayed. The Instance GUID, which starts with “idcs-” followed by a 32 character GUID, is the value needed for the IDCS_CLIENT_TENANT environment variable.
Modify the following commands to have the correct values, and then run them in the terminal to set the environment variables:
export IDCS_CLIENT_ID=*YOUR CLIENT ID HERE*
export IDCS_CLIENT_SECRET=*YOUR CLIENT SECRET HERE*
export IDCS_CLIENT_TENANT=idcs-*YOUR INSTANCE GUID HERE*
Note that there are additional environment variables in the config/database.js file that will need to be set to connect to the database.
With the configuration file added and environment variables set, the next step is to get the dependencies installed. Open a terminal in the application’s top-level hr_app directory. Run the following command to install the existing dependencies in the package.json file and add two new ones.
npm install -s express-session passport
Next, locate the IDCS SDK zip file that you downloaded previously and extract the contents (don’t just open, extract). Inside you’ll find two directories: docs and passport-idcs. Copy the entire passport-idcs directory to the hr_app/node_modules directory of the application.
Note that subsequent npm commands may remove the passport-idcs directory from the node_modules directory. If this happens, simply copy the directory over again. The IDCS team is working on publishing the module as a standard npm module, but this is the process for now.
The application is now configured to work with IDCS. It’s time to start building out the authentication logic.
Start a New Authentication Module
In this section, you will create an authentication module that will encapsulate all of the logic related to authentication. Create a new authentication.js file in the services directory. Copy and paste the following code into the file.
const passport = require('passport');
const expressSession = require('express-session');
const {OIDCStrategy, IdcsAuthenticationManager} = require('passport-idcs');
const config = require('../config/authentication.js');
const authMgr = new IdcsAuthenticationManager(config.idcs.classOpts);
function initWebServer(app) {
app.use(expressSession(config.sessionOpts));
passport.serializeUser(function(user, done) {
done(null, user);
});
passport.deserializeUser(function(obj, done) {
done(null, obj);
});
passport.use(new OIDCStrategy(config.idcs.classOpts, (idToken, tenant, user, done) => {
done(null, user);
}));
app.use(passport.initialize());
app.use(passport.session());
app.get('/user', user);
}
module.exports.initWebServer = initWebServer;
function user(req, res, next) {
if (req.isAuthenticated()) {
let response = Object.assign({authenticated: true}, req.user);
res.json(response);
} else {
res.json({authenticated: false});
}
}
Overview:
- Lines 1-6: Several external dependencies are required. The IdcsAuthenticationManager class is used to create a new instance named authMgr. This class contains all of the methods needed to handle authentication with IDCS and will be used in later parts of this tutorial.
- Lines 8-29: A function named initWebServer is declared and exported from the module. The web server module will invoke this method, passing along the express application. This allows all of the app’s authentication-related logic to reside in the authentication module.
- Line 9: Express-session is added to the app via app.use.
- Lines 11-17: Basic functions are defined for Passport’s serializeUser and deserializeUser methods. You can learn more about these functions in the Configure section of the Passport doc.
- Lines 19-21: The passport.use method is used to register the IDCS strategy with Passport.
- Lines 23-24: Passport’s initialize and session middleware are added to the express application.
- Line 26: A route handler for /user is added to the web server. This endpoint will be used by the frontend app to verify that the user is authenticated.
- Lines 31-38: The user function uses the isAuthenticated function that Passport adds to the request object to verify that the user is authenticated. If so, the user’s details are obtained from the user property of the request and sent back. Otherwise, an object that indicates the user is not authenticated is sent.
Now that the authentication module is in place, the next step is to call its initWebServer function from the web server module. Open the web-server.js file in the services directory. Add the following line toward the top of the file, under the line that requires the router module.
// *** line that requires ./router.js is here ***
const authentication = require('./authentication.js');
Add the following lines to the initialize function, under the lines that add the express.json middleware to the app.
// *** lines that invoke express.json are here ***
// Configure the web server to authenticate users
authentication.initWebServer(app);
With the authentication module in place, you can create the frontend demo application.
Create the Frontend Demo App
The frontend demo app is made up of just two files. Start by creating a directory named www under the hr_app directory. Within the www directory, create a new file named index.html. Copy the following code to the file and save your changes.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<title>IDCS Authentication Demo</title>
<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">
</head>
<body>
<nav class="navbar navbar-light bg-light">
<span class="navbar-brand">IDCS Demo</span>
<form class="form-inline" action="/login" method="get" hidden>
<button id="login-btn" class="btn btn-primary" type="submit">Log In</button>
</form>
<form class="form-inline" action="/logout" method="get" hidden>
<button id="logout-btn" class="btn btn-secondary" type="submit">Log Out</button>
</form>
</nav>
<div class="container">
<div class="jumbotron mt-3">
<h1>IDCS Authentication Demo</h1>
<p>This is an demo application that shows how to authenticate users and protect API routes via Oracle Identity Cloud Service.</p>
</div>
<div class="card">
<div class="card-body">
<h3 class="card-title">Employees</h3>
<p id="unauthenticated-msg" hidden>Accessing employee information requires authentication. You are not authenticated
so you will not see any employee details. <a href="/api/employees" target="_blank" rel="noopener noreferrer">Click here</a> to manually test
the API endpoint.</p>
</div>
<div class="table-responsive">
<table id="employees-tbl" class="table">
<thead>
<tr>
<th>Id</th>
<th>Name</th>
<th>Email</th>
<th>Phone</th>
<th>Job</th>
<th>Salary</th>
<th>Commission</th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
</div>
</div>
<script src="https://code.jquery.com/jquery-3.3.1.slim.min.js" integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.7/umd/popper.min.js" integrity="sha384-UO2eT0CpHqdSJQ6hJty5KVphtPhzWj9WO1clHTMGa3JDZwrnQq4sF86dIHNDz0W1" crossorigin="anonymous"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js" integrity="sha384-JjSmVgyd0p3pXB1rRibZUAYoIIy6OrQ6VrjIEaFf/nJGzIxFDsf4x0xIM+B07jRM" crossorigin="anonymous"></script>
<script src="idcs-authentication-demo.js"></script>
</body>
</html>
Overview:
- Lines 14-20: Two buttons are added to the page, one to log in and another to log out. These are form buttons that will issue GET requests to the endpoints specified in the action attribute. Handlers for those endpoints will be added to the web server later on.
- Lines 35-48: A table to display employee data is added to the page. The data will be fetched from the /api/employees endpoint, which will need to be locked down later on.
- Line 56: The main JavaScript file for the application is brought in. You will create this file in the next step.
Create another file named idcs-authentication-demo.js in the www directory. Add the following code to the file and save your changes.
(function() {
$(document).ready(init);
function init() {
fetch('/user')
.then(response => {
return response.json();
})
.then(user => {
if (user.authenticated) {
handleAuthenticated();
} else {
handleUnauthenticated();
}
});
}
function handleAuthenticated() {
$('#logout-btn').parent().removeAttr('hidden');
fetch('/api/employees')
.then(response => {
return response.json();
})
.then(employees => {
let newRowsHtml = '';
employees.forEach(function(employee) {
newRowsHtml +=
'<tr>' +
`<td>${employee.id}</td>` +
`<td>${employee.first_name} ${employee.last_name}</td>` +
`<td>${employee.email}</td>` +
`<td>${employee.phone_number}</td>` +
`<td>${employee.job_id}</td>` +
`<td>${employee.salary}</td>` +
`<td>${(employee.commission_pct === null ? '-' : employee.commission_pct)}</td>` +
'</tr>\n';
});
$('#employees-tbl').children('tbody').html(newRowsHtml);
});
}
function handleUnauthenticated() {
$('#login-btn').parent().removeAttr('hidden');
$('#unauthenticated-msg').removeAttr('hidden');
}
}());
Overview:
- Lines 3: jQuery’s ready event is used to invoke the init function when the DOM tree is loaded.
- Lines 5-17: The init function is defined. Within it, the fetch API is used to issue a GET request on the /user endpoint. If the end-user is authenticated, then the handleAuthenticated function is invoked. Otherwise, handleUnauthenticated is invoked.
- Lines 19-44: The handleAuthenticated function is defined. This function displays the logout button and then uses the fetch API to issue a GET request on the /api/employees endpoint. Because the end-user was verified as authenticated, the request should succeed, and the employee data returned is injected into the DOM.
- Lines 46-49: The handleUnauthenticated function is defined. This function displays the login button and a message indicating that the user is not logged in. The message includes a link to help test whether or not the actual API endpoint is locked down.
Now that the front-end application files are in place, the webserver needs to be updated to serve them. Return to the services/web-server.js file and add the following lines to the initialize function, under the lines that add the express.jsonmiddleware to the app.
// *** lines that invoke express.json are here ***
// Serves static files from the www directory
app.use(express.static('./www'));
Okay, it’s time to run the first test! Return to the terminal where hr_app is the working directory and the environment variables for the application have been set. Run the node to start the application. When you see the “Web server listening on localhost:3000” message, open a browser and navigate to localhost:3000. You should see the following.
Currently, if you click the Log In button, you’ll get an error because that route has not yet been defined in the Node.js app. Also, although it looks as though the employee data is safe because it’s not displayed, that is not the case. If you click the Click here link above the Employees table, you’ll see that the API route isn’t protected; it just wasn’t called.
In the steps that follow, you’ll add the authentication logic and lock down the API endpoints.
Add Logic to Log in
In this part, you’ll add the ability for end-users to authenticate with IDCS. Return to the services/authentication.js file and add the following lines of code to the initWebServer function just below the line that adds the route handler for /user.
// *** line that adds '/user' route handler ***
app.get('/login', login);
app.get('/callback',
callback,
passport.authenticate(config.idcs.strategyName),
(req, res, next) => {
const redirect = req.session.oauth2return || '/';
delete req.session.oauth2return;
res.redirect(redirect);
}
);
Overview:
- Line 3: As you’ve already seen, the /login route is used when the user clicks the Log In button in the front-end app. This route will invoke the loginmiddleware function, which is described below.
- Lines 5-15: The /callback route uses three middleware functions to complete the authentication process in Node.js after the user has successfully authenticated with IDCS. First, the callback function (described below) is invoked. Then, Passport’s authenticate middleware is used to authenticate the user using the IDCS strategy, and finally, an anonymous middleware function redirects the user to their original target page or the home page.
Copy and paste the following functions to the end of the services/authentication.js file after the user function definition. Don’t forget to save your changes.
// *** lines that define the 'user' function ***
async function login(req, res, next) {
try {
const authZurl = await authMgr.getAuthorizationCodeUrl(
config.idcs.loginRedirectUrl,
config.idcs.authCodeScope,
null,
config.idcs.authResponseType
);
res.redirect(authZurl);
} catch (err) {
next(err);
}
}
async function callback(req, res, next) {
try {
const authZcode = req.query.code;
const tokens = await authMgr.authorizationCode(authZcode);
// Storing id_token in the session (server side) for logout purposes.
req.session.id_token = tokens.id_token;
// Adding a request header that is required by the IDCS strategy.
req.headers[config.idcs.authHeaderName] = tokens.access_token;
next(); // Forwarding to passport.authenticate
} catch (err) {
next(err);
}
}
Overview:
- Lines 3-16: The login function is an Express middleware function that uses the authMgr instance to obtain the URL that the end-user is then redirected to. Upon successful authentication, IDCS will redirect the user’s browser to the callback route passing along the authZcode in the query string of the redirect header.
- Lines 18-33: The callback function obtains the authZcode from IDCS from the query string of the request object and uses it to get an id_token and an access_token (learn more about these tokens here). A header is then added to the request object before next is invoked, which forwards the request to the passport.authenticate middleware.
Restart the Node.js application and navigate to the front-end application again. If you click the Log In button, you should be redirected to the IDCS authentication page. If you were logged into your cloud account from before, you may skip this step. Either log out or use a different browser.
Note: The look and feel of the login page can be customized. See this Oracle By Example for more details.
After successfully authenticating, you should be redirected back to the application, where you’ll see the Log Out button and the employee data in the table.
Now that you can log in, all that’s left to do is add the ability to log out and lock down the API endpoints.
Add Logic to Log out
Return to the services/authentication.js file and add the following line of code to the end of the initWebServer function just below the lines that add the route handler for the /callback route.
// *** lines that add the '/callback' route handler are here ***
app.get('/logout', logout);
Next, add the following code to the end of the services/authentication.js file after the callback function definition.
// *** lines that define the 'callback' function are here ***
async function logout(req, res, next) {
try {
const logoutUrl = await authMgr.getLogoutUrl(config.idcs.logoutRedirectUrl, null, req.session.id_token);
req.logout();
req.session.destroy(err => {
if (err) {
next(err);
return;
}
res.redirect(logoutUrl);
});
} catch (err) {
next(err);
}
}
Overview:
- Line 5: The logout function first obtains the correct logout URL for IDCS and stores that in the logoutUrl variable.
- Line 7: The logout method that Passport adds to the request object is invoked, which logs the user out of Passport.
- Lines 9-16: The destroy method of the session instance is invoked to ensure that all information in the session is removed. Once complete, the user is redirected to the IDCS logout URL to log the user out there. When finished, IDCS will redirect the user to the Post Logout Redirect URL that was specified earlier.
Restart the Node.js application and ensure that the logout functionality is working as designed. Because express-session saves sessions in memory by default, you will need to log in again after restarting Node.js before you can log out successfully.
Note that even after logging out, if you click the Click here link above the employees table, you can still see employee data. You will lock down that endpoint in the last step.
Protect API Endpoints
The last step, locking down the API endpoints, is arguably the most crucial step of all! If API endpoints aren’t locked down, then authentication doesn’t really mean much. Return to the services/authentication.js file and add the following lines of code to the end of the file, just below the lines that define the logout function.
// *** lines that define the 'logout' function are here ***
function ensureAuthenticated(group) {
return function(req, res, next) {
if (req.isAuthenticated()) {
if (group === undefined) {
next();
} else if (typeof group === 'string') {
for (let groupIdx = 0; groupIdx < req.user.groups.length; groupIdx += 1) {
if (req.user.groups[groupIdx].name === group) {
next();
return;
}
}
res.status(401).send({message: 'Unauthorized'});
} else {
next(new Error('\'group\' must be undefined or a string'));
}
} else {
req.session.oauth2return = req.originalUrl;
res.redirect('/login');
}
}
}
module.exports.ensureAuthenticated = ensureAuthenticated;
Overview:
- Line 4: The ensureAuthenticated function returns an Express middleware function that can be used to secure API endpoints.
- Line 5: The isAuthenticated function that Passport adds to the request object is used to verify that the user is authenticated.
- Lines 8-16: If a group parameter was passed to ensureAuthenticated, then in addition to ensuring the user is authenticated, the middleware will ensure the user belongs to the specified group.
- Lines 20-22: If the user is not authenticated, then their destination page is saved to their session and they are redirected to login.
Open the services/router.js file and add the following line toward the top of the file, just after the line that requires in the router module.
// *** line that requires ./router.js is here ***
const authentication = require('./authentication.js');
Next, replace the existing /employees route definition (all five lines) with the following code.
router.route('/employees/:id?')
.all(authentication.ensureAuthenticated())
.get(employees.get)
.post(employees.post)
.put(employees.put)
.delete(employees.delete);
The all method of the router is used to add the ensureAuthenticated middleware. Note that ensureAuthenticated is invoked, not merely referenced. Router middleware functions are executed in the order that they’re added, so all will verify that the user is authenticated before passing control to any subsequent handlers.
Depending on your requirements, you can add the ensureAuthenticatedmiddleware in different places, such as in front of individual controller functions. This would allow some HTTP methods to be protected while others are not.
Save all of your changes and restart the Node.js application. If you are not logged in, you should no longer be able to access any employee information (even using the Click here link). You can optionally test passing a group name to the ensureAuthenticated function for a basic authorization check. Just be sure to create and add the user to the group in IDCS.
Controller logic that is executed after ensureAuthenticated will have access to the authenticated user’s information via request.user. This information can be useful for end-to-end tracing, auditing, and other security-related features of Oracle Database, such as VPD. See this section of the node-oracledb documentation to learn more.
Published at DZone with permission of Dan McGhan, DZone MVB. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments