0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Entra ID (旧Azure AD) の認証を理解する(Node.jsサーバー)

Posted at

はじめに

Node.jsサーバーにおいて認証に関するコードを理解する必要があるのですが、そもそもEntra IDが何かや、認証のコードに全然触れたことがなかったので、その学習軌跡をここにまとめ、後から見返せるように記事を書きます。
一番参考にさせていただいたのはこちらの記事です。

Entra IDとは

Entra ID とは、以前 Azure Active Directory (Azure AD) として知られていた Microsoft のクラウドベースのアクセス管理サービスです。Microsoft は 2023 年にこのサービスを「Entra ID」に改名しました。Entra ID は、企業や組織におけるユーザー認証やアクセス管理を行うための機能を提供します。

Entra ID の主な機能は以下の通りです:

  1. シングルサインオン (SSO):
    • ユーザーが一度のログインで複数のアプリケーションやサービスにアクセスできるようにします。

  2. 多要素認証 (MFA):
    • セキュリティ強化のために、パスワードだけでなく複数の認証方法(例:SMS 認証コード、認証アプリ)を使用して認証を行います。

  3. アクセス管理:
    • アプリケーションやリソースへのアクセス制御を柔軟に行うことができ、役割ベースのアクセス制御(RBAC)などで、権限を必要に応じて細かく設定可能です。

  4. アイデンティティ保護:
    • ユーザーのログイン行動を監視し、不審な動きを検知して自動的に対策を講じることができます(リスクベースの条件付きアクセスなど)。

  5. 条件付きアクセス:
    • ユーザーやデバイスの状況に応じて、特定のアクセス条件を課すことができ、組織のポリシーに基づいた安全なアクセスを実現します。

Microsoft Authorication library(MSAL)とは

Microsoft が提供する認証ライブラリで、Azure Active Directory (現在は Entra ID として知られています) への認証とトークン管理を簡単に実装するためのツールです。MSAL は、ユーザーやアプリケーションが Entra ID を利用して認証を行い、API やクラウドリソースへのアクセスを取得するのに役立ちます。

MSALの主な機能と特徴

  1. 認証とトークンの管理:
    • MSAL を利用することで、OAuth 2.0 や OpenID Connect に基づくトークンの取得やリフレッシュを簡単に行うことができます。ユーザーやアプリが Microsoft アカウント、Entra ID アカウントに対して認証できるようになります。

  2. 多様なプラットフォームに対応:
    • MSAL は複数のプラットフォーム(JavaScript、.NET、Java、Python、iOS、Android)向けに提供されており、デバイスやアプリケーションの種類に関わらず統一的な認証を行うことができます。

  3. シングルサインオン (SSO):
    • MSAL は SSO 機能をサポートしているため、ユーザーは一度の認証で複数のアプリケーションにシームレスにアクセスすることができます。

  4. 条件付きアクセスとセキュリティ強化:
    • 組織のセキュリティポリシーに基づいた条件付きアクセスや多要素認証を MSAL を通じて実装することが可能です。

  5. トークンキャッシュの管理:
    • MSAL は取得したアクセストークンやリフレッシュトークンをキャッシュし、効率的なアクセス管理をサポートします。キャッシュによってトークンの再取得を減らし、パフォーマンスを向上させます。

OpenIDConnect(OIDC)とは(IDトークン関連)

本人確認のための認証(Authentication)に利用されます。

こちらページに分かりやすい解説があります。

このプロセスでは、IDトークンが発行され、アプリケーションはこのトークンを使ってユーザーが誰であるかを確認します。IDトークンにはユーザーの基本情報(例えばユーザー名やメールアドレスなど)が含まれます。
OIDCのIDトークンは、JWT(JSON Web Token)形式で、ユーザーに関する情報(クレームと呼ばれる)や署名が含まれています。このIDトークンを検証することで、アプリケーションはユーザーのログイン状態を確認できます。

OAuth(Open Authorization) 2.0とは(アクセストークン関連)

使用できるサービスの範囲を決める認可(Authorization)に利用されます。
アプリケーションがユーザーのパスワードを共有することなく、他のサービスに対してユーザーの許可を得て特定のリソースにアクセスするための「認可プロトコル」です。OAuthを利用することで、たとえば、GoogleのAPIを使って他のアプリがユーザーのカレンダーや連絡先にアクセスすることができますが、「誰がログインしているか(認証)」についての詳細情報は提供しません。
たとえば、Microsoft Graph APIへのアクセス権を得るためにアクセストークンが使われます。

Microsoft Graphとは

Microsoftが提供するREST APIおよびデータサービスプラットフォームで、Microsoft 365(旧称Office 365)、Windows 10、Enterprise Mobility + SecurityなどのMicrosoftサービスに関連するデータや操作にアクセスするためのインターフェイスです。Microsoft Graphを使うことで、ユーザー、メール、カレンダー、ファイル、タスクなど、多様なデータや機能に一元的にアクセスでき、複数のサービスを横断するようなアプリケーションを開発するのが容易になります。
Microsoft Graphを使用するには、Microsoftの認証サービスであるEntra IDと連携する必要があり、OAuth 2.0やOpenID Connect (OIDC) を使って安全に認証・承認を行います。

JWT(JSON Web Token)とは

Entra IDとMSALとMicrosoft Graphの関係について

  1. 認証リクエストの処理: アプリケーションがMSALを使ってEntra IDに認証リクエストを送信します。
  2. トークン発行: Entra IDは認証が成功すると、アクセストークンやIDトークンを発行し、これをMSALを通してアプリケーションに返します。
  3. APIアクセス: MSALで取得したトークンを利用して、アプリケーションはMicrosoft GraphなどのAPIに安全にアクセスし、ユーザー情報や他のデータを取得できるようになります。

上記Qiitaの記事における、IDプロバイダがEntra IDで、アプリケーションAがMicrosoft Graphに対応しているのではないかと思います。

実際に作業をする

Quickstart: Sign in users in a single-page app (SPA) and call the Microsoft Graph API using JavaScript
まずは上記サイトに記載されているとおり、MSALを通してMicrosoft Graphにアクセスしてみたいと思います。

Azure accountを作成する

Azure accountを持っていないので作成します。無料で作成出来るようです。アカウントを作成することは出来ましたが、なぜかログイン出来ません。職場のアカウントも持っていることが関係しているのかもしれません。参考記事では職場の同僚にアカウントを作成してもらったとのことなので、私もその方法に切り替えます。
職場の同僚にお願いして、MicrosoftのActive Directory Management (ADM) アカウントを作成してもらいました。

上記サイトの手順通り進めていたところ、
Add a platform redirect URIの
2. On the Platform configurations page, select Add a platform, and then select SPA option.
のところで権限がなく、設定が出来ませんでした。
そのため、Redirect URIを「http://localhost:3000」と設定することが出来ず、うまく行きません。
そこで、職場の同僚に頼んで、権限付与をしてもらい、設定が出来るようにしました。

