SDK: Device authorization

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 Device authorization 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 access to a browser to complete the user authorization part of the runtime flow. This browser needs internet connectivity so that it can access your IBM Security Verify tenant.

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: Device flow

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

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=device
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 device-app.js

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

Import required SDK package

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

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 deviceClient = 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;
}

Start the device authorization process

To start the device authorization grant type flow, call the authorize function of the OAuthContext object. This returns a Promise which will resolve to an object containing the information needed to complete the flow.

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() {
  let initResult = await deviceClient.authorize();
  console.log(`\nTo authorize this app, visit: ${initResult.verification_uri}`)
  console.log(`Then input your user code: ${initResult.user_code}\n`);
  console.log('You could also cut and paste this URL into a browser:');
  console.log(initResult.verification_uri_complete);

  // Next code here

}

main()

In the code above, the verification_uri and user_code attributes returned from the authorize function are used to build a message for the user. They are instructed to go to the verification URI and input the user code. The verification_uri_complete attribute is also displayed which is a complete URI containing both the Verification URI and the user code.

📘

QR code option

By default, the response from the authorize function also includes a verification_uri_complete_qrcode attribute which is a base-64 encoded QR code image of the URL the user needs to visit to authorize the device. If the device has the ability to display images this can be a useful alternative to having the user cut and paste a URL.

Poll for user authorization completion

Once the user has been directed to complete authorization, the device must poll the token endpoint of IBM Security Verify. When the user completes authorization this poll will return the tokens requested.

The pollTokenApi function of the OAuthContext takes care of this polling. It returns a Promise that will resolve to the token response once user authorization completes. It will reject if the polling fails (due to timeout for example).

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

  console.log('\nThis program is now waiting for authorization to complete...');

  try {
    token = await deviceClient.pollTokenApi(initResult.device_code)
    token.expiry = new Date().getTime() + (token.expires_in * 1000);
    console.log('Polling complete, you are authenticated.')
  } catch (e) {
    console.log('Login failed', e);
  }

  if (token) {

    // Next code here

  }

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. The process_response utility function is used to process the response.

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

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

Run the application and test

Start the application:

node device-app.js

The following is displayed:

To authorize this app, visit: https://.../v1.0/endpoint/default/user_authorization
Then input your user code: XC4TJN

You could also cut and paste this URL into a browser:
https://.../v1.0/endpoint/default/user_authorization?user_code=XC4TJN

This program is now waiting for authorization to complete...

Cut and paste the URL into a browser. The login page of your IBM Security Verify tenant will be displayed.

Login to IBM Security Verify. If successful you will see a message indicating that user authorization of the device was successful.

Go back to the application. After a few seconds it should show the following message:

Polling complete, you are authenticated.
Name: <Your Name>
Username: <username>

This shows that the application has successfully used the OAuth access token obtained via the device authorization grant type flow.

Full source

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

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 deviceClient = 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;
}

async function main() {
  let initResult = await deviceClient.authorize();
  console.log(`\nTo authorize this app, visit: ${initResult.verification_uri}`)
  console.log(`Then input your user code: ${initResult.user_code}\n`);
  console.log('You could also cut and paste this URL into a browser:');
  console.log(initResult.verification_uri_complete);

  console.log('\nThis program is now waiting for authorization to complete...');

  try {
    token = await deviceClient.pollTokenApi(initResult.device_code)
    token.expiry = new Date().getTime() + (token.expires_in * 1000);
    console.log('Polling complete, you are authenticated.')
  } catch (e) {
    console.log('Login failed', e);
  }

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

main()