Implement MS Graph silent flow and device code flow using NodeJS backend
In this blog post, you will learn how to request MS Graph with silent flow using device code flow with NodeJS. I will use the NodeJS application I developed for my Teams status indicator light as an example.
You can find the complete code on my GitHub.
If you would like to skip all the chit chat at the beginning you can directly go to either “Implementation” or even further to “Implementation fo files“.
Problem
As I described in the mentioned blog post I created a Teams status indicator light using a Raspberry Pi. To save resources and since the Pi will only be hanging on the wall I installed it “headless”. This means that there is no User Interface I could remote to. The only way to access the Pi is via SSH.
I basically had the following main problems.
Authentication Provider
I use the npm package “@microsoft/microsoft-graph-client” to send requests to the Graph API. There is just one problem with that. The documentation says the following:
Important Note: MSAL is supported only for frontend applications, for server-side authentication you have to implement your own AuthenticationProvider. Learn how you can create a Custom Authentication Provider.
@microsoft/microsoft-graph-client
Since the application runs on the Pi without the intervention of the user and is not a frontend application we have to create our own custom Authentication Provider.
Authentication flow
When I start the application I (or the user to display the status from) have to log in so that the software can get the teams status from the Microsoft Graph API. Since we don’t have a UI the usual MS login screen can’t be used.
Caching
Another problem is that we have to cache our authentication token after we have generated it the first time. The Microsoft Authentication Library (MSAL) npm package I use does not provide a cache out of the box.
Solution
We have to find different solutions for the different problems.
Authentication Provider
As mentioned one part of the solution is to create a custom Authentication Provider. This will be the core component of your implementation to request MS Graph with silent flow and device code flow using NodeJS.
Authentication flow
As a solution for the Authentication flow, we will use a combination of different approaches (actually It’s no different since one uses the other).
The approach we use is to check whether there is a token in the cache (including a refresh token), if so we use the silent flow to acquire a valid access token.
If there isn’t any valid token in the cache we use the Device code flow to generate one. This flow creates a token that can be used to log in on a different device.
Caching
We will use node-persist to create a cache plugin our Authentication Provider uses.
Implementation
But now let’s start with the interesting part. How everything is implemented.
You will need an empty folder which will contain our project.
Init NodeJS project
First of all we have to init our NodeJS project. This will be done with the following command.
npm init
After you have run the command the init process will ask you some questions and after that creates all the necessary files.
If you write “npm init -y” it will use all the default values and run the initialization directly.
NPM packages
Next up we have to install all the necessary npm packages.
The following we add as normal dependencies
typescript
This package installs typescript and is needed for every typescript project. Otherwise NodeJS would only “understand” plain javascript.
@azure/msal-node
This packaged will be used to implement our authentication provider and generate our access token against the Microsoft services.
@microsoft/microsoft-graph-client
The microsoft graph client is a wrapper around all graph requests and makes it easier for us to request data from the Graph API.
dotenv
It makes it possible to have a environment file to configure our application.
isomorphic-fetch
This is a polly file to make fetch requests possible in JS. The graph-client needs it when we are in a backend application.
node-persist
We use this package to persist our data. This makes the caching possible as well as persisting data even when the Pi is turned off.
node-cron
This package allows us to run a certain function on a defined schedule. Since our Indicator light needs to check the status every x minutes/seconds we use this to run our “check” function in the correct interval.
This part is not needed for requesting the Graph API. I just needed it in my Teams status indicator light and therefore figured I will explain it as well.
nodemailer
As mentioned earlier we need to login on a different device since the Pi is “headless”. To make the flow easier and take a way the need of logging into the Pi at all we send an email with the device code to a configured email address. This package enables that.
This part is not needed for requesting the Graph API. I just needed it in my Teams status indicator light and therefore figured I will explain it as well.
So here is the command we have to execute to install all the needed packages.
npm install typescript @azure/msal-node @microsoft/microsoft-graph-client dotenv isomorphic-fetch node-cron node-persist nodemailer
The second list contains the ones we only need as dev dependencies. All of those are basically the typings to the already installed packages. We just need them to have intellicense while developing our application.
So here is the command we have to execute to install all the needed packages as dev dependenices.
npm install -d @microsoft/microsoft-graph-types @types/node @types/node-cron @types/node-persist @types/nodemailer
Configuration
After installing all the necessary packages we have to configure our application.
packege.json
In the package.json we add two scripts to the list of scripts.
The first one is called “start” which will run tsc to transpile our typescript to Javascript and the second is executing our statusIndicator.js using node.
"start": "tsc && node dist/StatusIndicator.js",
The second script will only build/transpile our TypeScript files to JavaScript.
"build": "tsc",
tsconfig.json
Next step is to create a tsconfig.json file in the root folder.
To generate a standard tsconfig.json you can also run “tsc –init”
For our purpose the file looks like this.
{ "compilerOptions": { "target": "es5", "module": "commonjs", "lib": ["es6"], "allowJs": true, "outDir": "./dist/", "strict": true, "noImplicitAny": true, "esModuleInterop": true, "resolveJsonModule": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true , "sourceMap": true, }, }
.env file
Last part of the configuration would be our .env file which should be created in the root folder as well. When using the dotenv package this file will be read automatically and we can directly use the configs in it.
For our status indicator the file looks like the following
# Credentials TENANT_ID=<Tenant ID> APP_ID=<APP ID> # Endpoints AAD_ENDPOINT=https://login.microsoftonline.com/ GRAPH_ENDPOINT=https://graph.microsoft.com/ # General Config POLL_INTERVAL=1 POLL_WEEKENDS=false START_TIME="7:30" END_TIME="17:00" DEBUG=false BRIGHTNESS=0.05 DEVICE_CODE_REQUEST_TIMEOUT=20 # Email settings EMAIL_ON=false EMAIL_HOST="<SMTP host>" EMAIL_PORT=<SMTP Port> EMAIL_USER="<User which should be used to send emails>" EMAIL_PASSWORD="<Password of the user that should send emails>" EMAIL_TO="<Email which should receive your emails>"
No all of those configurations are needed just to request the Graph API.
Implementation of files
Now that we have all the things set up we can finally start writing code to request MS Graph with silent flow and device code flow using NodeJS.
Config
First thing we create is a helper to get our config from the .env file. We create a folder called helper in the “src” folder. In the helper folder we create a config.ts file.
In this file we
- import “dotenv”
- get the config
- export the parsed configurations
const dotenv = require('dotenv'); const result = dotenv.config(); if (result.error) { throw result.error; } const { parsed: envs } = result; module.exports = envs;
Storage Service
Next up is a storage service to be used by the caching plugin later. We create a new folder called “services” in our “src” folder. In there we create a new file called “storageService.ts”.
The service uses node-persist to save stuff to the disc. The basic principle is to store a given value at a given identifier. It basically is a key-value pair. We have the following functions in the service.
init
This function initializes node-persist and needs to be executed before anything else is done.
async init() : Promise<void> { await storage.init() };
clear
It will clear all the stored data. This can be used to for example log out.
async clear(): Promise<void> { if(!isInitiated){ await this.init(); isInitiated = true; } await storage.clear(); };
getFromStorage
This function gets information from the storage. It needs the identifier of the data one would like to read.
async getFromStorage(identifier: string): Promise<string | null> { if(!isInitiated){ await this.init(); isInitiated = true; } const data = await storage.get(identifier); if (data) { return data; } else { return null; } };
setToStorage
It sets data to the storage. It takes the identifier and the value as a parameter.
async setToStorage(identifier: string, value: string): Promise<void> { if(!isInitiated){ await this.init(); isInitiated = true; } await storage.set(identifier, value); };
removeFromStorage
It removes data from storage. It takes the identifier which should be removed.
async removeFromStorage(identifier: string): Promise<void> { if(!isInitiated){ await this.init(); isInitiated = true; } await storage.del(identifier); }
setDeviceCodeDate
Sets the current date as the device code date.
async setDeviceCodeDate(): Promise<void> { const date = new Date(); await this.setToStorage(deviceCodeDateStorageIdentifier, JSON.stringify(date)); };
getDeviceCodeDate
Gets the currently saved device code date.
async getDeviceCodeDate(): Promise<Date | null> { const date = await this.getFromStorage(deviceCodeDateStorageIdentifier); if (date) { return new Date(date); } else { return null; } };
removeDeviceCodeDate
Removes the currently saved device code date.
async removeDeviceCodeDate(): Promise<void> { await this.removeFromStorage(deviceCodeDateStorageIdentifier); };
The complete code can be found here.
MSAL Cache Plugin
The next part is to implement a custom MSAL Cache Plugin which uses our storageService to cache the aquired token. Those Plugins need to follow a simple template to get them working with MSAL. It needs a “beforeCacheAccess” and a “afterCacheAccess” function.
The first one will be called to get everything from storage and the second one to save stuff to storage.
We create a file called “msalCachePlugin.ts” in our helper folder and paste the following code.
import { storageService } from './../services/storageService'; const _storageService = new storageService(); async function beforeCacheAccess(cacheContext: any) { return new Promise<void>(async (resolve, reject) => { cacheContext.tokenCache.deserialize(await _storageService.getFromStorage("tokenCache")); resolve(); }); } async function afterCacheAccess(cacheContext: any) { return new Promise<void>(async (resolve, reject) => { if(cacheContext.cacheHasChanged){ await _storageService.setToStorage("tokenCache", cacheContext.tokenCache.serialize()); resolve(); } else { resolve(); } }); } module.exports = { beforeCacheAccess: beforeCacheAccess, afterCacheAccess: afterCacheAccess }
Authentication Provider
The Authentication Provider is a bit more complex than the stuff we looked at until now. It also is the core part of the authentication against the Graph API. We create a new file called “authProvider.ts” in our helper folder.
To work it needs a class that implements “AuthenticationProvider” from “@microsoft/microsoft-graph-client”. The class also needs a “getAccessToken” function which is a string promise. This means that this function always has to return a string containing the accessToken.
It is up to the developer to implement this function correctly.
First of all, we get all the different “accounts” from our MSAL cache. Since we only use one account at the same time we can ignore that it is an array and only take the first one. If there is an account we try to acquire a token with the Silent flow by requesting the function “acquireTokenSilent” of the msal.PublicClientApplication.
In case of an error or when no account was present, we request a different function called “deviceCode”. This function will try to acquire a token using the device code flow by requesting the function “acquireTokenByDeviceCode”. It also sends an email with the code to the configured recipient using the Mail service I will describe a bit further down.
Lets look at the code.
First we have to import all the stuff we need.
import { AuthenticationProvider } from "@microsoft/microsoft-graph-client"; const { APP_ID, TENANT_ID, AAD_ENDPOINT, DEVICE_CODE_REQUEST_TIMEOUT, DEBUG} = require('./config'); const msal = require('@azure/msal-node'); import { mailService } from './../services/mailService'; import { storageService } from './../services/storageService'; const cachePlugin = require('./msalCachePlugin')
After that we create the class and all the variables we need.
export class CustomAuthenticationProvider implements AuthenticationProvider { config: any; pca: any; tokenCache: any; scopes: string[] = ["user.read", "presence.read", "offline_access"]; _mailService = new mailService(); _storageService = new storageService();
In the scope variable, you can define which access the application should request when the user logs in to it.
The constructor will create our PublicClientApplication with the correct configuration.
constructor(){ this.config = { auth: { clientId: APP_ID, authority: AAD_ENDPOINT + TENANT_ID }, cache: { cachePlugin } }; this.pca = new msal.PublicClientApplication(this.config); this.tokenCache = this.pca.getTokenCache(); }
As mentioned the class needs a “getAccessToken” function. In our case it tries the Silent flow and falls back to the devide code flow.
public async getAccessToken(): Promise<string> { try{ let accounts = await this.tokenCache.getAllAccounts(); if(accounts.length > 0){ return await this.pca.acquireTokenSilent({scopes: this.scopes, account: accounts[0]}).then(async (tokenResponse: any) => { console.log("Logged in as: " + tokenResponse.account.username) return tokenResponse.accessToken; }).catch(async (error: any) => { console.log(error); return await this.deviceCode(); }); } else { return await this.deviceCode(); } } catch(error: any){ console.error(error); throw error; } }
The last part is the device code flow fallback function. It tries to acquire the token via the device code flow and sends out an email if this is configured.
private async deviceCode(): Promise<string>{ const now: Date = new Date(); var deviceCodeDate = await this._storageService.getDeviceCodeDate(); if(DEBUG === "true" && deviceCodeDate){ console.log("Stored Device Code Date: " + deviceCodeDate); } if(!deviceCodeDate || deviceCodeDate <= now){ this._storageService.removeDeviceCodeDate(); const deviceCodeRequest = { deviceCodeCallback: (response: any) => { console.log(response.message); var expiresOn = new Date(now.getTime() + DEVICE_CODE_REQUEST_TIMEOUT * 60000); this._mailService.sendLoginEmail(expiresOn.toLocaleString(), response.message); this._storageService.setDeviceCodeDate(); }, scopes: this.scopes, timeout: DEVICE_CODE_REQUEST_TIMEOUT, }; await this.pca.acquireTokenByDeviceCode(deviceCodeRequest).then(async (response: any) => { if(DEBUG === "true"){ console.log(JSON.stringify(response)); } console.log("Logged in as: " + response.account.username) return response.accessToken; }).catch((error: any) => { console.log(error.message); console.log(error.stack); throw error; }); } else { if(DEBUG === "true"){ console.log("Device Code was requested within timout frame. Doing nothing."); } } return ""; }
You can find the complete code here.
LED Service
This part is not needed for requesting the Graph API. I just needed it in my Teams status indicator light and therefore figured I will explain it as well.
To be able to set our blinkti LEDs to the correct colour I am using an npm package called node-blinkt. Unfortunately, this package can only be installed in a Raspbery Pi since it requires WiringPi. For that reason, I will only log the colour to the console as a mock.
This file “ledService.ts” should be in the services folder. The Service has different functions for Available, Away and Busy as well as two helper functions to clear the LEDs and blink when an error occures.
You can find the complete code here.
Mail Service
This part is not needed for requesting the Graph API. I just needed it in my Teams status indicator light and therefore figured I will explain it as well.
It will be used to send emails, as the name suggests. The service is rather simple and only has two functions. One to send a general mail (it takes to, subject and the text as parameters) and one to send a login mail (it takes expiresOn and the Message as parameters).
You can find the complete code here.
StatusIndicator
The core of our team status indicator light is a file called StatusIndicator.ts, which is located directly in the “src” folder.
You can find the complete code here.
The Indicator contains one function called “app” which is the one that gets called once when the status light starts. It requests a second function called “checkStatus” and starts a cron based on the configuration.
async function app(){ _storageService.removeDeviceCodeDate(); //_storageService.clear(); checkStatus(); const intervall = (POLL_INTERVAL > 0 && POLL_INTERVAL < 60)? POLL_INTERVAL : 1; const seconds = (DEBUG === "true")? "*/15 " : ""; const minutes = "*/" + intervall + " "; const hours = (getEndHour() === 23)? "* " : getStartHour() + "-" + (getEndHour() + 1) + " "; const daysOfMonth = "* "; const month = "* " const daysOfWeek = (POLL_WEEKENDS === "true")? "*" : "1-5" const cronSchedule = seconds + minutes + hours + daysOfMonth + month + daysOfWeek; cron.schedule(cronSchedule, checkStatus); if(DEBUG === "true"){ console.log("Cron started with following schedule: " + cronSchedule); } }
“checkStatus” is the main function. Every time it get’s called it checks for the teams status and sets the colour of the LEDs accordingly.
Here is the complete function.
async function checkStatus(){ console.log("--------------------------------------------------") console.log(new Date()); console.log("Checking Presence Status"); if(withinConfiguredTimeframe()){ if(DEBUG === "true"){ console.log("Within configured timeframe. Will proceed"); } const authProvider = new CustomAuthenticationProvider(); const options = { authProvider, // An instance created from previous step }; const graphClient = Client.initWithMiddleware(options); try { if(DEBUG === "true"){ console.log("Attempt to get presence"); } let presence = await graphClient.api("me/presence").get(); if(presence && presence.availability){ if(presence.availability === Availability.Available || presence.availability === Availability.AvailableIdle){ _ledService.setAvailable(); } else if(presence.availability === Availability.Away || presence.availability === Availability.BeRightBack){ _ledService.setAway(); } else if(presence.availability === Availability.Busy || presence.availability === Availability.BusyIdle || presence.availability === Availability.DoNotDisturb){ _ledService.setBusy(); } else { _ledService.clearLEDs(); } } if(DEBUG === "true"){ console.log(presence); console.log("ready with getting presence"); } } catch (error) { console.log(error); await _ledService.errorBlink(); throw error; } } else { _ledService.clearLEDs(); if(DEBUG === "true"){ console.log("Outside configured timeframe. Will try again."); } } }
The interesting part is between line 10 and 22.
First we initialize a new instance of our custom Authentication Provider. Then we generate a graph Client with the mentioned provider instance. Lastly we request the Graph API. Here you could basically request what ever endpoint you like.
Conclusion
As you can see it isn’t very easy to request MS Graph with silent flow and device code flow using NodeJS backend application. If you know what to do it is still rather straight forward.
I hope this article could help you. Please feel free to contact me with any questions you have.
You can also subscribe and get new blog posts emailed to you directly.