SDK: ROPC

Introduction

This article will guide you through creating a command-line application using node.js that will allow a user to authenticate using the OAuth 2.0 Resource Owner Password Credentials (ROPC) flow supported by IBM Security Verify.

The full source code is available at the bottom of the page.

Prerequisites

You will need to have node.js installed on your system. Download node.js.

You will need to have a custom application definition created in IBM Security Verify with the following settings:

  • Sign-on method: OpenID Connect 1.0
  • Enabled grant types: Resource owner password credentials (ROPC)

You can create this application as a tenant administrator or you can use the Developer Portal.

Set up node.js

Create a project directory and initialize node.js

Create a new directory on your system and then go into that directory. This is your project directory.
All subsequent commands in this guide should be run in the project directory.

To initialize the project directory for node.js, enter the following command:

npm init -y

Install dependencies

The following dependencies are required for the application:

This command will install them into the node_modules directory - and add them to the package.json file which is used to keep track of required packages:

npm install ibm-verify-sdk dotenv readline-sync

Create .env file

The recommended way of storing environment-specific configuration (such as OAuth connection information) for a node.js application is to use an environment file. This is a file named .env in your project directory.

Using an environment file means that values are not hard coded into the source. Using the node package dotenv we can load the contents of the environment file into the process.env object.

Create a file named .env in your project directory and paste in the following content:

TENANT_URL=https://<tenant URL>
CLIENT_ID=<client id>
CLIENT_SECRET=<client secret>
FLOW_TYPE=ropc
SCOPE=openid

You will need to complete the values for TENANT_URL, CLIENT_ID, and CLIENT_SECRET.

The TENANT_URL is the URL of your IBM Security Verify tenant. This will usually have the format: https://xxx.verify.ibm.com.

You will need to get the CLIENT_ID and CLIENT_SECRET from the application definition in IBM Security Verify.

Create ropc-app.js

Create the file ropc-app.js in the project directory. This will be your application.

Import required packages

const OAuthContext = require('ibm-verify-sdk').OAuthContext;
const rls = require('readline-sync');

Load contents of .env into process.env

require('dotenv').config();

Instantiate Verify SDK OAuthContext

The context is generated by building a config object and then passing this to the OAuthContext constructor. Note that most of the configuration is being taken from variables that have been loaded from the environment file.

let config = {
    tenantUrl    : process.env.TENANT_URL,
    clientId     : process.env.CLIENT_ID,
    clientSecret : process.env.CLIENT_SECRET,
    flowType     : process.env.FLOW_TYPE,
    scope        : process.env.SCOPE
};

let ropcClient = new OAuthContext(config);

Create a global variable to hold token object

The token object will hold the tokens associated with the authenticated user. This is initially populated during the device code flow but may also need to be replaced if a SDK call returns a refreshed token.

var token;

Utility function to process SDK responses

When calling functions that require a token, the Verify SDK returns an object that contains a response object and a token object. If the token object is populated, it is a refreshed token that replaces the one currently held. The response object contains the API response.

This utility function performs the required token check and returns the embedded response object. You'll see this function used later to process the response from the userInfo call.

function process_response(response) {
  if (response.token && response.token.expires_in) {
    response.token.expiry = new Date().getTime() + (token.expires_in * 1000);
    token = response.token;
  }
  return response.response;
}

Prompt user for username and password

Use the readline-sync library to prompt the user for their username and their password. When collecting the password, do not echo to the terminal.

const username = rls.question('username: ');
const password = rls.question('password: ', { hideEchoBack: true });

Validate the username and password

Call the login function of the OAuthContext object to run the ROPC flow against your IBM Security Verify tenant. This sends the presented username and password to the token endpoint of your tenant.

Note that this code (and the rest of the code in this example application) is within an async function. This is required because the SDK functions are asynchronous; they return Promises which, in this case, are being handled with the await function.

async function main() {
 
  try {
    token = await ropcClient.login(username, password);
    token.expiry = new Date().getTime() + (token.expires_in * 1000);
    console.log("Authentication successful.\n")
  } catch (e) {
    console.log("Error.", e.messageDescription)
  }

  if (token) {

    // Next code here

  }
}

main()

Call User Info endpoint

Assuming the OIDC scope was included when the OAuthContext was initialized, the response from the token endpoint will include an id_token attribute which contains a signed JSON Web Token (JWT). This JWT can be validated and parsed to get user data.

However, rather than doing that here, you will now use the userInfo function of the OAuthContext to get identity information using the access token returned from the token endpoint.

Add the following code to ropc-app.js where it says \\ Next code here:

    let userInfo = process_response(await deviceClient.userInfo(token));
    console.log(`Username: ${userInfo.preferred_username}`);
    console.log(`Name: ${userInfo.name}`);

Run the application and test

Start the application:

node ropc-app.js

You should see a username prompt.

username: jessica
password: **********

Provide the username and password for a user that exists in your IBM Security Verify tenant.

If the username and password cannot be validated you will receive an error message:

Error. CSIAQ0264E The user name or password is invalid.

If the user is not authorized for the application you will receive and error message:

Error. CSIAQ0279E Only entitled users can single sign-on to the application. You must request for application access.

If the username and password are correct, and the user is authorized, you will see the following:

Authentication successful

Name: Jessica Bretton
Username: jessica

This shows that the ROPC flow successfully completed and that the application used the received OAuth access token to get user information from the UserInfo endpoint.

Full source

click here for the full source code
const OAuthContext = require('ibm-verify-sdk').OAuthContext;
const rls = require('readline-sync');

require('dotenv').config();

let config = {
    tenantUrl    : process.env.TENANT_URL,
    clientId     : process.env.CLIENT_ID,
    clientSecret : process.env.CLIENT_SECRET,
    flowType     : process.env.FLOW_TYPE,
    scope        : process.env.SCOPE
};

let ropcClient = new OAuthContext(config);

var token;

function process_response(response) {
  if (response.token && response.token.expires_in) {
    response.token.expiry = new Date().getTime() + (token.expires_in * 1000);
    token = response.token;
  }
  return response.response;
}

const username = rls.question('username: ');
const password = rls.question('password: ', { hideEchoBack: true });

async function main() {

  try {
    token = await ropcClient.login(username, password);
    token.expiry = new Date().getTime() + (token.expires_in * 1000);
    console.log("Authentication successful\n")
  } catch (e) {
    console.log("Error.", e.messageDescription)
  }

  if (token) {

    let userInfo = process_response(await ropcClient.userInfo(token));
    console.log(`Name: ${userInfo.name}`);
    console.log(`Username: ${userInfo.preferred_username}`);
    
  }
}

main()