それでもうまく行かなかったので、chatGPTにヒントをもらい以下の設定をオンに変更しました。
Access tokens (used for implicit flows)

しかし、結局Microsoft Graph APIからユーザ情報を取得して表示するところまで辿り着けませんでした。

参考ページの最初からもう一度始めて、アプリケーションの登録からやりましたが、こちらも権限がないようで、アプリケーションの登録が出来ませんでした。

以下にコード解説がありますがあまり参考にならないので、ここは飛ばして「Nodejs express appsの構築」を見た方が良いかもしれません。

コードの解説

server.js
const express = require('express');
const morgan = require('morgan');
const path = require('path');

const DEFAULT_PORT = process.env.PORT || 3000;

// initialize express.
const app = express();

// Configure morgan module to log all requests.
app.use(morgan('dev'));

// serve public assets.
app.use(express.static('public'));

// serve msal-browser module
app.use(express.static(path.join(__dirname, "node_modules/@azure/msal-browser/lib")));

// set up a route for signout.html
app.get('/signout', (req, res) => {
    res.sendFile(path.join(__dirname + '/public/signout.html'));
});

// set up a route for redirect.html
app.get('/redirect', (req, res) => {
    res.sendFile(path.join(__dirname + '/public/redirect.html'));
});

// set up a route for index.html
app.get('/', (req, res) => {
    res.sendFile(path.join(__dirname + '/index.html'));
});

app.listen(DEFAULT_PORT, () => {
    console.log(`Sample app listening on port ${DEFAULT_PORT}!`);
});

module.exports = app;

• /(ルート)にアクセスすると、プロジェクトのルートディレクトリにあるindex.htmlを返します。
• /signout: public/signout.htmlを表示します。
• /redirect: public/redirect.htmlを表示します。

index.html
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no">
    <title>Microsoft identity platform</title>
    <link rel="SHORTCUT ICON" href="./favicon.svg" type="image/x-icon">
    <link rel="stylesheet" href="./styles.css">
    
    <!-- adding Bootstrap 5 for UI components  -->
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.2/dist/css/bootstrap.min.css" rel="stylesheet"
        integrity="sha384-Zenh87qX5JnK2Jl0vWa8Ck2rdkQ2Bzep5IDxbcnCeuOxjzrPF/et3URy9Bv1WTRi" crossorigin="anonymous">

    <!-- msal.min.js can be used in the place of msal-browser.js -->
    <script src="/msal-browser.min.js"></script>
</head>

<body>
    <nav class="navbar navbar-expand-sm navbar-dark bg-primary navbarStyle">
        <a class="navbar-brand" href="/">Microsoft identity platform</a>
        <div class="navbar-collapse justify-content-end">
            <button type="button" id="signIn" class="btn btn-secondary" onclick="signIn()">Sign-in</button>
            <button type="button" id="signOut" class="btn btn-success d-none" onclick="signOut()">Sign-out</button>
        </div>
    </nav>
    <br />
    <div class="container">
            <div class="row">
                <br />
                 <h5 id="title-div" class="card-header text-center">
                    Vanilla JavaScript single-page application secured with MSAL.js
                </h5>
                <br />
                <h5 id="welcome-div" class="card-header text-center d-none"></h5>
                 <table id="table-div" class="table table-striped table-bordered d-none" id="table">
                    <thead id="table-head-div">
                        <tr>
                            <th>Claim Type</th>
                            <th>Value</th>
                            <th>Description</th>
                        </tr>
                    </thead>
                    <tbody id="table-body-div"></tbody>
                </table>
            </div>
        </div>
    <!-- importing bootstrap.js and supporting js libraries -->
    <script src="https://code.jquery.com/jquery-3.3.1.slim.min.js"
        integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo" crossorigin="anonymous">
        </script>
    <script src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.11.6/dist/umd/popper.min.js"
        integrity="sha384-oBqDVmMz9ATKxIep9tiCxS/Z9fNfEXiDAYTujMAeBAsjFuCZSmKbSSUnQlmh/jp3"
        crossorigin="anonymous"></script>
    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.2/dist/js/bootstrap.bundle.min.js"
        integrity="sha384-OERcA2EqjJCMA+/3y+gxIOqMEjwtxJY7qPCqsdltbNJuaOe923+mo//f6V8Qbsw3"
        crossorigin="anonymous"></script>
        
    <!-- importing app scripts (load order is important) -->
    <script type="text/javascript" src="./authConfig.js"></script>
    <script type="text/javascript" src="./ui.js"></script>
    <script type="text/javascript" src="./claimUtils.js"></script>
    <!-- <script type="text/javascript" src="./authRedirect.js"></script> -->
    <!-- uncomment the above line and comment the line below if you would like to use the redirect flow -->
    <script type="text/javascript" src="./authRedirect.js"></script>
</body>

</html>

index.htmlは以下のようなサイトであり、右上のSign-inをクリックすることで、関数signIn()が実行されます。この関数は、authRedirect.jsで定義されています。

スクリーンショット 2024-11-22 14.26.04.png

authRedirect.js
// Create the main myMSALObj instance
// configuration parameters are located at authConfig.js
const myMSALObj = new msal.PublicClientApplication(msalConfig);

let username = "";

/**
 * A promise handler needs to be registered for handling the
 * response returned from redirect flow. For more information, visit:
 * https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-browser/docs/initialization.md#redirect-apis
 */
myMSALObj.handleRedirectPromise()
    .then(handleResponse)
    .catch((error) => {
        console.error(error);
    });

function selectAccount() {

    /**
     * See here for more info on account retrieval: 
     * https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-common/docs/Accounts.md
     */

    const currentAccounts = myMSALObj.getAllAccounts();

    if (!currentAccounts) {
        return;
    } else if (currentAccounts.length > 1) {
        // Add your account choosing logic here
        console.warn("Multiple accounts detected.");
    } else if (currentAccounts.length === 1) {
        username = currentAccounts[0].username
        welcomeUser(currentAccounts[0].username);
        updateTable(currentAccounts[0]);
    }
}

function handleResponse(response) {

    /**
     * To see the full list of response object properties, visit:
     * https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-browser/docs/request-response-object.md#response
     */

    if (response !== null) {
        username = response.account.username
        welcomeUser(username);
        updateTable(response.account);
    } else {
        selectAccount();

        /**
         * If you already have a session that exists with the authentication server, you can use the ssoSilent() API
         * to make request for tokens without interaction, by providing a "login_hint" property. To try this, comment the 
         * line above and uncomment the section below.
         */

        // myMSALObj.ssoSilent(silentRequest).
        //     then((response) => {
        //         welcomeUser(response.account.username);
        //         updateTable(response.account);
        //     }).catch(error => {
        //         console.error("Silent Error: " + error);
        //         if (error instanceof msal.InteractionRequiredAuthError) {
        //             signIn();
        //         }
        //     });
    }
}

