💎

@azure/msal-node を使って Entra ID (Azure AD) で認証、JWT トークンを取得する (Next.js)

2023/11/07に公開

はじめに

Microsoft Entra ID (旧称: Azure Active Directory) は、Microsoft が提供する認証・認可サービスです。
この記事では、Next.js の API Route を使用して、Microsoft Entra ID にログインするユーザーを認証し、JWT トークンを取得する方法を紹介します。

JWT トークンを取得することで、アプリケーション内でユーザーの情報を取得したり、Microsoft Entra ID で保護された API (例: Microsoft Graph) を呼び出したりできます。

https://www.microsoft.com/ja-jp/security/business/identity-access/microsoft-entra-id

Microsoft Authentication Library (MSAL) とは

MSAL はセキュリティで保護された Web API へのアクセスを容易に実装するためのライブラリです。
ここで得られた JWT トークンを使用することで、Microsoft の各サービスのリソースにアクセスし、Web アプリ上で使用できます。

https://learn.microsoft.com/ja-jp/entra/identity-platform/msal-overview

事前準備

Microsoft Entra ID でアプリの登録

Microsoft Entra ID でアプリを認証するためには、Azure Portal でアプリを登録する必要があります。
Azure Portal にログインし、Microsoft Entra ID の画面に移動します。
その後、「アプリの登録」>「新規登録」を選択します。

Azure Portal の画面

任意の名前、サポートされているアカウントの種類(この組織ディレクトリのみに含まれるアカウント)を選択し、リダイレクト URI を web http://localhost:3000/auth/redirect に設定します。

Azure Portal の画面

クライアントシークレットの作成

続いて、クライアントシークレットを作成します。
Microsoft Entra ID に登録したアプリのページに移動し、「証明書とシークレット」を選択します。
「新しいクライアントシークレット」を選択し、任意の名前を入力し、シークレットを作成します。
このシークレットは、後ほど使用するので、コピーしておきます。

Alt text

Next.js のプロジェクトを作成

以下のコマンドで Next.js のプロジェクトを作成します。

npx create-next-app@latest msal-node-sample
cd msal-node-sample

@azure/msal-node のインストール

@azure/msal-node は、Node.js アプリにおいて Microsoft Entra ID で認証をするためのライブラリです。
以下のコマンドでインストールします。
また、クライアント側で API を叩くときに使用する axios もインストールします。

npm install @azure/msal-node axios

https://www.npmjs.com/package/@azure/msal-node

アプリの実装編

今回実装するアプリの認証フローは以下の通りです。
@msal/node は Next.js の API Route で使用するため、サーバーサイドで認証します。

アプリの構成

環境変数の設定

.env に以下の環境変数を設定します。

CLOUD_INSTANCE=https://login.microsoftonline.com/
TENANT_ID=<Microsoft Entra IDのテナント ID>
CLIENT_ID=<Azure ポータル で登録したアプリケーション(クライアント) ID>
CLIENT_SECRET=<Azure ポータルで作成したシークレット>

REDIRECT_URI=http://localhost:3000/auth/redirect

認証用のインスタンスを作成

./app/api/msal.ts を作成し、以下のように実装します。

import { AuthenticationResult, ConfidentialClientApplication, Configuration, CryptoProvider, LogLevel } from "@azure/msal-node"

export class MsalService {
  private _config: Configuration = {
    auth: {
        clientId: process.env.CLIENT_ID ?? "",
        authority: (process.env.CLOUD_INSTANCE ?? "") + (process.env.TENANT_ID ?? ""),
        clientSecret: process.env.CLIENT_SECRET
    },
    system: {
        loggerOptions: {
            piiLoggingEnabled: false,
            logLevel: LogLevel.Info,
        }
    }
  }

  private _msalInstance: ConfidentialClientApplication = new ConfidentialClientApplication(this._config)
  private _msalCryptProvider: CryptoProvider = new CryptoProvider()
  private _REDIRECT_URI: string = process.env.REDIRECT_URI ?? ""

  // 認証用のコードを発行する
  public getCryptoCodeVerifier = async(): Promise<{verifier: string, challenge: string, state: string}> => {
    const csrfToken = this._msalCryptProvider.createNewGuid()
    const {verifier, challenge} = await this._msalCryptProvider.generatePkceCodes()

    const state = this._msalCryptProvider.base64Encode(
      JSON.stringify({
        csrfToken,
        redirectTo: "/",
      })
    )
    return {verifier, challenge, state}
  }

  // 認証用のURLを発行する
  public getAuthCodeUrl = async(challenge: string, state: string, scopes?: string[]): Promise<string> => {
    const redirectURL = await this._msalInstance.getAuthCodeUrl({
      redirectUri: this._REDIRECT_URI,
      codeChallengeMethod: "S256",
      codeChallenge: challenge,
      responseMode: "query",
      state,
      scopes: scopes ?? [],
    })
    return redirectURL
  }

  // 認証コードを検証し、JWT トークンを取得する
  public acquireTokenByCode = async(code: string, verifier: string, scopes?: string[]): Promise<AuthenticationResult> => {
    return await this._msalInstance.acquireTokenByCode({
      code: code,
      codeVerifier: verifier,
      redirectUri: this._REDIRECT_URI,
      scopes: scopes ?? [],
    })
  }
}

