Intro

This tutorial expands on part one of this series. You’ll continue to explore using Roles-Based Access Control (RBAC) claims in an Oracle Cloud Infrastructure (OCI) Identity and Access Management (IAM) JSON Web Token (JWT) to access Oracle REST Data Services (ORDS) protected resources (i.e., API endpoints). In part one, you learned how this was done using an API testing tool (like Insomnia, or Postman).

But in part two of this tutorial you’ll test and experiment this RBAC JWT capability using a sample JavaScript single-page web application (HTML, Node.js and the Express.js framework). This sample application is browser-based, and asks the user to sign-in with their IAM user credentials, much like you might see with other “social login” methods (e.g., Azure, GitHub, Outlook, Gmail, Facebook, etc.).

If you are unfamiliar with the configuration and set-up required to follow along in part two of this tuturial, you should review part one first. Make sure you have completed all prerequisites found in part one; in particular steps 5-7 of the configuration.

Example 2: JavaScript and Node.js/Express.js

This application will consume a single ORDS API and display the results in the single-page web application. You are free to fork the repository, and expand or alter the code base. The application relies on JavaScript, HTML, Node.js (as a backend) with the Express.js framework. You may configure your application project however you like, but to replicate the application as you see in this tutorial, you should follow the structure seen here:

jwts/
├── node_modules
├── package-lock.json
├── package.json
├── public
│   ├── app.js
│   └── index.html
├── server.js
└── ordsdemo.sql

Project folder

All sample code can be found at the following GitHub repository.

You may optionally create your own project folder with the sample application code found on this page. The project consists of four main files. You’ll notice the ordsdemo.sql file. If you are coming from part one of this tutorial, you have already used this file. If you are not coming from part one of this tutorial, you will need to execute the ordsdemo.sql file in your schema. See part one for the steps to achieve this.

The main files for this project are below. The code features commenting to assist you in understanding the application’s execution path.

App.js

const loginButton = document.getElementById('login');

// There are several ways this can be accomplished. The focus shouldn't be on performance, or
// whether this conforms purely to ESM. 
const { clientId, tenantUrl } = await fetch('/config').then(r => r.json());
const authorizationEndpoint = `${tenantUrl}/oauth2/v1/authorize`;
const redirectUri = window.location.origin + '/callback';
const scope = 'audience01iam_groups';

loginButton.addEventListener('click', () => {
  const params = new URLSearchParams({
    response_type: 'code',
    client_id: clientId,
    redirect_uri: redirectUri,
    scope
  });

  // The "params" are sent to the IAM /oauth2/v1/authorize endpoint.
  window.location = `${authorizationEndpoint}?${params.toString()}`;
});

// Handling the redirect from Oracle IAM. RECALL, you'll need to set this redirect up in 
// the OAuth Configuration section for your Integrated Application (in IAM).
(async function handleRedirect() {
  const params = new URLSearchParams(window.location.search);
  const code = params.get('code');

  if (!code) return;

  // Using this Authorization code, to retrieve a JWT from the IAM /oauth2/v1/token endpoint.
  const response = await fetch('/exchange_token', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ code })
  });

  const tokenData = await response.json();

  if (tokenData.error) {
    console.log("Token exchange error:\n" + tokenData);
  } else {

    // A GET request is issued to the ORDS endpoint via the Server.js backend.
    const accessToken = tokenData.access_token;

    const ordsInfo = await fetch('/to_ords', {
      headers: { Authorization: `Bearer ${accessToken}` }
    }).then(r => r.json());

    // With the response in hand, we display it on the screen
    // the results of the GET request.

    const dbActual = document.createElement('p');
    const crtUsr = document.createElement('p');

    // dbActual and crtUser refer to the output bind parameters found in the individual ORDS Resource Handlers. Refer to the ORDS > Resource Module section of part one of the blog series to review the ORDS Resource Module/Template/Handler definitons.
    dbActual.innerHTML = `The current <code>SYSTIMESTAMP</code> for your database server is: <code>${ordsInfo.dbActual}</code>`;
    crtUsr.innerHTML = `You are currently logged in as the following user: <code>${ordsInfo.crtUser}</code>`
    document.body.appendChild(dbActual);
    document.body.appendChild(crtUsr);

      
      if (loginButton) {
        loginButton.classList.add('fade-out');
        setTimeout(() => loginButton.remove(), 500);
      };
    
    const backButton = document.createElement('button');
    backButton.textContent = 'Home';
    backButton.style.color = 'blue'
    backButton.style.marginTop = '1rem';
    backButton.addEventListener('click', () => {
      window.location.href = '/';
    });
    document.body.appendChild(backButton);
    
  };
})();