function signIn() {

    /**
     * You can pass a custom request object below. This will override the initial configuration. For more information, visit:
     * https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-browser/docs/request-response-object.md#request
     */

    myMSALObj.loginRedirect(loginRequest);
}

function signOut() {

    /**
     * You can pass a custom request object below. This will override the initial configuration. For more information, visit:
     * https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-browser/docs/request-response-object.md#request
     */

    // Choose which account to logout from by passing a username.
    const logoutRequest = {
        account: myMSALObj.getAccountByUsername(username),
        postLogoutRedirectUri: '/signout', // remove this line if you would like navigate to index page after logout.

    };

    myMSALObj.logoutRedirect(logoutRequest);
}
authRedirect.js
const myMSALObj = new msal.PublicClientApplication(msalConfig);

msalConfigは、authConfig.jsで定義されたアプリケーションの認証に必要な設定を含むオブジェクトです。
認証フローを管理するためのメインオブジェクト myMSALObj を作成しています。

authRedirect.js
myMSALObj.handleRedirectPromise()
    .then(handleResponse)
    .catch((error) => {
        console.error(error);
    });

リダイレクトフローでは、ユーザーが認証ページでサインインした後にアプリケーションに戻るため、このメソッドで戻ってきた情報を受け取る必要があります。
リダイレクトフローが成功した場合、handleResponse 関数が呼び出されます。

authRedirect.js
function selectAccount() {
    const currentAccounts = myMSALObj.getAllAccounts();

    if (!currentAccounts) {
        return;
    } else if (currentAccounts.length > 1) {
        console.warn("Multiple accounts detected.");
    } else if (currentAccounts.length === 1) {
        username = currentAccounts[0].username
        welcomeUser(currentAccounts[0].username);
        updateTable(currentAccounts[0]);
    }
}

ユーザーのアカウント情報を取得してアプリケーション内で適切に処理するための関数 selectAccount を定義しています。MSAL.js を使用して認証を管理するアプリケーションで、ログイン済みのアカウントを選択する際に使われます。

authRedirect.js
function handleResponse(response) {

    if (response !== null) {
        username = response.account.username
        welcomeUser(username);
        updateTable(response.account);
    } else {
        selectAccount();
    }
}

認証フローから返されたレスポンスを処理する関数 handleResponse を定義しています。
認証済みのユーザーの情報をアプリケーション内で利用可能にします。
welcomeUser("user@example.com") を呼び出して「ようこそ、user@example.com」というメッセージを表示。
updateTable() を呼び出してアカウント情報をUIに表示。

authRedirect.js
function signIn() {
    myMSALObj.loginRedirect(loginRequest);
}

ユーザーのブラウザが AAD のログインページにリダイレクトされ、認証が完了すると指定したリダイレクト URI に戻ります。
loginRequestは、authConfig.jsにあるログイン要求の設定を定義するオブジェクトです。

整理

今までのコードを見て挙動を整理します。start npm(node server.js)としてサーバを起動して、http://localhotst:3000にアクセスします。
Sign inボタンをクリックすることで、authRedirect.jsの関数signIn()が実行され、Azure Active Directory のログインページにリダイレクトされ、認証が完了すると指定したリダイレクト URI に戻ります。

Nodejs express appsの構築

結局、上記のやり方では出来なかったので、以下のURLに記載されているNodejs express appsの構築をやってみることにします。

Entra IDのアカウントなどはすでにあるので作成しなくても良いですが、以下の項目の作業は行いました。
5. Select Certificates & secrets under Manage. Select the New client secret button. Enter a value in Description and select one of the options for Expires and choose Add.
つまり、Certificates & secretsに新しいclient secretsを作成しました。

次にexample.env ファイルの名前を .env に変更し、OAUTH_CLIENT_IDとOAUTH_CLIENT_SECRETの値を設定します。OAUTH_CLIENT_SECRETは先程作成した、Certificates & secretsのValue値を記載します。

手順に沿って進めていましたが、認証のコールバックでエラーが発生し、認証に失敗しました。
chatGPTに聞いたところ原因は以下のようです。
• アプリケーションが Azure AD の認証エンドポイント /common を使用していますが、/common は マルチテナントアプリケーション 用です。
• このアプリケーションはシングルテナント (特定の Azure テナント専用) として構成されています。

そこで、.envで以下のように設定されているところを、修正します。
OAUTH_AUTHORITY=https://login.microsoftonline.com/common/

具体的には、以下のURLに設定します。

https://login.microsoftonline.com/<your-tenant-id>

再度試してみたところ、先ほど出ていたエラーメッセージは出なくなりましたが、以下の通りNeed admin approvalと表示されうまくいきませんでした。

スクリーンショット 2024-11-25 15.29.34.png

よく分かりませんが、管理者の承認が必要なようです。

Entra IDのMEAPQCアプリのAPI permissionsにおいて、以下のスコープに管理者の承認が必要ということかもしれません。OAUTH_SCOPES='user.read,calendars.readwrite,mailboxsettings.read'

試しに管理者の承認を必要としないuser.readだけを指定(以下のように設定)して試してみます。
OAUTH_SCOPES='user.read'

設定を変えて実行してみたところ上記のNeed admin approvalというメッセージは出ずに、以下のURLにリダイレクトされました。
http://localhost:3000/

まだ、どこかにエラーがあるようです。

色々と調べた結果、routes/auth.jsの"/callback"の部分でメールアドレスがうまく取得出来ていないようでした。同僚に作成してもらったアカウントはメールアドレスなどを持たないことが原因かもしれません。
具体的には、以下のコードで、graph.getUserDetails関数が返すデータを取得する際にエラーが発生しているようです。
const user = await graph.getUserDetails(
req.app.locals.msalClient,
req.session.userId
);

graph.jsの.select('displayName,mail,mailboxSettings,userPrincipalName')
の部分を.select("displayName")ように変更しました。
さらに、auth.jsの以下の部分をコメントアウトするとエラーが消え、無事にsign in出来ました。
email: user.mail || user.userPrincipalName,
timeZone: user.mailboxSettings.timeZone,

スクリーンショット 2024-11-29 16.19.17.png

コードの解説

bin/www

bin/www
#!/usr/bin/env node

/**
 * Module dependencies.
 */

var app = require('../app');
var debug = require('debug')('graph-sample:server');
var http = require('http');

/**
 * Get port from environment and store in Express.
 */

var port = normalizePort(process.env.PORT || '3000');
app.set('port', port);

/**
 * Create HTTP server.
 */

var server = http.createServer(app);

/**
 * Listen on provided port, on all network interfaces.
 */

server.listen(port);
server.on('error', onError);
server.on('listening', onListening);

/**
 * Normalize a port into a number, string, or false.
 */

function normalizePort(val) {
  var port = parseInt(val, 10);

  if (isNaN(port)) {
    // named pipe
    return val;
  }

  if (port >= 0) {
    // port number
    return port;
  }

  return false;
}

