8
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

株式会社船井総研デジタルAdvent Calendar 2022

Day 14

Node.js(TypeScript) からAzure AD に認証をして、ついでにGraph APIからユーザ情報を取得する

Last updated at Posted at 2022-12-13

はじめに

前回の記事で、MSALの力を借りてSPA(React)からAzure AD認証を行いました。

今回は、サーバサイドからNode.jsを使って、Azure AD認証を行いたいと思います。あ、あとついでに認証後に取得できるアクセストークンを使って、Microsoft Graph APIにリクエストを送り、現在ログイン中のユーザのデータも引っ張ってきたいと思います。

Azure ADにアプリケーションを追加する。

今回は、azure ポータルからではなく、az CLIからアプリケーションの追加を行います。ポータルから追加する方法は、以下の記事を参考にしてださい。

アプリ名は任意の名前で、Redirct URLはNode.jsサーバーが動いているURLで設定してください。

az ad app create --display-name <アプリ名> --web-redirect-uris <Redirect URL>

上記もコマンド実行後、作成されたアプリケーションの情報が出力されます。その中のappIdpublisherDomainを後で色々使うので、メモしよう!

az ad app create --display-name test-aad-app --web-redirect-uris http://localhost:3000 --enable-access-token-issuance
{
  "@odata.context": "https://graph.microsoft.com/v1.0/$metadata#applications/$entity",
  "addIns": [],
  "api": {
    "acceptMappedClaims": null,
    "knownClientApplications": [],
    "oauth2PermissionScopes": [],
    "preAuthorizedApplications": [],
    "requestedAccessTokenVersion": 2
  },
  "appId": "3dd9f38c-e4eb-41f3-80eb-e9ade2762b98", # <= メモしよう!
  "appRoles": [],
  "applicationTemplateId": null,
  "certification": null,
  "createdDateTime": "2022-11-16T02:37:35.7400463Z",
  "defaultRedirectUri": null,
  "deletedDateTime": null,
  "description": null,
  "disabledByMicrosoftStatus": null,
  "displayName": "test-aad-app",
  "groupMembershipClaims": null,
  "id": "1edeeefc-e2c0-4620-82b9-cafc0a184b04",
  "identifierUris": [],
  "info": {
    "logoUrl": null,
    "marketingUrl": null,
    "privacyStatementUrl": null,
    "supportUrl": null,
    "termsOfServiceUrl": null
  },
  "isDeviceOnlyAuthSupported": null,
  "isFallbackPublicClient": null,
  "keyCredentials": [],
  "notes": null,
  "optionalClaims": null,
  "parentalControlSettings": {
    "countriesBlockedForMinors": [],
    "legalAgeGroupRule": "Allow"
  },
  "passwordCredentials": [],
  "publicClient": {
    "redirectUris": []
  },
  "publisherDomain": "<あなたのアカウント名>.onmicrosoft.com", # <= メモしよう!
  "requiredResourceAccess": [],
  "samlMetadataUrl": null,
  "serviceManagementReference": null,
  "signInAudience": "AzureADandPersonalMicrosoftAccount",
  "spa": {
    "redirectUris": []
  },
  "tags": [],
  "tokenEncryptionKeyId": null,
  "verifiedPublisher": {
    "addedDateTime": null,
    "displayName": null,
    "verifiedPublisherId": null
  },
  "web": {
    "homePageUrl": null,
    "implicitGrantSettings": {
      "enableAccessTokenIssuance": true,
      "enableIdTokenIssuance": false
    },
    "logoutUrl": null,
    "redirectUriSettings": [
      {
        "index": null,
        "uri": "http://localhost:3000"
      }
    ],
    "redirectUris": [
      "http://localhost:3000"
    ]
  }
}

アクセストークンのシークレット(パスワード)を設定します。

az ad app credential reset --id <アプリケーション ID>

