Node.jsでMicrosoft Graph APIのアクセストークンをMicrosoft Entra ID(AzureAD)で作成したアプリケーションのクライアントシークレットを利用して取得する

タイトルが長くなってしまった…。
いきなり余談だが[Azure Active DirectoryがMicrosoft Entra IDに名称変更](https://www.microsoft.com/ja-jp/security/business/identity-access/microsoft-entra-id)したみたいだ…。

Node.jsでMicrosoft GraphのAPIを使う時に、まず前段としてアクセストークを取得する必要があるのだが、[公式のチュートリアル](https://learn.microsoft.com/en-us/azure/active-directory/develop/quickstart-console-app-nodejs-acquire-token)だと[msal-nodeモジュール](https://github.com/AzureAD/microsoft-authentication-library-for-js/tree/dev/lib/msal-node)を使う方法しか記載がなく、モジュールを使わずに取得する情報が見当たらなかったので書く。

# Node.jsでの取得方法
アクセストークンを取得するためにはAzureAD上で作成したアプリケーション（サービスプリンシパル）のクライアントID、テナントID、クライアントシークレットが必要になってくる。

そもそもアプリケーションを作成していない場合は、[ここ](https://learn.microsoft.com/ja-jp/graph/tutorials/javascript-app-only?view=graph-rest-1.0&tabs=aad&tutorial-step=1)を参考にアプリケーションを作成し、APIアクセス許可やクライアントシークレットの発行まで済ませる。

## 環境
```
$ node -v
v18.17.1

$ npm -v
10.0.0

$ npm list
|-- axios@1.5.1
```
httpリクエストには[Axiosモジュール](https://github.com/axios/axios)を使用。


## コード
ヘッダーのContent-Typeが、よくある`application/json`ではなくて、`application/x-www-form-urlencoded`なところが注意点。

```javascript
// モジュール読み込み
const axios = require('axios');

// AzureAD情報
const clientId = 'クライアントID';
const tenantId = 'テナントID';
const clientSecret = 'クライアントシークレット';

// トークンリクエストのAPIエンドポイント
const tokenEndpoint = `https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/token`;

// トークンリクエストボディ
const reqTokenBody = {
  client_id: clientId,
  scope: 'https://graph.microsoft.com/default',
  client_secret: clientSecret,
  grant_type: 'client_credentials'
};

// トークンリクエストのコンフィグ
const reqTokenConfig = {
  headers: {
    'Content-Type': 'application/x-www-form-urlencoded' // application/jsonではないので注意
  }
};

/* main */
(async () => {
  // アクセストークン取得
  const resToken = await axios.post(tokenEndpoint, reqTokenBody, reqTokenConfig);
  console.log(resToken.status); // レスポンスステータスコード
  const accessToken = resToken.data.access_token;
  console.log(accessToken); // アクセストークン
})();
```

## トークンの使い方
上記で取得したアクセストークンをコンフィグのヘッダーに乗せればOK。
```javascript
// 上述の処理と同じなので省略

/* main */
(async () => {
  // アクセストークン取得
  const resToken = await axios.post(tokenEndpoint, reqTokenBody, reqTokenConfig);
  const accessToken = resToken.data.access_token;

  // Get a user API
  const targetUser = 'john132564@hogehoge.com';
  const usersApi = `https://graph.microsoft.com/v1.0/users/${targetUser}`;
  const reqUsersConfig = {
    headers: {
      'Content-Type': 'application/json',       // ここはjson
      'Authorization': `Bearer ${accessToken}`  // ここに取得したアクセストークンを乗せる
    }
  };

  const resUser = await axios.get(usersApi, reqUsersConfig);
  console.log(reqUser.data);
})();


```
# そもそも困っていたところ（余談）
元々この記事を書こうとした経緯としては、`application/x-www-form-urlencoded`の時にaxiosにリクエストボディのオブジェクトを渡しても空扱いされてうまくいかなかったため、「URLエンコードすると解決するよ」という旨の記事を書こうとしたのだが、実際に記事を書こうとして検証してみると再現しなかった。

```javascript
const qs = require('qs');

// 以前はこうしないとpostした時にclient_id = nullのエラーになっていた
const reqTokenBodyStr = qs.stringify(reqTokenBody);
```
半年くらい前に実装していた時は上記のようにしないと動かなかったのだが、夢でも見ていたのだろうか…。

[AxiosのREADME](https://github.com/axios/axios#-automatic-serialization-to-urlsearchparams)にも「`application/x-www-form-urlencoded`に設定すればオブジェクトでもOK」的なことが書かれている…。
>🆕 Automatic serialization to URLSearchParams
Axios will automatically serialize the data object to urlencoded format if the content-type header is set to "application/x-www-form-urlencoded".
> ```javascript
> const data = {
>   x: 1,
>   arr: [1, 2, 3],
>   arr2: [1, [2], 3],
>   users: [{name: 'Peter', surname: 'Griffin'}, {name: 'Thomas', surname: 'Anderson'}],
> };
> 
> await axios.postForm('https://postman-echo.com/post', data,
>   {headers: {'content-type': 'application/x-www-form-urlencoded'}}
> );
> ```
この事象について何か知見のある方、よろしければコメントください。

# おわりに
以上の方法でMicrosoft Graph APIのアクセストークンが取得できる。
基本的には[msal-nodeモジュール](https://github.com/AzureAD/microsoft-authentication-library-for-js/tree/dev/lib/msal-node)を使うと思うが、そうでない時の参考にしていただければうれしい。

# 参考
- [Microsoft Graph API利用手順忘備録](https://zenn.dev/santafs/articles/ee6f7b4d8ea1fd)
…PowerShellでのクライアントシークレットを使ったアクセストークン取得方法
- [Microsoft identity platform and the OAuth 2.0 client credentials flow](https://learn.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-client-creds-grant-flow#get-a-token)
…公式ドキュメントのOAuthトークン取得方法（HTTPしかない…）
- [Axios - github](https://github.com/axios/axios)