MS Graph API + Microsoft Authentication Library
In the process of refactoring an internal tool that connects to the Microsoft Graph API I re-worked the process of retrieving an authentication token that is needed for making a request to the MS Graph API.
While doing some research on how to best get the needed access token, I came across the Microsoft Authentication Library for JavaScript. The library "enables both client-side and server-side JavaScript applications to authenticate users using Azure AD for work and school accounts (AAD), Microsoft personal accounts (MSA), and social identity providers". That sounds exactly what I was looking for, even though in our use case, we don't authenticate users but an application.
To get the access token that we need to authenticate against the MS Graph API, we need to create an ConfidentialClientApplication
and pass clientId
, clientSecret
(both information you get when creating an application in AAD) and optionally the authority
which is like the tenant you want to query:
const clientConfig = {
auth: {
clientId: 'your_client_id',
clientSecret: 'your_client_secret',
authority: 'your_authority',
}
};
const clientCredentialRequest = {
scopes: ["https://graph.microsoft.com/.default"],
};
const cca = new msal.ConfidentialClientApplication(clientConfig);
const response = await cca.acquireTokenByClientCredential(clientCredentialRequest);
console.log(response.accessToken);
How can we integrate this with the Microsoft Graph client library for JavaScript that we are using? Luckily, the SDK is pretty extensible. We have to create a class that implements the AuthenticationProvider
interface to return the needed access token to the MS Graph API client.
This is how we implemented the logic:
import {AuthenticationProvider, AuthenticationProviderOptions} from "@microsoft/microsoft-graph-client";
import * as msal from "@azure/msal-node";
export class ClientTokenAuthProvider implements AuthenticationProvider {
private readonly clientId: string;
private readonly clientSecret: string;
private readonly tenantId: string;
public constructor(clientId: string, clientSecret: string, tenantId: string) {
this.clientId = clientId;
this.clientSecret = clientSecret;
this.tenantId = tenantId;
}
/* eslint-disable @typescript-eslint/no-unused-vars */
public async getAccessToken(authenticationProviderOptions: AuthenticationProviderOptions | undefined): Promise<string> {
const clientConfig = {
auth: {
clientId: this.clientId,
clientSecret: this.clientSecret,
authority: 'https://login.microsoftonline.com/'+this.tenantId,
}
};
const clientCredentialRequest = {
scopes: ["https://graph.microsoft.com/.default"],
};
const cca = new msal.ConfidentialClientApplication(clientConfig);
const response = await cca.acquireTokenByClientCredential(clientCredentialRequest);
if (response === null) {
throw new Error('Not able to retrieve MS Graph API Authtoken!');
}
return Promise.resolve(response.accessToken);
}
}
We pass the clientId
, clientSecret
and the tenantId
when creating the class instance. The tenantId
variable is used to create the authority URL which will limit the scope of all queries to our own tenant, e.g. no external users are able to authenticate this way.
With the help of the ConfidentialClientApplication
class we query the authentication token from the Microsoft webservice and return it.
To make the MS Graph API client aware of the ClientTokenAuthProvider
, we need to create a ClientOptions
configuration and
configure the ClientTokenAuthProvider
instance:
import {ClientTokenAuthProvider} from "infrastructure/msgraph/clientTokenAuthProvider";
import {Client, ClientOptions} from "@microsoft/microsoft-graph-client";
const clientId = '...';
const clientSecret = '...';
const tenantId = '...';
const clientOptions: ClientOptions = {
authProvider: new ClientTokenAuthProvider(clientId, clientSecret, tenantId)
};
const client = Client.initWithMiddleware(clientOptions);
Now when querying the MS Graph API, the Javascript client will automatically authenticate itself and pass the authentication token back to the MS Graph API on each call:
const response = await client.api(`/users`).get();