@azure/msal-node を使って Entra ID (Azure AD) で認証、JWT トークンを取得する (Next.js)
はじめに
Microsoft Entra ID (旧称: Azure Active Directory) は、Microsoft が提供する認証・認可サービスです。
この記事では、Next.js の API Route を使用して、Microsoft Entra ID にログインするユーザーを認証し、JWT トークンを取得する方法を紹介します。
JWT トークンを取得することで、アプリケーション内でユーザーの情報を取得したり、Microsoft Entra ID で保護された API (例: Microsoft Graph) を呼び出したりできます。
Microsoft Authentication Library (MSAL) とは
MSAL はセキュリティで保護された Web API へのアクセスを容易に実装するためのライブラリです。
ここで得られた JWT トークンを使用することで、Microsoft の各サービスのリソースにアクセスし、Web アプリ上で使用できます。
事前準備
Microsoft Entra ID でアプリの登録
Microsoft Entra ID でアプリを認証するためには、Azure Portal でアプリを登録する必要があります。
Azure Portal にログインし、Microsoft Entra ID の画面に移動します。
その後、「アプリの登録」>「新規登録」を選択します。
任意の名前、サポートされているアカウントの種類(この組織ディレクトリのみに含まれるアカウント)を選択し、リダイレクト URI を web
http://localhost:3000/auth/redirect
に設定します。
クライアントシークレットの作成
続いて、クライアントシークレットを作成します。
Microsoft Entra ID に登録したアプリのページに移動し、「証明書とシークレット」を選択します。
「新しいクライアントシークレット」を選択し、任意の名前を入力し、シークレットを作成します。
このシークレットは、後ほど使用するので、コピーしておきます。
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
アプリの実装編
今回実装するアプリの認証フローは以下の通りです。
@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
では、responseMode
を query
に設定しています。これによって認証が完了した後、リダイレクト先の 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)
}
動作確認
-
npm run dev
でアプリを起動し、http://localhost:3000
にアクセスします。 - サインインボタンを押すと、Microsoft のユーザーログイン画面が表示されます。
- ログインすると、
http://localhost:3000/auth/redirect
にリダイレクトされ、JWT トークンが表示されます。
まとめ
今回は、Next.js の API Route を使用して、Microsoft Entra ID で認証し、JWT トークンを取得する方法を紹介しました。
JWT トークンを取得することで、アプリケーション内でユーザーの情報を取得したり、Microsoft Entra ID で保護された API を呼び出したりできます。
Azure Entra ID のアプリ登録に scope を追加することで、このトークンを使用して、Outlook のメールやカレンダーの予定などをアプリケーション側で取得できます。
参考文献
Discussion