/**
 * Event listener for HTTP server "error" event.
 */

function onError(error) {
  if (error.syscall !== 'listen') {
    throw error;
  }

  var bind = typeof port === 'string'
    ? 'Pipe ' + port
    : 'Port ' + port;

  // handle specific listen errors with friendly messages
  switch (error.code) {
    case 'EACCES':
      console.error(bind + ' requires elevated privileges');
      process.exit(1);
      break;
    case 'EADDRINUSE':
      console.error(bind + ' is already in use');
      process.exit(1);
      break;
    default:
      throw error;
  }
}

/**
 * Event listener for HTTP server "listening" event.
 */

function onListening() {
  var addr = server.address();
  var bind = typeof addr === 'string'
    ? 'pipe ' + addr
    : 'port ' + addr.port;
  debug('Listening on ' + bind);
}

package.jsonの中で、"start": "node ./bin/www"と記載されており、npm startコマンドで "node ./bin/www"が実行されます。Node.js アプリケーションでは、www は一般的にサーバーのエントリーポイント(アプリの起動スクリプト)として利用されます。
ファイル名に拡張子は必須ではなく、www のような名前が慣例的に使われる場合があります。
www の役割は、Express アプリケーションのデフォルト構成では、サーバーの設定や起動処理を定義するファイルです。

www
#!/usr/bin/env node

この行はスクリプトを Node.js で実行するよう指定しています。

www
var app = require('../app');
var debug = require('debug')('graph-sample:server');
var http = require('http');

•require('../app')
アプリケーションの主要なロジックが記述されている app モジュールをインポートします。

•debug
デバッグ用ツールで、アプリケーションのログを見やすくするために使用されます。

•http
Node.js の標準モジュールで、HTTP サーバーを作成します。

www
var port = normalizePort(process.env.PORT || '3000');
app.set('port', port);

•process.env.PORT:
環境変数 PORT に設定されているポート番号を取得します。
環境変数が設定されていない場合、デフォルトでポート 3000 を使用します。

•normalizePort 関数
ポート番号を数値、文字列、または無効な値の場合は false に変換します。

•app.set('port', port)
アプリケーション全体でポート番号を使用できるように、Express の設定にポートを保存します。

www
var server = http.createServer(app);

アプリケーション (app) をリクエストハンドラーとして設定した HTTP サーバーを作成します。

www
server.listen(port);
server.on('error', onError);
server.on('listening', onListening);

•server.listen(port):
指定したポートでサーバーを起動します。

•server.on('error', onError):
エラー発生時に onError 関数を実行します。

•server.on('listening', onListening):
サーバーが正常に起動し、待機状態になったときに onListening 関数を実行します。

www
function normalizePort(val) {
  var port = parseInt(val, 10);
  if (isNaN(port)) return val; // ポートが名前付きパイプの場合
  if (port >= 0) return port;  // 有効なポート番号
  return false; // 無効なポート
}

与えられたポート番号を適切な形式に変換します。
文字列や無効な値の場合も適切に処理します。

www
function onError(error) {
  if (error.syscall !== 'listen') throw error;

  var bind = typeof port === 'string'
    ? 'Pipe ' + port
    : 'Port ' + port;

  switch (error.code) {
    case 'EACCES':
      console.error(bind + ' requires elevated privileges');
      process.exit(1);
      break;
    case 'EADDRINUSE':
      console.error(bind + ' is already in use');
      process.exit(1);
      break;
    default:
      throw error;
  }
}

• サーバー起動時のエラーを捕捉し、適切に処理します。
EACCES: 権限不足。
EADDRINUSE: ポートが既に使用中。

www
function onListening() {
  var addr = server.address();
  var bind = typeof addr === 'string'
    ? 'pipe ' + addr
    : 'port ' + addr.port;
  debug('Listening on ' + bind);
}

サーバーが正常に起動したことをログに記録します。
デバッグモジュールを使って起動中のポートやパイプを出力します。

app.js

app.js
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

const createError = require('http-errors');
const express = require('express');
const path = require('path');
const cookieParser = require('cookie-parser');
const logger = require('morgan');
require('dotenv').config();

const indexRouter = require('./routes/index');
const usersRouter = require('./routes/users');

const session = require('express-session');
const flash = require('connect-flash');
const msal = require('@azure/msal-node');

const authRouter = require('./routes/auth');
const calendarRouter = require('./routes/calendar');
var app = express();
// <MsalInitSnippet>
// In-memory storage of logged-in users
// For demo purposes only, production apps should store
// this in a reliable storage
app.locals.users = {};

// MSAL config
const msalConfig = {
  auth: {
    clientId: process.env.OAUTH_CLIENT_ID || '',
    authority: process.env.OAUTH_AUTHORITY,
    clientSecret: process.env.OAUTH_CLIENT_SECRET
  },
  system: {
    loggerOptions: {
      loggerCallback(loglevel, message, containsPii) {
        if (!containsPii) console.log(message);
      },
      piiLoggingEnabled: false,
      logLevel: msal.LogLevel.Verbose,
    }
  }
};

// Create msal application object
app.locals.msalClient = new msal.ConfidentialClientApplication(msalConfig);
// </MsalInitSnippet>
// <SessionSnippet>
// Session middleware
// NOTE: Uses default in-memory session store, which is not
// suitable for production
app.use(session({
  secret: 'your_secret_value_here',
  resave: false,
  saveUninitialized: false,
  unset: 'destroy'
}));

// Flash middleware
app.use(flash());

// Set up local vars for template layout
app.use(function(req, res, next) {
  // Read any flashed errors and save
  // in the response locals
  res.locals.error = req.flash('error_msg');

  // Check for simple error string and
  // convert to layout's expected format
  var errs = req.flash('error');
  for (var i in errs){
    res.locals.error.push({message: 'An error occurred', debug: errs[i]});
  }

  // Check for an authenticated user and load
  // into response locals
  if (req.session.userId) {
    res.locals.user = app.locals.users[req.session.userId];
  }

  next();
});
// </SessionSnippet>

// view engine setup
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'hbs');

// <FormatDateSnippet>
var hbs = require('hbs');
var dateFns = require('date-fns');
// Helper to format date/time sent by Graph
hbs.registerHelper('eventDateTime', function(dateTime) {
  const date = dateFns.parseISO(dateTime);
  return dateFns.format(date, 'M/d/yy h:mm a');
});
// </FormatDateSnippet>

app.use(logger('dev'));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public')));

app.use('/', indexRouter);
app.use('/auth', authRouter);
app.use('/calendar', calendarRouter);
app.use('/users', usersRouter);

// catch 404 and forward to error handler
app.use(function(req, res, next) {
  next(createError(404));
});

// error handler
app.use(function(err, req, res) {
  // set locals, only providing error in development
  res.locals.message = err.message;
  res.locals.error = req.app.get('env') === 'development' ? err : {};

  // render the error page
  res.status(err.status || 500);
  res.render('error');
});