上記もコマンド実行後、passwordが出力されます。Node.jsアプリケーションで使用します。おそらく、この機会を逃すと二度とパスワードを見ることができません、絶対にメモしてください。

❯ az ad app credential reset --id "3dd9f38c-e4eb-41f3-80eb-e9ade2762b98"
The output includes credentials that you must protect. Be sure that you do not include these credentials in your code or check the credentials into your source control. For more information, see https://aka.ms/azadsp-cli
{
  "appId": "3dd9f38c-e4eb-41f3-80eb-e9ade2762b98",
  "password": "5ba8Q~6l1YRS-A7HIKyRM62oTi5faGmpcE8wDbij", # <= 絶対にメモしてください!
  "tenant": "43e7a86a-7f63-48e7-aec5-73f97ec541a4"
}

Azure AD にテスト用のユーザ追加する

またまた、azure ポータルからではなく、az CLIからユーザの追加を行います。ポータルから追加する方法は、以下の記事を参考にしてださい。

以下のコマンドを叩くだけで、簡単にAzure ADにユーザを追加することができます。

az ad user create \
  --display-name <ユーザ名> \
  --password <強めの任意のパスワード> \
  --user-principal-name <publisherDomain(さっきメモした)> \
  --force-change-password-next-sign-in false

ユーザが無事作られると、以下のようにメタ情報が出力されます。メタ情報の中には、givenName``surname``jobTitleなど含まれていますが、現状CLIからではそれらの情報を追加することができないみたいです。(以下のリンク参照) ですので、ポータル上で必要であれば追加してください。

userPrincipalNameはログイン時に必要なので、メモしよう!

az ad user create \
  --display-name niceuser \
  --password thisIsReallyStrongPassword123 \
  --user-principal-name niceuser@ryuichinishidevgmail.onmicrosoft.com \
  --force-change-password-next-sign-in false

{
  "@odata.context": "https://graph.microsoft.com/v1.0/$metadata#users/$entity",
  "businessPhones": [],
  "displayName": "niceuser",
  "givenName": null,
  "id": "04e82f0f-dd3f-40f1-b5e0-65b1544a39ff",
  "jobTitle": null,
  "mail": null,
  "mobilePhone": null,
  "officeLocation": null,
  "preferredLanguage": null,
  "surname": null,
  "userPrincipalName": "niceuser@ryuichinishidevgmail.onmicrosoft.com" # <= ログイン時に必要!メモ!
}

Node.js(TypeScript)のプロジェクトの作成

プロジェクトファルダーの作成

proj=az-auth-node
mkdir ${proj} && cd ${proj}
npm init -y

ライブラリーのインストール

  • @azure/msal-node => Azure AD 認証のためのクライアント
  • express => サーバー
  • ejs => テンプレートエンジン
  • express-session => セッションストレージ。アクセストークンの保存に使用します。
  • axios => HTTP クライアント。Graph APIに通信するために使います。
npm install -E @azure/msal-node express ejs express-session axios

開発用ライブラリーのインストール

  • nodemon => ホットリロードのため。
  • typescript => typescript ファイルのトランスパイラー。
  • @types/node => 型定義
  • @types/express => 型定義
  • @types/express-session => 型定義
npm install -DE nodemon typescript @types/node @types/express @types/express-session

package.json ファイルの編集

scriptsセクションを編集します。開発時は、watchをバックグラウンドで実行しつつ、devnodemonの力を使ってTypeScript環境下でのホットリロードを実現しています。開発が終わった際の実行は、buildしてから、startしてください。

package.json
{
  "name": "az-auth-node",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "start": "node dist/index.js", # <= 編集!
    "dev": "nodemon dist/index.js", # <= 編集!
    "watch": "tsc --watch", # <= 編集!
    "build": "tsc" # <= 編集!
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "@azure/msal-node": "1.14.3",
    "axios": "1.1.3",
    "ejs": "3.1.8",
    "express": "4.18.2",
    "express-session": "1.17.3"
  },
  "devDependencies": {
    "@types/express": "4.17.14",
    "@types/express-session": "1.17.5",
    "@types/node": "18.11.9",
    "nodemon": "2.0.20",
    "typescript": "4.8.4"
  }
}

