はじめに
前回の記事で、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>
上記もコマンド実行後、作成されたアプリケーションの情報が出力されます。その中のappId
と publisherDomain
を後で色々使うので、メモしよう!
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
をバックグラウンドで実行しつつ、dev
でnodemon
の力を使ってTypeScript環境下でのホットリロードを実現しています。開発が終わった際の実行は、build
してから、start
してください。
{
"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 の作成
とりあえず、完成品のコードを先に載せます。その後に、部分に分けて、解説します。
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のレスポンスオブジェクトから型を定義しました。
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
がタイプエラーなります。
export {}
declare module "express-session" {
interface SessionData {
accessToken: string;
}
}
Views ファイルたち
Graph APIから取得したユーザのデータをテーブルの表示しています。
<!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>
エラーページです。ログインページへのリンクがあります。
<!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をクリック。
パスワードを入力して、Sign in します。
ログインが成功すると、自動的に登録したRedirect URLに飛ばされます。そしてそのレスポンスを受け取ったNode.jsサーバは発行されたアクセストークンをセッションに登録して、http://localhost:3000/user にリダイレクトします。その後に、セッションに登録したアクセストークンを使いGraph APIにユーザの情報を取りに行き、以下のようにviewにレンダーします。無事ユーザの情報が表示されました。
おわり
無事に、Node.jsからAzure ADの認証に成功して、さらにGraph APIからログイン中のユーザ情報を取得することができました。
今回もMSALの力を最大限にお借りしました。ありがとうございます。