module.exports = app;
app.js
const createError = require("http-errors");

概要: HTTP エラーを作成するためのモジュール。
用途: 404 や 500 エラーを簡単に作成し、エラー処理ミドルウェアに渡すために使用します。

app.js
const path = require("path");

OS に依存しない方法でファイルパスを解決。
Express.js の views や static ファイルのパスを設定する際に役立ちます。

app.js
const cookieParser = require("cookie-parser");

概要: クッキーを解析するミドルウェアです。
用途:
•クッキーの内容を簡単に読み取れるようにします。
•認証やセッション管理でクッキーを利用する際に便利です。

app.js
require("dotenv").config();

概要: .env ファイルに記載された環境変数を Node.js アプリに読み込むモジュール。

app.js
const session = require("express-session");

概要: ユーザーセッションを管理するミドルウェア。
用途:
•ユーザーのログイン情報やアクティビティをセッションとして一時的に保存。
•注意: 本番環境では、デフォルトのインメモリストアは適していないため、Redis などの永続的なストアが推奨されます。

app.js
const flash = require("connect-flash");

概要: 一時的なメッセージをセッション経由で保存し、次のリクエストで使用するミドルウェア。
用途: 認証エラーや成功メッセージをユーザーに伝える。

app.js
const msal = require("@azure/msal-node");

概要: Microsoft の OAuth 認証ライブラリ。
用途: Azure AD を利用したシングルサインオン (SSO) やトークン管理を行う。

app.js
app.locals.users = {};

app.locals は、Express アプリケーション内でグローバルなコンテキストを提供するオブジェクトです。このオブジェクトに格納されたプロパティや値は、アプリケーション内のすべてのルートやミドルウェアでアクセスできます。テンプレートエンジン(例えば hbs)でも自動的に利用可能になります。

app.js
// MSAL config
const msalConfig = {
  auth: {
    clientId: process.env.OAUTH_CLIENT_ID || '',
    authority: process.env.OAUTH_AUTHORITY,
    clientSecret: process.env.OAUTH_CLIENT_SECRET
  },
  system: {
    loggerOptions: {
      loggerCallback(loglevel, message, containsPii) {
        if (!containsPii) console.log(message);
      },
      piiLoggingEnabled: false,
      logLevel: msal.LogLevel.Verbose,
    }
  }
};

auth セクション: アプリケーションの認証に必要な情報を Azure AD に提供。

•clientId:
アプリケーションのクライアント ID を指定。
Azure ポータルで登録したアプリケーションに割り当てられる一意の識別子。
環境変数 OAUTH_CLIENT_ID から読み込むか、指定されていない場合は空文字列に。

•authority:
Azure AD の認証エンドポイントを指定。

•clientSecret:
アプリケーションが安全に認証されるためのクライアントシークレット。

system セクション: デバッグ用のログ設定をカスタマイズし、アプリの動作を追跡可能に。

•loggerOptions: ログ記録の設定。
loggerCallback:
ログの出力をカスタマイズする関数。
ここでは、個人識別情報 (PII: Personally Identifiable Information) を含まないメッセージをログに出力するように設定。

•piiLoggingEnabled:
デフォルトは false。
個人識別情報をログに記録しない設定。

•logLevel:
ログの詳細度を設定。
ここでは、すべてのログを記録する Verbose を使用(開発・デバッグ時に推奨)。

app.js
// Create msal application object
app.locals.msalClient = new msal.ConfidentialClientApplication(msalConfig);

. app.locals
概要: Express フレームワークで提供されるオブジェクト。アプリケーション全体で共有されるデータを保存するために使われます。
用途: app.locals に保存したデータは、アプリケーション内のすべてのルートやミドルウェアでアクセスできます。

・msal.ConfidentialClientApplication
概要: Microsoft Authentication Library (MSAL) におけるクラスで、認証を行うクライアントを表します。
ConfidentialClientApplication: サーバーサイドアプリケーション用で、秘密鍵(クライアントシークレットや証明書)を使って認証します。
これに対して、PublicClientApplication はクライアントサイドアプリケーション(ネイティブアプリやSPA)向けです。
機能: このクラスのインスタンスを使用して、以下の操作を実行します。
•Azure Active Directory (Azure AD) を介したユーザー認証
•トークンの取得(アクセストークン、リフレッシュトークンなど)
•セッション管理

app.js
app.use(session({
  secret: 'your_secret_value_here',
  resave: false,
  saveUninitialized: false,
  unset: 'destroy'
}));

const session = require('express-session'); で定義した session は、そのファイル内でのみ有効です。しかし、上記のようにミドルウェアとして登録することで、すべてのルートハンドラーで req.session にアクセスできるようになります。

session
概要: セッションを管理するためのミドルウェアを提供するライブラリ。
ユーザーごとの状態をサーバー側で一時的に保存する仕組み。
通常、セッション ID はクッキーに保存され、セッションデータ自体はサーバーに保存されます。

secret
概要: セッション ID の署名を生成するためのキー(文字列)。
※.env ファイルに定義されている OAUTH_CLIENT_SECRET は通常、OAuth 2.0 のクライアントシークレットであり、セッション管理の secret とは別物です。

resave
概要: セッションデータに変更がなくても、セッションを強制的に再保存するかどうか。

saveUninitialized
概要: 初期化されていないセッション(新規作成されたがデータがない)を保存するかどうか。

unset
概要: セッションを削除する方法を指定。
値:'destroy': セッションデータを完全に削除します。

JWTはサーバ側でユーザの情報を持たないですが、この方法だとサーバ側で情報を保持します。この時点では、MSALとEntra IDとのやり取りなので、サーバ側で情報を保持していると思われます。Graphを使用する際には、トークンユーザ情報が保存され、サーバには保存されないと思います。認証についてまだ理解が不完全なので、また何か分かれば追記します。

app.js
// Flash middleware
app.use(flash());

フラッシュメッセージは、リクエスト間で一時的にデータを保持するメカニズムです。
主に以下の用途で使用されます:
•ユーザーがフォームを送信した後に成功やエラーの通知を表示する。
•ログイン処理後に特定の通知を表示する。
•メッセージは 1 度読み取られると削除されるため、次のリクエストでは表示されません。

app.js
// Set up local vars for template layout
app.use(function(req, res, next) {
  // Read any flashed errors and save
  // in the response locals
  res.locals.error = req.flash('error_msg');

  // Check for simple error string and
  // convert to layout's expected format
  var errs = req.flash('error');
  for (var i in errs){
    res.locals.error.push({message: 'An error occurred', debug: errs[i]});
  }

  // Check for an authenticated user and load
  // into response locals
  if (req.session.userId) {
    res.locals.user = app.locals.users[req.session.userId];
  }

  next();
});

このミドルウェアは、Express アプリケーションにおいて、リクエストごとにレスポンスオブジェクトにローカル変数をセットアップするミドルウェアの設定です。これにより、テンプレート(ビュー、つまりindex.hbsなど)内でエラーや認証情報などを簡単に利用できるようになります。