index.ts の作成

とりあえず、完成品のコードを先に載せます。その後に、部分に分けて、解説します。

src/index.ts
import axios from "axios";
import express from "express";
import expressSession from "express-session";
import { ConfidentialClientApplication } from "@azure/msal-node";
import { GraphMeResponse } from "./types/msal";

async function main() {
  const PORT = process.env.PORT || 3000;
  const SESSION_SECRET = process.env.SESSION_SECRET || "niceSecret123";
  const MSAL_REDIRECT_URI = process.env.REDIRECT_URL || "http://localhost:3000/redirect";
  const MSAL_CLIENT_ID = process.env.MSAL_CLIENT_ID || "940db8ec-e7dc-4f9c-ba93-d7e9b9789065";
  const MSAL_AUTHORITY = process.env.MSAL_AUTHORITY || "https://login.microsoftonline.com/ryuichinishidevgmail.onmicrosoft.com/";
  const MSAL_CLIENT_SECRET = process.env.MSAL_CLIENT_SECRET || "LAd8Q~VyeQbCwI~WpLvpoaDopbX6eOEcI12rgbaN";
  const authClient = new ConfidentialClientApplication({
    auth: {
      clientId: MSAL_CLIENT_ID,
      authority: MSAL_AUTHORITY,
      clientSecret: MSAL_CLIENT_SECRET,
    },
  });

  const app = express();

  app.set("view engine", "ejs");

  const sessionMaxAge = 1000 * 60 * 60 * 24; // 1 day
  app.use(
    expressSession({
      cookie: { maxAge: sessionMaxAge, httpOnly: true },
      secret: SESSION_SECRET,
      resave: false,
      saveUninitialized: false,
    })
  );

  app.get("/", (req, res) => {
    const accessToken = req.session.accessToken;
    if (!accessToken) return res.redirect("/login");
    res.redirect("user");
  });

  app.get("/login", async (_, res) => {
    const authCodeUrlParameters = {
      scopes: ["user.read"],
      redirectUri: MSAL_REDIRECT_URI,
    };
    const response = await authClient.getAuthCodeUrl(authCodeUrlParameters);
    res.redirect(response);
  });

  app.get("/redirect", async (req, res) => {
    const response = await authClient.acquireTokenByCode({
      code: req.query.code as string,
      scopes: ["user.read"],
      redirectUri: MSAL_REDIRECT_URI,
    });
    req.session.accessToken = response.accessToken;
    res.redirect("/user");
  });

  app.get("/user", async (req, res) => {
    try {
      const accessToken = req.session?.accessToken;
      const result = await axios.get<GraphMeResponse>(
        "https://graph.microsoft.com/v1.0/me",
        {
          headers: {
            Authorization: `Bearer ${accessToken}`,
          },
        }
      );

      const data = result.data;

      res.render("user", {
        id: data.id,
        userName: data.userPrincipalName,
        firstName: data.givenName,
        lastName: data.surname
      });
    } catch (error) {
      res.redirect("/error");
    }
  });

  app.get("/error", (_, res) => {
    res.render("error");
  });

  app.listen(PORT, () =>
    console.log(`server is up and running at http://localhost:${PORT}`)
  );
}

main().catch((err) => {
  console.error(err);
  process.exit(1);
});

設定値を定義しています。

  • PORT => サーバーのポート番号。
  • SESSION_SECRET => express-session用のシークレットキー。
  • MSAL_REDIRECT_URI => Azure ADでアプリを登録する際に設定したRedirect URL。
  • MSAL_CLIENT_ID => さっきメモしたappId
  • MSAL_AUTHORITY => https://login.microsoftonline.com/ + publisherDomain(さっきメモした)
  • MSAL_CLIENT_SECRET => さっきメモしたパスワード