Server.js

import express from 'express';
import fetch from 'node-fetch';
import dotenv from 'dotenv';
import path from 'path';
import { fileURLToPath } from 'url';

// Required to load the .env file from your project. 
dotenv.config();

// We've "abstracted" the following properties:
// - CLIENT_ID
// - CLIENT_SECRET
// - TENANT_URL
// - REDIRECT_URI
// - AUTH_ENDPOINT
// - TOKEN_ENDPOINT
// - ORDS_ENDPOINT

const app = express();
const PORT = process.env.PORT || 3000;

// The following are used to construct file paths reliably, especially for:
// - Sending static files 
// - Locating `.env` or other local files
// - Navigating relative directories

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

// This first line ensures we’re pointing to the correct absolute file path for `index.html`, 
// no matter where or how the app is launched.

app.use(express.static(path.join(__dirname, 'public')));
app.use(express.json());
app.use(express.urlencoded({ extended: true }));

// These may need to be modified depending on which OAuth2.0 Code Flow you are using. In this case, we use
// Authorization Code, since we need to identify the user. In this case, the user is assigned to an OCI IAM Group.
// And that Group is mapped to an ORDS Role. The ORDS Role is assigned to an ORDS Privilege. That is how we map the OCI IAM 
// Group to the ORDS JWT Profile. 

// Note: The callback needs to be added to your OCI Integrated Application's OAuth2.0 Configuration.

const clientId = process.env.CLIENT_ID;
const clientSecret = process.env.CLIENT_SECRET;
const tenantUrl = process.env.TENANT_URL;
const redirectUri = process.env.REDIRECT_URI;
const tokenEndpoint = `${tenantUrl}/oauth2/v1/token`;
const ordsEndpoint = process.env.ORDS_ENDPOINT;

// Adding this so we can securely retrieve the Client ID and Tenant URL from the .env file.

app.get('/config', (req, res) => {
  res.json({
    clientId: process.env.CLIENT_ID,
    tenantUrl: process.env.TENANT_URL
  });
});

// To keep this application simple, the /callback endpoint ultimately ends up serving the index.html page.
app.get('/callback', (req, res) => {
  res.sendFile(path.join(__dirname, 'public', 'index.html'));
});

// This step happens AFTER you have signed into IAM with your user credentials. You have arrived here via the handleRedirect() function in the 
// app.js file.
app.post('/exchange_token', async (req, res) => {
  const { code } = req.body;

  // These parameters are ALL sent to the /oauth2/v1/token endpoint, so that the client/user can retrieve a valid JWT.
  const body = new URLSearchParams({
    grant_type: 'authorization_code',
    code,
    redirect_uri: redirectUri,
    client_id: clientId,
    client_secret: clientSecret
  });

  try {
    const response = await fetch(tokenEndpoint, {
      method: 'POST',
      headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
      body: body.toString()
    });

    // With the JWT "in hand," the application "exits" the /exchange_token endpoint (and its functions).

    const tokenData = await response.json();

// We convert the JSON Object to a JSON-formatted string. And assign/set the 
// header to "Content-Type: application/json". We also well as sending to the app.js
// front end. 
    res.json(tokenData);
  } catch (err) {
    console.error(err);
    res.status(500).json({ error: 'Token exchange failed' });
  }
});

// "Splitting" on the whitespace between Bearer ' ' and the token, and using that to issue a GET
// request to the ORDS endpoint. 
app.get('/to_ords', async (req, res) => {
  const accessToken = req.headers.authorization?.split(' ')[1];

  try {
    const ordsInfoResponse = await fetch(ordsEndpoint, {
      headers: {
        Authorization: `Bearer ${accessToken}`
      }
    });
    
    // The response from the GET request to the ORDS endpoint.
    const ordsInfo = await ordsInfoResponse.json();

    // The "res" will be passed back to the app.js front end for display. Which will then in turn
    // pass to the index.html page.

    res.json(ordsInfo);
  } catch (err) {
    console.error(err);
    res.status(500).json({ error: 'Fetching ORDS data failed, review console details for clues.' });
  }
});

app.listen(PORT, () => {
  console.log(`Server running at http://localhost:${PORT}`);
});

Index.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>ORDS and RBAC JWTs</title>
  <style>
    #login.fade-out {
      transition: opacity 0.5s ease;
      opacity: 0;
    }
  </style>
</head>
<body>
  <h3 style="color: darkslategray">Demo: ORDS Roles-Based Access Claims (RBAC) and OCI Identity and Access Management JSON Web Tokens (JWTs)</h3>
  <button style="color: blue" id="login">Login as IAM user</button>
  <script type="module" src="app.js"></script>