以下の情報をテンプレートに渡すために使用されます

  1. エラーメッセージ(res.locals.error)
    ユーザーにエラーや警告を表示するためのフラッシュメッセージを準備します。

  2. ユーザー情報(res.locals.user)
    認証されているユーザーの情報をテンプレートに渡すことで、認証済みのユーザーに個別の情報を表示できます(例えば、ユーザー名やメールアドレスなど)。

req.flash('error_msg') は、フラッシュメッセージで保存されたエラーメッセージを取得します。
取得したメッセージを res.locals.error に格納します。res.locals はテンプレートエンジンに渡すローカル変数です。

次に、req.flash('error') からエラーを取得し、res.locals.error に追加します。つまり、上記でres.locals.errorに格納したデータにさらに追加しているということだと思います。
各エラーメッセージに対して、{message: 'An error occurred', debug: errs[i]} という形式でオブジェクトを作成し、これを res.locals.error にプッシュします。これにより、テンプレートでエラーメッセージを詳細に表示できます。

※フラッシュメッセージは、req.flash('キー', 'メッセージ') の形式で設定し、req.flash('キー') で取得します。routes/auth.jsで定義されています。res.redirect("/");のコードでリダイレクトされ、その際に、取得されたエラーメッセージがres.locals.error に格納されます。

req.session.userId が存在する場合、つまりユーザーが認証されている場合、そのユーザーの情報をレスポンスのローカル変数 res.locals.user に格納します。

app.js
// view engine setup
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'hbs');

app.set('views', path.join(__dirname, 'views'));
この行で、ビューが格納されているディレクトリを設定します。views フォルダがテンプレートファイルの場所となります。

app.set('view engine', 'hbs');
この行は、ビューエンジンとして hbs を指定しています。
'hbs' は、Handlebars テンプレートエンジンを意味します。hbsは、HTMLを動的に生成するためのテンプレートエンジンで、条件分岐やループなどをテンプレート内で使うことができます。

app.js
var hbs = require('hbs');

hbs は、Node.js 用の Handlebars テンプレートエンジンのライブラリです。Handlebarsは、動的なHTMLコンテンツを生成するために使用されるテンプレートエンジンで、特に条件分岐、ループ、部分テンプレートの機能などが使いやすいという特徴があります。

app.js
var dateFns = require('date-fns');

date-fns は、日付操作に関する様々な関数を提供する人気のあるライブラリです。JavaScriptの標準の日付オブジェクト (Date) と組み合わせて、日付や時間の計算、フォーマット、解析などを簡単に行うことができます。

app.js
// Helper to format date/time sent by Graph
hbs.registerHelper('eventDateTime', function(dateTime) {
  const date = dateFns.parseISO(dateTime);
  return dateFns.format(date, 'M/d/yy h:mm a');
});

hbs.registerHelper は、Handlebars でカスタムヘルパーを登録するメソッドです。ヘルパーとは、テンプレート内で利用できる独自の関数やロジックを定義するものです。ここでは、eventDateTime という名前のヘルパーを定義しています。
このヘルパーは、dateTime という引数を受け取り、日付/時刻のフォーマットを行う処理を行います。

dateTime 引数は、ISO 8601 形式の文字列(例: "2024-11-26T12:30:00Z")で渡されると仮定しています。
dateFns.parseISO は、ISO 8601 形式の文字列を Date オブジェクトに変換するための関数です。これにより、日付文字列を JavaScript の Date オブジェクトに変換し、日付/時刻を操作できるようにします。

dateFns.format は、Date オブジェクトを指定したフォーマットの文字列に変換する関数です。
例えば、dateTime が "2024-11-26T12:30:00Z" という値の場合、eventDateTime ヘルパーはこれを "11/26/24 12:30 pm" という形式でフォーマットします。

app.js
app.use(logger('dev'));

'dev' は、morgan におけるログフォーマットのひとつです。このフォーマットは開発環境向けに最適化されており、リクエストのメソッド、URL、レスポンスのステータスコード、処理時間などを短い形式で表示します。

ex. 以下の200はレスポンスコード(成功)です。
GET /home 200 15ms

app.js
app.use(express.json());

JSON ボディパーサー ミドルウェアを使用するための設定です。このミドルウェアは、HTTP リクエストのボディ部分に含まれる JSON データを解析し、req.body にその内容を自動的に格納します。

例えば、POST リクエストで送られてきた JSON データが次のようなものであれば:

{
  "name": "John Doe",
  "email": "john.doe@example.com"
}

req.body にアクセスすることで、このオブジェクトを簡単に取得できるようになります:

console.log(req.body.name); // "John Doe"
console.log(req.body.email); // "john.doe@example.com"
app.js
app.use(express.urlencoded({ extended: false }));

フォーム送信などで送られる URL エンコードされたデータを解析して req.body に格納するためのミドルウェア設定です。

express.urlencoded() は、URL エンコードされたデータ(主にフォームデータ)を解析するためのミドルウェアです。データが application/x-www-form-urlencoded という形式で送信されるときに使います。この形式は、フォームのデータをキーと値のペアとして送信する方法です。
例えば、フォームで次のようなデータを送信する場合:

<form method="POST" action="/submit">
  <input type="text" name="username" value="JohnDoe">
  <input type="password" name="password" value="1234">
  <button type="submit">Submit</button>
</form>

送信されるデータは次のようにURLエンコードされます:
username=JohnDoe&password=1234

app.js
app.use(cookieParser());

クッキー を解析するためのミドルウェアです。このミドルウェアを使用すると、リクエストに含まれるクッキーを簡単に取り出すことができ、req.cookies オブジェクトにクッキーの値を格納します。

例えば、クライアントが送信するクッキーが次のようであった場合:
userId=12345; sessionToken=abcdefg

cookie-parser を使うと、req.cookies は次のようにアクセスできます:

req.cookies = {
  userId: '12345',
  sessionToken: 'abcdefg'
};
app.js
app.use(express.static(path.join(__dirname, 'public')));

静的ファイル を提供するためのミドルウェアです。この行は、アプリケーションがサーバー上にある public ディレクトリ内のファイルを静的コンテンツとしてクライアントに配信することを設定します。

app.js
app.use('/', indexRouter);
app.use('/auth', authRouter);
app.use('/calendar', calendarRouter);
app.use('/users', usersRouter);

ルーティングの設定を行うための関数です。ここでのコードは、各パスに対応するルーター(indexRouter、authRouter、calendarRouter、usersRouter)を指定して、それぞれの URL パスに対するリクエストを処理するように設定しています。

app.js
// catch 404 and forward to error handler
app.use(function(req, res, next) {
  next(createError(404));
});

リクエストされた URL がどのルートにも一致しなかった場合に、404 エラーを生成し、次のエラーハンドラーに渡す処理をしています。