長々と解説してもアレなので、ポイントで説明します。

  • getCryptoCodeVerifier で、認証用のコードを発行します。このコードは、認証用の URL を発行する際に使用します。
  • getAuthCodeUrl で、認証用の URL を発行します。この URL にアクセスすることで、Microsoft Entra ID で認証できます。
  • acquireTokenByCode で、認証コードを検証し、JWT トークンを取得します。

getAuthCodeUrl では、responseModequery に設定しています。これによって認証が完了した後、リダイレクト先の URL に認証コードが付与されます。
認証方法によってはパラメータを調整する必要があります。

サインイン用の API Route を実装

app/api/auth/signin/route.ts を作成し、以下のように実装します。
CSRF トークンを発行し、それを使用した認証用の URL を発行し、リダイレクトします。その際、検証用のトークンを Cookie に保存します。

import { NextResponse } from "next/server"
import { MsalService } from "../../msal"

// 認証用のURLを発行する
export async function GET() {
  const msalService = new MsalService()

  const {verifier, challenge, state} = await msalService.getCryptoCodeVerifier()
  const redirectURL = await msalService.getAuthCodeUrl(challenge, state)

  return NextResponse.json({redirect_url: redirectURL}, {status: 200, headers: {'Set-Cookie': `csrfToken=${verifier}`}})
}

サインインページの作成

pages/index.tsx を作成し、以下のように実装します。
サインインボタンを押すと、API Route /api/auth/signin から返却された URL にリダイレクトします。
ここでリダイレクトされると、Microsoft のユーザーログイン画面が表示され、認証が完了すると、http://localhost:3000/auth/redirect にリダイレクトされます。

'use client';
import { AuthenticationResult } from "@azure/msal-node"
import axios from "axios"
import { useSearchParams } from "next/navigation"
import { useEffect, useState } from "react"

export default function Home() {
  const signin = async() => {
    const {data} = await axios.get("/api/auth/signin")
    // API Route から返却された URL にリダイレクトする
    window.location.href = data.redirect_url
  }

  return (
    <main>
      <button onClick={() => signin()}>サインイン</button>
    </main>
  )
}

リダイレクト先のページを作成

pages/auth/redirect/page.tsx を作成し、リダイレクト先のページを実装します。
リダイレクトされた後のページには ?code=xxxx というクエリパラメータが付与されています。
そこで、クエリパラメータの code を取得し、API Route /api/auth/verify に送信します。

'use client';
import { AuthenticationResult } from "@azure/msal-node"
import axios from "axios"
import { useSearchParams } from "next/navigation"
import { useEffect, useState } from "react"

export default function Home() {
  const params = useSearchParams()
  const [state, setState] = useState({
    jwt: "",
  })
  const [code, _] = useState(params.get("code"))

  useEffect(() => {
    (async() => {
      // 認証をかける
      const url = "/api/auth/verify"
      const {data}: {data: AuthenticationResult} = await axios.post(url, {
        code
      })
      setState({jwt: data.accessToken})
    })()
  }, [code])

  return (
    <div>
      {state.jwt}
    </div>
  )
}

リダイレクト用の API Route を実装

app/api/auth/verify/route.ts を作成します。
Cookie に保存されている検証用のトークンを取得し、認証コードを検証した後、JWT トークンを取得します。

import { NextRequest, NextResponse } from "next/server"
import { MsalService } from "../../msal"

export async function POST(request: NextRequest) {
  const msalService = new MsalService()
  const json = await request.json()
  const code = json.code as string
  if (!code) {
    return NextResponse.json({error: "code is not found"}, {status: 400})
  }

  const verifier = request.cookies.get("csrfToken")?.value
  if (!verifier) {
    return NextResponse.json({error: "invalid request"}, {status: 400})
  }

  const result = await msalService.acquireTokenByCode(code, verifier)
  return NextResponse.json(result)
}

動作確認

  1. npm run dev でアプリを起動し、http://localhost:3000 にアクセスします。
  2. サインインボタンを押すと、Microsoft のユーザーログイン画面が表示されます。
  3. ログインすると、http://localhost:3000/auth/redirect にリダイレクトされ、JWT トークンが表示されます。

まとめ

今回は、Next.js の API Route を使用して、Microsoft Entra ID で認証し、JWT トークンを取得する方法を紹介しました。
JWT トークンを取得することで、アプリケーション内でユーザーの情報を取得したり、Microsoft Entra ID で保護された API を呼び出したりできます。
Azure Entra ID のアプリ登録に scope を追加することで、このトークンを使用して、Outlook のメールやカレンダーの予定などをアプリケーション側で取得できます。

参考文献

https://www.microsoft.com/ja-jp/security/business/identity-access/microsoft-entra-id

https://learn.microsoft.com/ja-jp/entra/identity-platform/msal-overview

https://www.npmjs.com/package/@azure/msal-node

GitHubで編集を提案
Microsoft (有志)

Discussion