前回まではEntra ID周りの設定を行いました。
次はアプリケーション側で実際に利用してみたいと思います。
今回は単純にログインボタンを表示する画面とログインが完了した後の画面の2つを作成し、
実際に認証・認可を行ってみたいと思います。
(今回利用するユーザーは前回作ったテナント内に所属しています)
まずはログインボタンを表示する画面から簡単に作成します。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Entra ID ログイン</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div class="container">
<a href="/login" class="login-button">ログイン</a>
</div>
</body>
</html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Entra ID ログイン</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div class="container">
<a href="/login" class="login-button">ログイン</a>
</div>
</body>
</html>
こんな感じのボタンだけの画面。
別ファイルでJavasciptファイルを定義して、ログインボタンを押下した後の処理を記載します。
今回は「PKCE(コードインジェクションを防ぐ仕組み)」を使って認証・認可を行っていたのでそのやり方を記載します。
生成したコードチャレンジをauthorizeのAPIに渡す形になります。
// MSAL設定
const msalConfig = {
auth: {
clientId: process.env.CLIENT_ID,
authority: `https://login.microsoftonline.com/${process.env.TENANT_ID}`,
clientSecret: process.env.CLIENT_SECRET,
}
};
// MSALクライアントの作成
const msal = new msalNode.ConfidentialClientApplication(msalConfig);
// リダイレクトURLとスコープ
const redirectUri = process.env.REDIRECT_URI || "http://localhost:3000/redirect";
const scopes = ["User.Read"];
// ログイン
app.get('/login', (req, res) => {
const cryptoProvider = new msalNode.CryptoProvider();
// PKEC認証用 challengeはoauth2/v2.0/authorizeで利用
const { verifier, challenge } = cryptoProvider.generatePkceCodes();
const authUrlParams = {
scopes: scopes,
redirectUri: redirectUri,
codeChallenge: challenge,
codeChallengeMethod: "S256"
};
// verifierはoauth2/v2.0/tokenで利用
req.session.pkce = { verifier };
// oauth2/v2.0/authorize を発行
msal.getAuthCodeUrl(authUrlParams)
.then((url) => {
res.redirect(url);
})
.catch((error) => {
res.status(500).send('認証URLの生成に失敗しました');
});
});
const msalConfig = {
auth: {
clientId: process.env.CLIENT_ID,
authority: `https://login.microsoftonline.com/${process.env.TENANT_ID}`,
clientSecret: process.env.CLIENT_SECRET,
}
};
// MSALクライアントの作成
const msal = new msalNode.ConfidentialClientApplication(msalConfig);
// リダイレクトURLとスコープ
const redirectUri = process.env.REDIRECT_URI || "http://localhost:3000/redirect";
const scopes = ["User.Read"];
// ログイン
app.get('/login', (req, res) => {
const cryptoProvider = new msalNode.CryptoProvider();
// PKEC認証用 challengeはoauth2/v2.0/authorizeで利用
const { verifier, challenge } = cryptoProvider.generatePkceCodes();
const authUrlParams = {
scopes: scopes,
redirectUri: redirectUri,
codeChallenge: challenge,
codeChallengeMethod: "S256"
};
// verifierはoauth2/v2.0/tokenで利用
req.session.pkce = { verifier };
// oauth2/v2.0/authorize を発行
msal.getAuthCodeUrl(authUrlParams)
.then((url) => {
res.redirect(url);
})
.catch((error) => {
res.status(500).send('認証URLの生成に失敗しました');
});
});
authorizeのAPIから認可コードが返却されるので、そのコードを使ってアクセストークンを取得。
その際にログイン前に生成したコードベリファイアも一緒に渡します。
⇒ 認可サーバー(Entra ID)側で検証を行い問題なければアクセストークンが返却されます。
// リダイレクト処理
app.get('/redirect', async (req, res) => {
if (req.query.error) {
return res.status(500).send(`認証エラー: ${req.query.error}`);
}
// verifierを取得
const verifier = req.session.pkce.verifier;
try {
// oauth2/v2.0/token を発行
const tokenResponse = await msal.acquireTokenByCode({
code: req.query.code,
scopes: scopes,
redirectUri: redirectUri,
codeVerifier: verifier
});
// とりあえず次の画面に取得したアクセストークンを渡す
res.redirect(`/success.html?token=${encodeURIComponent(tokenResponse.accessToken)}`);
} catch (error) {
res.status(500).send('トークンの取得に失敗しました');
}
});
app.get('/redirect', async (req, res) => {
if (req.query.error) {
return res.status(500).send(`認証エラー: ${req.query.error}`);
}
// verifierを取得
const verifier = req.session.pkce.verifier;
try {
// oauth2/v2.0/token を発行
const tokenResponse = await msal.acquireTokenByCode({
code: req.query.code,
scopes: scopes,
redirectUri: redirectUri,
codeVerifier: verifier
});
// とりあえず次の画面に取得したアクセストークンを渡す
res.redirect(`/success.html?token=${encodeURIComponent(tokenResponse.accessToken)}`);
} catch (error) {
res.status(500).send('トークンの取得に失敗しました');
}
});
完了画面では先ほど返却されたアクセストークンを使って、Microsoft GraphのAPIをコールしてみたいと思います。
ログインしたユーザー自身の情報を取得します。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ログイン成功</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div class="container">
<h1>ログイン成功</h1>
<div id="user-info">
<p>読み込み中...</p>
</div>
<a href="/" class="button">ログインに戻る</a>
</div>
<script src="profile.js"></script>
</body>
</html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ログイン成功</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div class="container">
<h1>ログイン成功</h1>
<div id="user-info">
<p>読み込み中...</p>
</div>
<a href="/" class="button">ログインに戻る</a>
</div>
<script src="profile.js"></script>
</body>
</html>
完了画面表示時のJavascript
document.addEventListener('DOMContentLoaded', function() {
// URLからトークンを取得
const urlParams = new URLSearchParams(window.location.search);
const token = urlParams.get('token');
if (!token) {
document.getElementById('user-info').innerHTML = 'トークンがありません';
return;
}
// ユーザー情報を取得
fetch('/api/me', {
headers: {
'Authorization': `Bearer ${token}`
}
})
.then(response => {
if (!response.ok) {
throw new Error('ユーザー情報の取得に失敗');
}
return response.json();
})
.then(user => {
// ユーザー情報を表示
const userInfoHtml = `
名前:
${user.displayName}
メール:
${user.mail || user.userPrincipalName}
`;
document.getElementById('user-info').innerHTML = userInfoHtml;
})
.catch(error => {
document.getElementById('user-info').innerHTML = `エラー: ${error.message}`;
});
});
// ユーザー情報取得API
app.get('/api/me', async (req, res) => {
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res.status(401).send('認証が必要です');
}
const token = authHeader.split(' ')[1];
try {
// Microsoft Graphクライアント初期化
const client = graph.Client.init({
authProvider: (done) => {
done(null, token);
}
});
// Microsoft GraphAPIでユーザー情報を取得
const user = await client.api('/me').select('displayName,mail,userPrincipalName').get();
res.json(user);
} catch (error) {
res.status(500).send('ユーザー情報の取得に失敗しました');
}
});
// URLからトークンを取得
const urlParams = new URLSearchParams(window.location.search);
const token = urlParams.get('token');
if (!token) {
document.getElementById('user-info').innerHTML = 'トークンがありません';
return;
}
// ユーザー情報を取得
fetch('/api/me', {
headers: {
'Authorization': `Bearer ${token}`
}
})
.then(response => {
if (!response.ok) {
throw new Error('ユーザー情報の取得に失敗');
}
return response.json();
})
.then(user => {
// ユーザー情報を表示
const userInfoHtml = `
名前:
${user.displayName}
メール:
${user.mail || user.userPrincipalName}
`;
document.getElementById('user-info').innerHTML = userInfoHtml;
})
.catch(error => {
document.getElementById('user-info').innerHTML = `エラー: ${error.message}`;
});
});
// ユーザー情報取得API
app.get('/api/me', async (req, res) => {
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res.status(401).send('認証が必要です');
}
const token = authHeader.split(' ')[1];
try {
// Microsoft Graphクライアント初期化
const client = graph.Client.init({
authProvider: (done) => {
done(null, token);
}
});
// Microsoft GraphAPIでユーザー情報を取得
const user = await client.api('/me').select('displayName,mail,userPrincipalName').get();
res.json(user);
} catch (error) {
res.status(500).send('ユーザー情報の取得に失敗しました');
}
});
完了画面ではログインしたユーザーの情報を取得し表示することが出来ました。
今回はログインまでをとりあえず行いたかったので簡単に記載させていただきましたが、
scopeパラメータの値を変えることで取得できる情報も変わってくるようなので、もう少し調べてみますか。
今回はここで終了です、ありがとうございました!