app.js
// error handler
app.use(function(err, req, res) {
  // set locals, only providing error in development
  res.locals.message = err.message;
  res.locals.error = req.app.get('env') === 'development' ? err : {};

  // render the error page
  res.status(err.status || 500);
  res.render('error');
});

エラーハンドリングミドルウェアは、通常のミドルウェアと少し異なり、err(エラーオブジェクト)が最初の引数として渡されます。
アプリケーション内で発生したエラーを適切にキャッチし、開発環境では詳細なエラー情報を、本番環境では一般的なエラーメッセージを表示するための仕組みを提供します。

routes/index.js

routes/index.js
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

// <IndexRouterSnippet>
var express = require('express');
var router = express.Router();

/* GET home page. */
router.get('/', function(req, res) {
  let params = {
    active: { home: true }
  };

  res.render('index', params);
});

module.exports = router;
// </IndexRouterSnippet>

let params = { active: { home: true } };
テンプレートエンジンに渡すパラメータを定義しています。
この情報は、ビュー(例えば、index.hbs)に渡され、ナビゲーションメニューの「ホーム」がアクティブ状態であることを表すために使われます???
→結局、index.hbsで使用されていない模様です。

active.home を true に設定して、ビュー内で現在のページがトップページであることを示す役割を持ちます。

res.render():
•指定されたテンプレート(この場合、index)をレンダリングします。
•第 2 引数(params)は、テンプレートで利用可能なデータとして渡されます。

※routes/index.js 内で params に user を明示的に代入するコードはありませんが、res.render を使った際にテンプレートエンジンは res.locals の内容を自動的に利用します。

views/index.hbs

views/index.hbs
<!-- Copyright (c) Microsoft Corporation.
     Licensed under the MIT License. -->