async function main() {
  const PORT = process.env.PORT || 3000;
  const SESSION_SECRET = process.env.SESSION_SECRET || "niceSecret123";
  const MSAL_REDIRECT_URI = process.env.REDIRECT_URL || "http://localhost:3000/redirect";
  const MSAL_CLIENT_ID = process.env.MSAL_CLIENT_ID || "940db8ec-e7dc-4f9c-ba93-d7e9b9789065";
  const MSAL_AUTHORITY = process.env.MSAL_AUTHORITY || "https://login.microsoftonline.com/ryuichinishidevgmail.onmicrosoft.com/";
  const MSAL_CLIENT_SECRET = process.env.MSAL_CLIENT_SECRET || "LAd8Q~VyeQbCwI~WpLvpoaDopbX6eOEcI12rgbaN";
  // 以下省略...

型定義ファイルたち

Graph APIのレスポンスオブジェクトから型を定義しました。

src/types/msal.ts
export type GraphMeResponse = {
  displayName: string;
  surname: string;
  givenName: string;
  id: string;
  userPrincipalName: string;
  businessPhones: never[];
  jobTitle: string;
  mail: string;
  mobilePhone: string;
  officeLocation: string;
  preferredLanguage: string;
};

以下の定義ファイルを用意しないと、req.session.accessTokenがタイプエラーなります。

src/types/express-session.d.ts
export {}

declare module "express-session" {
  interface SessionData {
    accessToken: string;
  }
}

Views ファイルたち

Graph APIから取得したユーザのデータをテーブルの表示しています。

views/user.ejs
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>User Information</title>
</head>
<body>
  <div>
    <h2>User information</h2>
  
    <table border="1">
      <tr>
        <th>ID</th>
        <th>Username</th>
        <th>First Name</th>
        <th>Last Name</th>
      </tr>
  
      <tr>
        <td><%= id %></td>
        <td><%= userName %></td>
        <td><%= firstName %></td>
        <td><%= lastName %></td>
      </tr>
    </table>
  </div>
  
</body>
</html>

エラーページです。ログインページへのリンクがあります。

views/error.ejs
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Error</title>
</head>
<body>
    <h2>Error</h2>
    <div>
        <p>Something went wrong. Please login again.</p>
        <a href="/login">Login</a>
    </div>
</body>
</html>

プロジェクトの全体像

├── package-lock.json
├── package.json
├── src
│   ├── index.ts
│   └── types
│       ├── express-session.d.ts
│       └── msal.ts
├── tsconfig.json
└── views
    ├── error.ejs
    └── user.ejs

試してみる

npm run build && npm run start
❯ npm run start

> az-auth-node@1.0.0 start
> node dist/index.js

server is up and running at http://localhost:3000

http://localhost:3000 にアクセスすると、自動的にmicrosoftのログインページに飛ばされます。そこに作成した、ユーザのuserPrincipalNameを入力します。そしてnextをクリック。
Screen Shot 2022-11-16 at 13.36.50.png
パスワードを入力して、Sign in します。
Screen Shot 2022-11-16 at 13.38.12.png
ログインが成功すると、自動的に登録したRedirect URLに飛ばされます。そしてそのレスポンスを受け取ったNode.jsサーバは発行されたアクセストークンをセッションに登録して、http://localhost:3000/user にリダイレクトします。その後に、セッションに登録したアクセストークンを使いGraph APIにユーザの情報を取りに行き、以下のようにviewにレンダーします。無事ユーザの情報が表示されました。
Screen Shot 2022-11-16 at 13.38.29.png

おわり

無事に、Node.jsからAzure ADの認証に成功して、さらにGraph APIからログイン中のユーザ情報を取得することができました。
今回もMSALの力を最大限にお借りしました。ありがとうございます。

8
5
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
8
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?