</body>
</html>

.ENV

CLIENT_ID=
CLIENT_SECRET=
TENANT_URL=
REDIRECT_URI=http://localhost:3000/callback
AUTH_ENDPOINT=
TOKEN_ENDPOINT=
ORDS_ENDPOINT=http://localhost:8080/ords/ordsdemo/jwtdemoalpha/alpha_group

Application dependencies

NPM

You can  add the required dependencies for the application with the following command:

npm install express dotenv node-fetch

.ENV file

If you choose to use an .ENV file, you’ll need to configure yours to match your development environment and OCI IAM settings. You should replace the following:

CLIENT_ID=[Your Integrated Application Client ID]
CLIENT_SECRET=[Your Integrated Application Client Secret]
TENANT_URL=https://idcs-[Your unique Tenant Identifier].identity.oraclecloud.com:443
REDIRECT_URI=http://localhost:3000/callback
AUTH_ENDPOINT=https://idcs-[Your unique Tenant Identifier].identity.oraclecloud.com:443/oauth2/v1/authorize
TOKEN_ENDPOINT=https://idcs-[Your unique Tenant Identifier].identity.oraclecloud.com:443/oauth2/v1/token
ORDS_ENDPOINT=http://localhost:8080/ords/ordsdemo/jwtdemoalpha/alpha_group

Note: Your ORDS endpoint may differ depending on your installation and deployment. The use of a .ENV file is optional; although, code changes will be required if you decide to omit it.

OCI IAM configuration

This demo application will rely on the settings configured in your OCI IAM ords-jwt-demo-app Integrated Application. Refer to part one of this tutorial for configuration settings. Verify that you have also included the following Client configuration settings:

  • Allowed grant types: Authorization code
  • Redirect URL: http://localhost:3000/callback

Note: Additional grant types and redirect URLs may be included. However, the above values must be included to reproduce this demo as-is.

ORDS installation and configuration

In this demonstration, ORDS has been installed locally, with an Oracle 23ai database running in a Podman container. ORDS has been deployed in Standalone mode (embedded Jetty server) on localhost on port 8080. The schema used is named ORDSDEMO. The ORDS Resource Modules, Templates, Handlers, Roles, and Privileges remain unchanged from the previous example in part one of this tutorial. If you have not created your ORDS APIs, you’ll need to review part one to set this up.

Note: ORDS and Oracle Database 23ai Docker/Podman containers are both available in the Oracle Container Registry.

Launching the demo app

With the configuration complete, and from within your project’s root folder, you can launch the application using a Node.js server from your IDE’s console. Use the following command:

node server.js

You will see the following output in your IDE’s console (view the GIF below for reference):

Server running at http://localhost:3000

Click the link to open the app’s Index.html page. Then click the button. You will be temporarily redirected to the OCI IAM Sign In page. Sign in with the following credentials:

  • User Name: alphauser
  • Password: [Password selected upon creating the alphauser]

Once you have signed in, you will be redirected to the sample application. The results of the ORDS GET request will be displayed on screen:

The current SYSTIMESTAMP for your database server is: 13-MAY-25 06.55.23.452414 PM +00:00

You are currently logged in as the following user: alphauser

Sample demonstration 

GIF demonstrating the OAuth code flow in IAM

If you clear your browser’s cache and press the button again, you’ll be redirected to the OCI IAM Sign In page. Sign in as the Beta User, and you will be redirected back to the sample application. Both fields will return the following message:

The current SYSTIMESTAMP for your database server is: undefined

You are currently logged in as the following user: undefined

Invalid JWT/Unauthorized

The web browser displays “undefined” in both fields because the JWT used was invalid. If you decode (using  the JWT, you would notice the following Custom Claim:

{...
"iam_groups": [ "betagroup" ]
...}

Note: You may decode a JWT using the library of your choice, or with a browser-based tool like jwt.io or jwt.ms.

Since the /ordsdemo/alpha_v1/alpha_group endpoint is protected by the alphagroup role and since the alphagroup privilege does not include the betagroup role, this Beta User cannot access this endpoint.

Wrap-up

By now, you should have a better understanding of how to:

  1. Create a custom claim from a Group in your Identity Domain’s Integrated Application
  2. Protect ORDS with a role that matches the custom claim and create the requisite JWT Profile
  3. Navigate an OAuth2.0 Authorization Code grant type (for acquiring an OCI IAM JWT) two ways:
    • cURL
    • Single-page JavaScript web application using Node.js and Express.js

Resources

The following are helpful resources when working with ORDS and JWTs