Command Line Deskera API Client in Node.js | Part 4: OAuth 2.0 Authentication

Engineering Jul 10, 2020

This is Part 4 of the series of posts on writing a basic Command Line Deskera API client in Node.js.

Part 1 — Hello, World!

Part 2 — Hello, You!

Part 3 — Call an API Endpoint

Part 4 — OAuth 2.0 Authentication (this post)

Part 5 — Show Me the Data!

(Note: The GitHub links for this part are: Browse, Zip, Diff)

Set up a Public Callback URL

Deskera OAuth requires callback URL to be publicly accessible over Internet. If you can expose your application to a public IP (using port forwarding or otherwise), you may skip this section. For the purpose of this post, we will use tunneling service ngrok. There are other services which do this, you may select what works well for you.

After downloading and configuring , we will expose port 8080.

./ngrok http 8080

It will give us a public URL, which we will use for subsequent steps.

At this time, we don't need any local server listening to port 8080; it will be started later.

Sign up for Deskera Developer Account

In order to create Apps on Deskera platform, developers need to register their app first by providing

  • App name
  • Developer email address
  • The redirect URL of developer’s app (to be used for authentication flow)

We will use curl to register the app on Deskera API Platform Sandbox.

curl --location --request POST \
'https://api-dev.deskera.xyz/oauth/partner' \
--header 'Content-Type: application/json' \
--header 'Accept: application/json' \
--data-raw '{
"name": {Your App Name},
"email": {Your Email Id},
"webServerRedirectUri" : {Your Callback URL}
}'

After executing the above command successfully, we will get the following values in the response:

  • clientId
  • clientSecret

We will create a file named .env in the root of project, and add the following information.

DESKERA_AUTH_URL=https://appauth-dev.deskera.xyz
DESKERA_API_URL=https://api-dev.deskera.xyz
DESKERA_CLIENT_ID={Your Client Id}
DESKERA_CLIENT_SECRET={Your Client Secret}
DESKERA_SCOPES=read+write
DESKERA_REDIRECT_URL={Your Callback URL}

(Please refer to the Deskera developer documentation for Production endpoints.)

Add Support for API Handling

Install hapi, dotenv, open, and uuid modules as dependencies:

npm install @hapi/hapi dotenv open uuid

Authenticating via Our Command Line Application

Create a folder named src in the root folder. Create a new file named auth.js in src. Add the following code in this file:

"use strict";

const axios = require( "axios" );
const crypto = require( "crypto" );
const hapi = require( "@hapi/hapi" );
const open = require( "open" );
const querystring = require( "querystring" );
const { v1: uuid } = require('uuid');

const base64url = str => {
  return str.replace( /\+/g, "-" ).replace( /\//g, "_" ).replace( /=+$/, "" );
};
module.exports = ( { deskeraAuthUrl, deskeraApiUrl, clientId, clientSecret, scopes, redirectUrl } ) => {
  if ( !deskeraAuthUrl || !deskeraApiUrl || !clientId || !clientSecret || !scopes || !redirectUrl ) {
   throw new Error( "Deskera URLs, client ID and Secret, scopes, and redirect URL are required." );
 }

 const redirectUri = `${redirectUrl}`;

 const buildAuthorizeUrl = () => {
   const data = {
     client_id: clientId,
     response_type: "code",
     scope: scopes,
     redirect_uri: redirectUri,
     state: uuid()
   };
   const params = querystring.stringify( data );
   const authorizeUrl = `${deskeraAuthUrl}?${params}`;
   return authorizeUrl;
 };
 
 const getToken = async code => {
   try {
     const config = {
       headers: { Authorization: `Basic `.concat(Buffer.from(`${clientId}:${clientSecret}`).toString('base64'))      }
     };
     const request = {
       grant_type: "authorization_code",
       scope: scopes,
       code: code
     };
     const url = `${deskeraApiUrl}/oauth/token`;
     const data = querystring.stringify( request );

     const res = await axios.post( url, data, config);
     return res.data;
   } catch ( err ) {
     console.log( "error getting token", err );
     throw err;
   }
 };

 const executeAuthFlow = () => {
   return new Promise( async ( resolve, reject ) => {
     //Run a local web server
     const server = hapi.server( {
       port: 8080,
       host: "localhost",
       routes: {
         cors: true
       }
     } );

     server.route( {
       method: ['GET'],
       path: "/callback",
       handler: async request => {
         try {
           const code = request.query.code;
           const token = await getToken( code );
           resolve ( { token } );
           return `Token received ${token['deskera-token']}`;
         } catch ( err ) {
           reject( err );
         } finally {
           server.stop();
         }
       }
     } );
     await server.start();

     const authorizeUrl = buildAuthorizeUrl();
     open( authorizeUrl );
   } );
 };
  return {
   executeAuthFlow
 };
};

Calling executeAuthFlow goes through the following steps:

  1. A new ephemeral web server is created using hapi with  /callback route (e.g. https://localhost:8080/callback) and CORS enabled.
  2. It opens the authorization server URL in the default browser.
  3. If the user is not already logged in, he/she has to log in to the authentication server using Deskera user credentials.
  4. Once authenticated, the browser is redirected to the callback URL with a code.
  5. The callback handler uses the code and requests an authentication token using Client id and Client secret as basic authentication in headers.
  6. The authorization server validates the code, and replies with a token object  containing  deskera-token and deskera-refresh-token.
  7. The token object is returned to the caller that invoked executeAuthFlow.

Now, we will update our command line application to use  auth.js module. Create a new file under bin named login.js, and add the following code

#!/usr/bin/env node

"use strict";

const chalk = require( "chalk" );
const dotenv = require( "dotenv" );
const authClient = require( "../src/auth" );
dotenv.config();

const config = {
 deskeraAuthUrl: process.env.DESKERA_AUTH_URL,
 deskeraApiUrl: process.env.DESKERA_API_URL,
 clientId: process.env.DESKERA_CLIENT_ID,
 clientSecret: process.env.DESKERA_CLIENT_SECRET,
 scopes: process.env.DESKERA_SCOPES,
 redirectUrl: process.env.DESKERA_REDIRECT_URL
};

const main = async () => {
 try {
   const auth = authClient( config );
   const token = await auth.executeAuthFlow();
   console.log( chalk.green.bold( "Successfully authenticated Deskera CLI application!" ) );
 } catch ( err ) {
   console.log( chalk.red( err ) );
 }
};
main();

Update the package.json file to include another command in the bin section.

"bin": {
   "hello": "./bin/index.js",
   "deskera-login": "./bin/login.js"
 },

Update the CLI applications that are installed globally,

npm install -g .

and initial the login process

deskera-login

If all goes well, it should open a browser window with a user login form. Once we authenticate using a valid Deskera user credentials,

the console should display a success message

% deskera-login      
Successfully authenticated Deskera CLI application!

Almost there!

Next up: Part 5 — Show Me the Data!

Brajesh Sachan

Brajesh, drives direction of Deskera’s future technology and shapes Deskera as the technology leader. With his expertise and over 15 years of experience, he has significantly contributed to Deskera

Great! You've successfully subscribed.
Great! Next, complete checkout for full access.
Welcome back! You've successfully signed in.
Success! Your account is fully activated, you now have access to all content.