<!-- <IndexSnippet> -->
<div class="p-5 mb-4 bg-light rounded-3">
  <div class="container-fluid py-5">
    <h1 class="display-5 fw-bold">Node.js Graph Sample</h1>
    <p class="lead">This sample app shows how to use the Microsoft Graph API to access a user's data from Node.js</p>
    {{#if user}}
      <h4>Welcome {{ user.displayName }}!</h4>
      <p class="col-md-8 fs-6">Use the navigation bar at the top of the page to get started.</p>
    {{else}}
      <a href="/auth/signin" class="btn btn-primary btn-large">Click here to sign in</a>
    {{/if}}
  </div>
</div>
<!-- </IndexSnippet> -->

Microsoft Graph サンプルアプリケーションのトップページ(ホームページ)として表示される HTML テンプレートを示しています。hbs(Handlebars)テンプレートエンジンを使用して、ユーザーの状態に応じた動的なコンテンツを提供します。

{{#if user}}:
user が存在する場合(サインイン済みの状態)、user.displayName(サインインしたユーザーの名前)を表示します。
※{{user}} はテンプレートエンジンにより、res.locals.user の値を使用します。res.locals.userはユーザ認証を行う前はデータがありませんが、ユーザ認証後には取得されており、そのためリダイレクト後に本ページを表示させる際に読み込まれます。

{{else}}:
user が存在しない場合(未サインインの状態)、サインインページ(/auth/signin)へのリンクを表示します。

layout.hbsについての記載がありませんが、こちらも適用されています。これは、デフォルトで適用されるようです。

routes/auth.js

routes/auth.js
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

const graph = require('../graph');
const router = require('express-promise-router').default();

/* GET auth callback. */
router.get('/signin',
  async function (req, res) {
    const scopes = process.env.OAUTH_SCOPES || 'https://graph.microsoft.com/.default';
    const urlParameters = {
      scopes: scopes.split(','),
      redirectUri: process.env.OAUTH_REDIRECT_URI
    };

    try {
      const authUrl = await req.app.locals
        .msalClient.getAuthCodeUrl(urlParameters);
      res.redirect(authUrl);
    }
    catch (error) {
      console.log(`Error: ${error}`);
      req.flash('error_msg', {
        message: 'Error getting auth URL',
        debug: JSON.stringify(error, Object.getOwnPropertyNames(error))
      });
      res.redirect('/');
    }
  }
);

// <CallbackSnippet>
router.get('/callback',
  async function(req, res) {
    const scopes = process.env.OAUTH_SCOPES || 'https://graph.microsoft.com/.default';
    const tokenRequest = {
      code: req.query.code,
      scopes: scopes.split(','),
      redirectUri: process.env.OAUTH_REDIRECT_URI
    };

    try {
      const response = await req.app.locals
        .msalClient.acquireTokenByCode(tokenRequest);

      // Save the user's homeAccountId in their session
      req.session.userId = response.account.homeAccountId;

      const user = await graph.getUserDetails(
        req.app.locals.msalClient,
        req.session.userId
      );

      // Add the user to user storage
      req.app.locals.users[req.session.userId] = {
        displayName: user.displayName,
      // email: user.mail || user.userPrincipalName,
      // timeZone: user.mailboxSettings.timeZone,
      };
    } catch(error) {
      req.flash('error_msg', {
        message: 'Error completing authentication',
        debug: JSON.stringify(error, Object.getOwnPropertyNames(error))
      });
    }

    res.redirect('/');
  }
);
// </CallbackSnippet>

router.get('/signout',
  async function(req, res) {
    // Sign out
    if (req.session.userId) {
      // Look up the user's account in the cache
      const accounts = await req.app.locals.msalClient
        .getTokenCache()
        .getAllAccounts();

      const userAccount = accounts.find(a => a.homeAccountId === req.session.userId);

      // Remove the account
      if (userAccount) {
        req.app.locals.msalClient
          .getTokenCache()
          .removeAccount(userAccount);
      }
    }

    // Destroy the user's session
    req.session.destroy(function () {
      res.redirect('/');
    });
  }
);

module.exports = router;
routes/auth.js
const graph = require('../graph');
const router = require('express-promise-router').default();

•graph: Microsoft Graph API とのやり取りを行うためのカスタムモジュール。
•express-promise-router: 非同期関数を使用したルーティングを簡略化するためのラッパー。

routes/auth.js
/* GET auth callback. */
router.get('/signin',
  async function (req, res) {
    const scopes = process.env.OAUTH_SCOPES || 'https://graph.microsoft.com/.default';
    const urlParameters = {
      scopes: scopes.split(','),
      redirectUri: process.env.OAUTH_REDIRECT_URI
    };

    try {
      const authUrl = await req.app.locals
        .msalClient.getAuthCodeUrl(urlParameters);
      res.redirect(authUrl);
    }
    catch (error) {
      console.log(`Error: ${error}`);
      req.flash('error_msg', {
        message: 'Error getting auth URL',
        debug: JSON.stringify(error, Object.getOwnPropertyNames(error))
      });
      res.redirect('/');
    }
  }
);

OAUTH_SCOPES: アプリケーションが要求する権限のスコープを定義します。
環境変数 OAUTH_SCOPES から取得。指定がない場合は https://graph.microsoft.com/.default が使用されます。

const authUrl = await req.app.locals.msalClient.getAuthCodeUrl(urlParameters);
指定したスコープとリダイレクトURIを使い、認証ページのURLを生成します。

routes/auth.js
// <CallbackSnippet>
router.get('/callback',
  async function(req, res) {
    const scopes = process.env.OAUTH_SCOPES || 'https://graph.microsoft.com/.default';
    const tokenRequest = {
      code: req.query.code,
      scopes: scopes.split(','),
      redirectUri: process.env.OAUTH_REDIRECT_URI
    };

    try {
      const response = await req.app.locals
        .msalClient.acquireTokenByCode(tokenRequest);

      // Save the user's homeAccountId in their session
      req.session.userId = response.account.homeAccountId;

      const user = await graph.getUserDetails(
        req.app.locals.msalClient,
        req.session.userId
      );

      // Add the user to user storage
      req.app.locals.users[req.session.userId] = {
        displayName: user.displayName,
      // email: user.mail || user.userPrincipalName,
      // timeZone: user.mailboxSettings.timeZone,
      };
    } catch(error) {
      req.flash('error_msg', {
        message: 'Error completing authentication',
        debug: JSON.stringify(error, Object.getOwnPropertyNames(error))
      });
    }

    res.redirect('/');
  }
);
// </CallbackSnippet>

Microsoft の認証サーバーからリダイレクトされてきた後、アクセストークンを取得し、ユーザー情報を取得するためのものです。

code: Microsoft 認証サーバーからリダイレクト時に渡される認可コード (req.query.code)。

const response = await req.app.locals.msalClient.acquireTokenByCode(tokenRequest);

acquireTokenByCode: 認可コードを使ってアクセストークンを取得する関数。
レスポンスには以下の情報が含まれます:
•accessToken: API呼び出しに必要なトークン。
•account: 認証されたユーザーの情報。ここから homeAccountId を取得します。

req.session.userId = response.account.homeAccountId;
•homeAccountId は、ユーザーを一意に識別するためのID。
•セッション (req.session) に保存して、以降のリクエストでこのユーザーを識別します。

const user = await graph.getUserDetails(
req.app.locals.msalClient,
req.session.userId
);
graph.getUserDetails: Microsoft Graph API を呼び出して、ユーザーの詳細情報を取得する関数。
取得される情報の例:
•displayName: ユーザーの表示名。
•mail または userPrincipalName: ユーザーのメールアドレス。
•mailboxSettings.timeZone: メールボックスのタイムゾーン設定。

req.app.locals.users[req.session.userId] = {
displayName: user.displayName,
email: user.mail || user.userPrincipalName,
timeZone: user.mailboxSettings.timeZone
};
•取得したユーザー情報をアプリケーションのローカルストレージ (req.app.locals.users) に保存。
•セッションID (userId) をキーとして保存することで、複数のユーザーを管理できます。

} catch(error) {
req.flash('error_msg', {
message: 'Error completing authentication',
debug: JSON.stringify(error, Object.getOwnPropertyNames(error))
});
}
エラー内容を取得して、フラッシュメッセージに格納しています。このコードでは画面に表示することはしていません。app.jsのコードからres.render("error");として、さらにerror.hbsで画面に表示されています。

resは単一のレスポンスでしか利用されないため、永続的にデータを保持する目的には適していません。そのため、req.app.localsやセッション(req.session)がデータの保存に使用されています。

req.app.localsはapp.localsへのアクセス手段に過ぎず、両者は同じオブジェクトを参照しています。どちらを使うかは、コードの文脈や好みによります。

routes/auth.js
router.get('/signout',
  async function(req, res) {
    // Sign out
    if (req.session.userId) {
      // Look up the user's account in the cache
      const accounts = await req.app.locals.msalClient
        .getTokenCache()
        .getAllAccounts();

      const userAccount = accounts.find(a => a.homeAccountId === req.session.userId);

      // Remove the account
      if (userAccount) {
        req.app.locals.msalClient
          .getTokenCache()
          .removeAccount(userAccount);
      }
    }

    // Destroy the user's session
    req.session.destroy(function () {
      res.redirect('/');
    });
  }
);

if (req.session.userId) {
req.session.userIdが存在する場合(すなわち、ユーザーがログイン中の場合)、サインアウト処理を進めます。

req.session.destroy(function () {
res.redirect('/');
});
•Expressセッションを破棄し、サーバー側に保存されているセッションデータを削除します。
•セッションが破棄された後、ユーザーはアプリケーションのホームページ(/)にリダイレクトされます。

graph.js(一部抜粋)

graph.js
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

var graph = require('@microsoft/microsoft-graph-client');
require('isomorphic-fetch');

module.exports = {
  getUserDetails: async function(msalClient, userId) {
    const client = getAuthenticatedClient(msalClient, userId);

    const user = await client
      .api('/me')
      // .select('displayName,mail,mailboxSettings,userPrincipalName')
      .select('displayName')
      .get();
    return user;
  },

const client = getAuthenticatedClient(msalClient, userId);
getAuthenticatedClientを使用して認証済みのクライアントを取得します。

Graph APIの/meエンドポイントを呼び出します。
.select: 必要なプロパティだけを取得するためのメソッドです。

return user;
APIから取得したデータをそのまま呼び出し元に返します。

コードの流れまとめ

今までいろんなファイルのコードを見てきましたが、ここでまとめます。

1,npm startとするとbin/wwwが実行され、中身としてはapp.jsが実行され、サーバが起動します。
2,app.jsでは.envに設定されたクライアントの情報などが読み込まれ、ルーティングなどが定義されています。app.localsオブジェクトや、req.sessionオブジェクトを定義して、アプリケーション全体で使えるようにしています。
3,http://localhost:3000にアクセスすると、./routes/index.jsのファイルが実行され、index.hbsの内容のウェブサイトが表示されます。
4,ウェブサイト上でsign inボタンを押すと、./routes/auth.jsのファイルの"/signin"のルーティングの関数が実行され、認証ページのURLにリダイレクトされます。
5,認証後は、.envファイルに定義されているようにhttp://localhost:3000/auth/callbackにリダイレクトされます。
6,auth/callbackでは、認証サーバーから渡される認可コードを使用して、アクセストークンを取得し、さらにgraph.jsを使用してユーザ情報を取得したあとに、http://localhost:3000にリダイレクトします。
7,./routes/index.jsのファイルが実行され、index.hbsの内容のウェブサイトが表示される際に、userが存在するので、ユーザは自身の情報をウェブサイト上で見ることが出来ます。
8,sign outボタンをクリックすることで、セッション情報が削除され、元のページに戻ります。

まとめ

Entra IDとMSALを利用して、認証コードの解説を記載しました。全てを理解出来ているわけではないですが、どのような流れで認証を行っているのか把握出来ました。

コードはこちらにあります。

Reference

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?