Azure Active Directory B2C を使用して独自の Node Web アプリケーションで認証を有効にする
この記事では、独自の Node.js Web アプリケーションで Azure Active Directory B2C (Azure AD B2C) 認証を追加する方法について説明します。 ユーザーは Azure AD B2C ユーザー フローを使用してサインイン、サインアウト、プロファイルの更新、パスワードのリセットを行うことができます。 この記事では Node 用の Microsoft Authentication Library (MSAL) を使用して、ノード Web アプリケーションへの認証の追加を簡略化します。
この記事の目的は、「Azure AD B2C を使ってサンプル Node.js Web アプリケーションで認証を構成する」で使用したサンプル アプリケーションを、独自の Node.js Web アプリケーションに置き換える方法です。
この記事では、Node.js と Express を使用して、基本的な Node.js Web アプリを作成します。 アプリケーションのビューでは、Handlebars が使用されます。
必須コンポーネント
- 「Azure AD B2C を使ってサンプル Node.js Web アプリケーションで認証を構成する」の手順を完了します。 Azure AD B2C ユーザー フローを作成し、Web アプリケーションを Azure portal に登録します。
手順 1: ノード プロジェクトを作成する
ノード アプリケーションをホストするフォルダーを作成します (例: active-directory-b2c-msal-node-sign-in-sign-out-webapp
)。
ターミナルで、
cd active-directory-b2c-msal-node-sign-in-sign-out-webapp
などのノード アプリ フォルダーにディレクトリを変更し、npm init -y
を実行します。 このコマンドは、Node.js プロジェクトのデフォルトのpackage.json
ファイルを作成します。ターミナルで、
npm install express
を実行します。 このコマンドで、Express フレームワークがインストールされます。次のプロジェクト構造体を実現するために、さらに多くのフォルダーとファイルを作成します。
active-directory-b2c-msal-node-sign-in-sign-out-webapp/ ├── index.js └── package.json └── .env └── views/ └── layouts/ └── main.hbs └── signin.hbs
views
フォルダーには、アプリの UI のハンドルバーファイルが含まれています。
手順 2: アプリの依存関係をインストールする
ターミナルで、次のコマンドを実行して、 dotenv
、 express-handlebars
、 express-session
、 および @azure/msal-node
パッケージをインストールします。
npm install dotenv
npm install express-handlebars
npm install express-session
npm install @azure/msal-node
手順 3: アプリ UI コンポーネントをビルドする
main.hbs
ファイルに、次のコードを追加します。
<!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>Tutorial | Authenticate users with MSAL for B2C</title>
<!-- adding Bootstrap 4 for UI components -->
<!-- CSS only -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous">
<link rel="SHORTCUT ICON" href="https://c.s-microsoft.com/favicon.ico?v2" type="image/x-icon">
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-dark bg-primary">
<a class="navbar-brand" href="/">Microsoft Identity Platform</a>
{{#if showSignInButton}}
<div class="ml-auto">
<a type="button" id="SignIn" class="btn btn-secondary" href="/signin" aria-haspopup="true" aria-expanded="false">
Sign in
</a>
</div>
{{else}}
<div class="ml-auto">
<a type="button" id="EditProfile" class="btn btn-warning" href="/profile" aria-haspopup="true" aria-expanded="false">
Edit profile
</a>
<a type="button" id="PasswordReset" class="btn btn-warning" href="/password" aria-haspopup="true" aria-expanded="false">
Reset password
</a>
</div>
<p class="navbar-brand d-flex ms-auto">Hi {{givenName}}</p>
<a class="navbar-brand d-flex ms-auto" href="/signout">Sign out</a>
{{/if}}
</nav>
<br>
<h5 class="card-header text-center">MSAL Node Confidential Client application with Auth Code Flow</h5>
<br>
<div class="row" style="margin:auto" >
{{{body}}}
</div>
<br>
<br>
</body>
</html>
main.hbs
ファイル は layout
フォルダー にあります。 アプリケーション全体で必要な HTML コードが含まれている必要があります。 signin.hbs
など、あるビューから別のビューに変わった UI は、{{{body}}}
として表示されるプレースホルダーに配置されます。
main.hbs
ファイルには、ブートストラップ 5 CSS フレームワークを使用して構築された UI が実装されています。 サインインすると、ユーザーには [パスワードの編集]、[パスワードのリセット]、および [サインアウト] UI コンポーネント (ボタン) が表示されます。 サインアウトすると、ユーザーには [サインイン] と表示されます。この動作は、アプリ サーバーが送信する showSignInButton
ブール変数によって制御されます。
signin.hbs
ファイルに、次のコードを追加します。
<div class="col-md-3" style="margin:auto">
<div class="card text-center">
<div class="card-body">
{{#if showSignInButton}}
<h5 class="card-title">Please sign-in to acquire an ID token</h5>
{{else}}
<h5 class="card-title">You have signed in</h5>
{{/if}}
</div>
<div class="card-body">
{{#if message}}
<h5 class="card-title text-danger">{{message}}</h5>
{{/if}}
</div>
</div>
</div>
手順 4: Web サーバーと MSAL クライアントを構成する
.env
ファイルに次のコードを追加し、サンプル Web アプリの構成に関するページで説明するように更新します。#HTTP port SERVER_PORT=3000 #web apps client ID APP_CLIENT_ID=<You app client ID here> #session secret SESSION_SECRET=sessionSecretHere #web app client secret APP_CLIENT_SECRET=<Your app client secret here> #B2C sign up and sign in user flow/policy authority SIGN_UP_SIGN_IN_POLICY_AUTHORITY=https://<your-tenant-name>.b2clogin.com/<your-tenant-name>.onmicrosoft.com/<sign-in-sign-up-user-flow-name> #B2C password reset user flow/policy authority RESET_PASSWORD_POLICY_AUTHORITY=https://<your-tenant-name>.b2clogin.com/<your-tenant-name>.onmicrosoft.com/<reset-password-user-flow-name> #B2C edit profile user flow/policy authority EDIT_PROFILE_POLICY_AUTHORITY=https://<your-tenant-name>.b2clogin.com/<your-tenant-name>.onmicrosoft.com/<profile-edit-user-flow-name> #B2C authority domain AUTHORITY_DOMAIN=https://<your-tenant-name>.b2clogin.com #client redirect url APP_REDIRECT_URI=http://localhost:3000/redirect #Logout endpoint LOGOUT_ENDPOINT=https://<your-tenant-name>.b2clogin.com/<your-tenant-name>.onmicrosoft.com/<sign-in-sign-up-user-flow-name>/oauth2/v2.0/logout?post_logout_redirect_uri=http://localhost:3000
index.js
ファイルに、アプリの依存関係を使用する次のコードを追加します。require('dotenv').config(); const express = require('express'); const session = require('express-session'); const {engine} = require('express-handlebars'); const msal = require('@azure/msal-node');
index.js
ファイルに次のコードを追加して、認証ライブラリを構成します。/** * Confidential Client Application Configuration */ const confidentialClientConfig = { auth: { clientId: process.env.APP_CLIENT_ID, authority: process.env.SIGN_UP_SIGN_IN_POLICY_AUTHORITY, clientSecret: process.env.APP_CLIENT_SECRET, knownAuthorities: [process.env.AUTHORITY_DOMAIN], //This must be an array redirectUri: process.env.APP_REDIRECT_URI, validateAuthority: false }, system: { loggerOptions: { loggerCallback(loglevel, message, containsPii) { console.log(message); }, piiLoggingEnabled: false, logLevel: msal.LogLevel.Verbose, } } }; // Initialize MSAL Node const confidentialClientApplication = new msal.ConfidentialClientApplication(confidentialClientConfig);
confidentialClientConfig
は、Azure AD B2C テナントの認証エンドポイントに接続するために使用される MSAL 構成オブジェクトです。index.js
ファイルにグローバル変数を追加するには、次のコードを追加します。/** * The MSAL.js library allows you to pass your custom state as state parameter in the Request object * By default, MSAL.js passes a randomly generated unique state parameter value in the authentication requests. * The state parameter can also be used to encode information of the app's state before redirect. * You can pass the user's state in the app, such as the page or view they were on, as input to this parameter. * For more information, visit: https://docs.microsoft.com/azure/active-directory/develop/msal-js-pass-custom-state-authentication-request * In this scenario, the states also serve to show the action that was requested of B2C since only one redirect URL is possible. */ const APP_STATES = { LOGIN: 'login', LOGOUT: 'logout', PASSWORD_RESET: 'password_reset', EDIT_PROFILE : 'update_profile' } /** * Request Configuration * We manipulate these two request objects below * to acquire a token with the appropriate claims. */ const authCodeRequest = { redirectUri: confidentialClientConfig.auth.redirectUri, }; const tokenRequest = { redirectUri: confidentialClientConfig.auth.redirectUri, }; /** * Using express-session middleware. Be sure to familiarize yourself with available options * and set them as desired. Visit: https://www.npmjs.com/package/express-session */ const sessionConfig = { secret: process.env.SESSION_SECRET, resave: false, saveUninitialized: false, cookie: { secure: false, // set this to true on production } }
APP_STATES
: 要求にタグを付けて、Azure AD B2C から受信した応答を区別するために使用されます。 Azure AD B2C に送信される任意の数の要求に対するリダイレクト URI は 1つだけです。authCodeRequest
: 承認コードを取得するために使用される構成オブジェクト。tokenRequest
: 承認コードによってトークンを取得するために使用される構成オブジェクト。sessionConfig
: 簡易セッションの構成オブジェクト。
ビュー テンプレート エンジンと 簡易セッション構成を設定するには、
index.js
ファイルに次のコードを追加します。//Create an express instance const app = express(); //Set handlebars as your view engine app.engine('.hbs', engine({extname: '.hbs'})); app.set('view engine', '.hbs'); app.set("views", "./views"); //usse session configuration app.use(session(sessionConfig));
手順 5: 高速ルートを追加する
アプリ ルートを追加する前に、承認コード URL を取得するロジックを追加します。これは、承認コード付与フローの最初のステップです。 index.js
ファイルに、次のコードを追加します。
/**
* This method is used to generate an auth code request
* @param {string} authority: the authority to request the auth code from
* @param {array} scopes: scopes to request the auth code for
* @param {string} state: state of the application
* @param {Object} res: express middleware response object
*/
const getAuthCode = (authority, scopes, state, res) => {
// prepare the request
console.log("Fetching Authorization code")
authCodeRequest.authority = authority;
authCodeRequest.scopes = scopes;
authCodeRequest.state = state;
//Each time you fetch Authorization code, update the relevant authority in the tokenRequest configuration
tokenRequest.authority = authority;
// request an authorization code to exchange for a token
return confidentialClientApplication.getAuthCodeUrl(authCodeRequest)
.then((response) => {
console.log("\nAuthCodeURL: \n" + response);
//redirect to the auth code URL/send code to
res.redirect(response);
})
.catch((error) => {
res.status(500).send(error);
});
}
authCodeRequest
オブジェクトには redirectUri
、 authority
、 scopes
、 および state
のプロパティがあります。 オブジェクトは、パラメーターとして getAuthCodeUrl
メソッドに渡されます。
index.js
ファイルに、次のコードを追加します。
app.get('/', (req, res) => {
res.render('signin', { showSignInButton: true });
});
app.get('/signin',(req, res)=>{
//Initiate a Auth Code Flow >> for sign in
//no scopes passed. openid, profile and offline_access will be used by default.
getAuthCode(process.env.SIGN_UP_SIGN_IN_POLICY_AUTHORITY, [], APP_STATES.LOGIN, res);
});
/**
* Change password end point
*/
app.get('/password',(req, res)=>{
getAuthCode(process.env.RESET_PASSWORD_POLICY_AUTHORITY, [], APP_STATES.PASSWORD_RESET, res);
});
/**
* Edit profile end point
*/
app.get('/profile',(req, res)=>{
getAuthCode(process.env.EDIT_PROFILE_POLICY_AUTHORITY, [], APP_STATES.EDIT_PROFILE, res);
});
/**
* Sign out end point
*/
app.get('/signout',async (req, res)=>{
logoutUri = process.env.LOGOUT_ENDPOINT;
req.session.destroy(() => {
//When session destruction succeeds, notify B2C service using the logout uri.
res.redirect(logoutUri);
});
});
app.get('/redirect',(req, res)=>{
//determine the reason why the request was sent by checking the state
if (req.query.state === APP_STATES.LOGIN) {
//prepare the request for authentication
tokenRequest.code = req.query.code;
confidentialClientApplication.acquireTokenByCode(tokenRequest).then((response)=>{
req.session.sessionParams = {user: response.account, idToken: response.idToken};
console.log("\nAuthToken: \n" + JSON.stringify(response));
res.render('signin',{showSignInButton: false, givenName: response.account.idTokenClaims.given_name});
}).catch((error)=>{
console.log("\nErrorAtLogin: \n" + error);
});
}else if (req.query.state === APP_STATES.PASSWORD_RESET) {
//If the query string has a error param
if (req.query.error) {
//and if the error_description contains AADB2C90091 error code
//Means user selected the Cancel button on the password reset experience
if (JSON.stringify(req.query.error_description).includes('AADB2C90091')) {
//Send the user home with some message
//But always check if your session still exists
res.render('signin', {showSignInButton: false, givenName: req.session.sessionParams.user.idTokenClaims.given_name, message: 'User has cancelled the operation'});
}
}else{
res.render('signin', {showSignInButton: false, givenName: req.session.sessionParams.user.idTokenClaims.given_name});
}
}else if (req.query.state === APP_STATES.EDIT_PROFILE){
tokenRequest.scopes = [];
tokenRequest.code = req.query.code;
//Request token with claims, including the name that was updated.
confidentialClientApplication.acquireTokenByCode(tokenRequest).then((response)=>{
req.session.sessionParams = {user: response.account, idToken: response.idToken};
console.log("\AuthToken: \n" + JSON.stringify(response));
res.render('signin',{showSignInButton: false, givenName: response.account.idTokenClaims.given_name});
}).catch((error)=>{
//Handle error
});
}else{
res.status(500).send('We do not recognize this response!');
}
});
高速ルートは次のとおりです。
/
:- Web アプリを入力するために使用されます。
signin
ページをレンダリングします。
/signin
:- サインイン時に使用されます。
getAuthCode()
メソッドを呼び出し、サインインとサインアップ ユーザー フロー/ポリシー、APP_STATES.LOGIN
、 および空のscopes
配列をメソッドに渡します。authority
- 必要に応じて、資格情報を入力する必要があります。 アカウントを持ってない場合は、サインアップを求めるメッセージが表示されます。
- このルートからの最終的な結果には Azure AD B2C から
/redirect
ルートにポストバックされる承認コードが含まれます。
/password
:- パスワードのリセット時に使用されます。
getAuthCode()
メソッドを呼び出し、 パスワードリセットの ユーザーフロー/ポリシー、APP_STATES.PASSWORD_RESET
、 および空のscopes
配列をメソッドに渡します。authority
- これにより、パスワードのリセット エクスペリエンスを使用してパスワードを変更するか、操作を取り消すことができます。
- このルートからの最終的な結果には Azure AD B2C から
/redirect
ルートにポストバックされる承認コードが含まれます。 操作を取り消した場合は、エラーがポストバックされます。
/profile
:- プロファイルの更新時に使用されます。
getAuthCode()
メソッドを呼び出し、 プロファイル編集の ユーザーフロー/ポリシー、APP_STATES.EDIT_PROFILE
、 および空のscopes
配列をメソッドに渡しますauthority
。- これにより、プロファイルを更新し、プロファイル編集エクスペリエンスを使用することができます。
- このルートからの最終的な結果には Azure AD B2C から
/redirect
ルートにポストバックされる承認コードが含まれます。
/signout
:- サインアウト時に使用されます。
- Web アプリによってセッションがクリアされ、Azure AD B2C サインアウト エンドポイントへの HTTP 呼び出しが行われます。
/redirect
:- Azure portal の Web アプリのリダイレクト URI として設定されたルートです。
- Azure AD B2C からの要求で
state
クエリ パラメーターを使用して、Web アプリから行われた要求を区別します。 Azure AD B2C からのすべてのリダイレクトを処理します。ただし、サインアウトは除きます。 - アプリの状態が
APP_STATES.LOGIN
の場合は、取得された承認コードを使用してacquireTokenByCode()
メソッドからトークンを取得します。 このトークンには、ユーザーの識別に使用されるidToken
およびidTokenClaims
が含まれます。 - アプリの状態が
APP_STATES.PASSWORD_RESET
の場合は、user cancelled the operation
などのエラーが処理されます。AADB2C90091
エラー コードは、このエラーを識別します。 それ以外の場合は、次のユーザーエクスペリエンスを決定します。 - アプリの状態が
APP_STATES.EDIT_PROFILE
の場合、承認コードを使用してトークンを取得します。 トークンには、新しい変更を含むidTokenClaims
が含まれてい ます。
手順 6: Node サーバーを起動する
ノードサーバーを起動するには、 index.js
ファイルに次のコードを追加します。
app.listen(process.env.SERVER_PORT, () => {
console.log(`Msal Node Auth Code Sample app listening on port !` + process.env.SERVER_PORT);
});
index.js
ファイルに必要なすべての変更を行うと、次のファイルのようになります。
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
//<ms_docref_use_app_dependencies>
require('dotenv').config();
const express = require('express');
const session = require('express-session');
const {engine} = require('express-handlebars');
const msal = require('@azure/msal-node');
//</ms_docref_use_app_dependencies>
//<ms_docref_configure_msal>
/**
* Confidential Client Application Configuration
*/
const confidentialClientConfig = {
auth: {
clientId: process.env.APP_CLIENT_ID,
authority: process.env.SIGN_UP_SIGN_IN_POLICY_AUTHORITY,
clientSecret: process.env.APP_CLIENT_SECRET,
knownAuthorities: [process.env.AUTHORITY_DOMAIN], //This must be an array
redirectUri: process.env.APP_REDIRECT_URI,
validateAuthority: false
},
system: {
loggerOptions: {
loggerCallback(loglevel, message, containsPii) {
console.log(message);
},
piiLoggingEnabled: false,
logLevel: msal.LogLevel.Verbose,
}
}
};
// Initialize MSAL Node
const confidentialClientApplication = new msal.ConfidentialClientApplication(confidentialClientConfig);
//</ms_docref_configure_msal>
//<ms_docref_global_variable>
/**
* The MSAL.js library allows you to pass your custom state as state parameter in the Request object
* By default, MSAL.js passes a randomly generated unique state parameter value in the authentication requests.
* The state parameter can also be used to encode information of the app's state before redirect.
* You can pass the user's state in the app, such as the page or view they were on, as input to this parameter.
* For more information, visit: https://docs.microsoft.com/azure/active-directory/develop/msal-js-pass-custom-state-authentication-request
* In this scenario, the states also serve to show the action that was requested of B2C since only one redirect URL is possible.
*/
const APP_STATES = {
LOGIN: 'login',
LOGOUT: 'logout',
PASSWORD_RESET: 'password_reset',
EDIT_PROFILE : 'update_profile'
}
/**
* Request Configuration
* We manipulate these two request objects below
* to acquire a token with the appropriate claims.
*/
const authCodeRequest = {
redirectUri: confidentialClientConfig.auth.redirectUri,
};
const tokenRequest = {
redirectUri: confidentialClientConfig.auth.redirectUri,
};
/**
* Using express-session middleware. Be sure to familiarize yourself with available options
* and set them as desired. Visit: https://www.npmjs.com/package/express-session
*/
const sessionConfig = {
secret: process.env.SESSION_SECRET,
resave: false,
saveUninitialized: false,
cookie: {
secure: false, // set this to true on production
}
}
//</ms_docref_global_variable>
//<ms_docref_view_tepmplate_engine>
//Create an express instance
const app = express();
//Set handlebars as your view engine
app.engine('.hbs', engine({extname: '.hbs'}));
app.set('view engine', '.hbs');
app.set("views", "./views");
//usse session configuration
app.use(session(sessionConfig));
//</ms_docref_view_tepmplate_engine>
//<ms_docref_authorization_code_url>
/**
* This method is used to generate an auth code request
* @param {string} authority: the authority to request the auth code from
* @param {array} scopes: scopes to request the auth code for
* @param {string} state: state of the application
* @param {Object} res: express middleware response object
*/
const getAuthCode = (authority, scopes, state, res) => {
// prepare the request
console.log("Fetching Authorization code")
authCodeRequest.authority = authority;
authCodeRequest.scopes = scopes;
authCodeRequest.state = state;
//Each time you fetch Authorization code, update the relevant authority in the tokenRequest configuration
tokenRequest.authority = authority;
// request an authorization code to exchange for a token
return confidentialClientApplication.getAuthCodeUrl(authCodeRequest)
.then((response) => {
console.log("\nAuthCodeURL: \n" + response);
//redirect to the auth code URL/send code to
res.redirect(response);
})
.catch((error) => {
res.status(500).send(error);
});
}
//</ms_docref_authorization_code_url>
//<ms_docref_app_endpoints>
app.get('/', (req, res) => {
res.render('signin', { showSignInButton: true });
});
app.get('/signin',(req, res)=>{
//Initiate a Auth Code Flow >> for sign in
//no scopes passed. openid, profile and offline_access will be used by default.
getAuthCode(process.env.SIGN_UP_SIGN_IN_POLICY_AUTHORITY, [], APP_STATES.LOGIN, res);
});
/**
* Change password end point
*/
app.get('/password',(req, res)=>{
getAuthCode(process.env.RESET_PASSWORD_POLICY_AUTHORITY, [], APP_STATES.PASSWORD_RESET, res);
});
/**
* Edit profile end point
*/
app.get('/profile',(req, res)=>{
getAuthCode(process.env.EDIT_PROFILE_POLICY_AUTHORITY, [], APP_STATES.EDIT_PROFILE, res);
});
/**
* Sign out end point
*/
app.get('/signout',async (req, res)=>{
logoutUri = process.env.LOGOUT_ENDPOINT;
req.session.destroy(() => {
//When session destruction succeeds, notify B2C service using the logout uri.
res.redirect(logoutUri);
});
});
app.get('/redirect',(req, res)=>{
//determine the reason why the request was sent by checking the state
if (req.query.state === APP_STATES.LOGIN) {
//prepare the request for authentication
tokenRequest.code = req.query.code;
confidentialClientApplication.acquireTokenByCode(tokenRequest).then((response)=>{
req.session.sessionParams = {user: response.account, idToken: response.idToken};
console.log("\nAuthToken: \n" + JSON.stringify(response));
res.render('signin',{showSignInButton: false, givenName: response.account.idTokenClaims.given_name});
}).catch((error)=>{
console.log("\nErrorAtLogin: \n" + error);
});
}else if (req.query.state === APP_STATES.PASSWORD_RESET) {
//If the query string has a error param
if (req.query.error) {
//and if the error_description contains AADB2C90091 error code
//Means user selected the Cancel button on the password reset experience
if (JSON.stringify(req.query.error_description).includes('AADB2C90091')) {
//Send the user home with some message
//But always check if your session still exists
res.render('signin', {showSignInButton: false, givenName: req.session.sessionParams.user.idTokenClaims.given_name, message: 'User has cancelled the operation'});
}
}else{
res.render('signin', {showSignInButton: false, givenName: req.session.sessionParams.user.idTokenClaims.given_name});
}
}else if (req.query.state === APP_STATES.EDIT_PROFILE){
tokenRequest.scopes = [];
tokenRequest.code = req.query.code;
//Request token with claims, including the name that was updated.
confidentialClientApplication.acquireTokenByCode(tokenRequest).then((response)=>{
req.session.sessionParams = {user: response.account, idToken: response.idToken};
console.log("\AuthToken: \n" + JSON.stringify(response));
res.render('signin',{showSignInButton: false, givenName: response.account.idTokenClaims.given_name});
}).catch((error)=>{
//Handle error
});
}else{
res.status(500).send('We do not recognize this response!');
}
});
//</ms_docref_app_endpoints>
//start app server to listen on set port
//<ms_docref_start_node_server>
app.listen(process.env.SERVER_PORT, () => {
console.log(`Msal Node Auth Code Sample app listening on port !` + process.env.SERVER_PORT);
});
//</ms_docref_start_node_server>
手順 7: Web アプリを実行する
Web アプリの実行に関するページの手順に従って、Node.js Web アプリをテストします。