こんにちは、富士榮です。
前回に引き続きEntra IDの外部認証を試していきます。
こちらのドキュメントを見つつ設定をしていきますが、なかなかハマりポイントがあります。ちなみに外部認証は細かい動きを見るためにも自前のIdPを使っていきます。
ハマりポイントだけ先に書いておきます。
- jwks_uriエンドポイントに公開するjwk(id_tokenの署名検証するための鍵)はx5cを含む必要がある
- id_tokenのJWTヘッダのメディアタイプは大文字"JWT”を指定する必要がある(昨日のポストの通り)
- 外部認証プロバイダのdiscoveryとjwks_uriの情報をEntra ID側がキャッシュをするので変更があっても24時間は読みにきてくれない
- 外部認証プロバイダから返却するid_tokenの中のamrは配列で返す必要はあるが、返す値は単一である必要がある
- response_typeはid_token、reponse_modeはform_postで認証レスポンスを返却する必要がある(まぁ、この辺はいつものMicrosoftですね)
- 外部認証プロバイダの認可エンドポイントへ各種パラメータがPOSTされてくる(こちらは前回書いた通り)
とりあえず、こんな感じで動きます。ngrokでローカルで動かしているIdPを読みにこさせているので画面左側にtrafic inspectorを出しています。Entra IDでログインする際にリクエストが来ているのがわかります。
外部認証プロバイダの認可エンドポイントへPOSTされてくるデータなどをtrafic inspectorで確認しながら実装を進めていくのが良いと思います。
ということで、外部認証プロバイダを実装していきます。
まずは認可エンドポイントです。
こちらが認可エンドポイントへ投げ込まれてくるパラメータですので、こちらに対応する形でid_tokenを発行して返してあげれば良いはずです。
パラメータ | 値 |
scope | openid |
response_mode | id_token |
client_id | 外部認証プロバイダ側にEntra IDをRPとして登録した際のclient_idの値 |
redirect_uri | https://login.microsoftonline.com/common/federation/externalauthprovider |
claims | {"id_token":{"amr":{"essential":true,"values":["face","fido","fpt","hwk","iris","otp","tel","pop","retina","sc","sms","swk","vbm"]},"acr":{"essential":true,"values":["possessionorinherence"]}}}' |
nonce | Entra IDが払い出すnonceの値 |
id_token_hint | Entra ID側で認証済みのユーザに関する情報 |
client-request-id | Entra ID側でのトラッキングに使う識別子(サポート用) |
state | Entra ID側が払い出すstateの値 |
最終ゴールはid_tokenを生成し、リクエスト内のstateと合わせてredirect_uriへPOSTしてあげることとなります。
こんな感じでエンドポイントを作っていきましょう。
// 認可エンドポイント(POST)
router.post("/authorize", async (req, res) => {
// Todo
// - redirect_uriが登録済みでEntra IDから提供されている規定値(https://login.microsoftonline.com/common/federation/externalauthprovider)であることの検証
// - client_idがEntra IDに割り当てたものであることの検証
// - id_token_hintの署名等の検証
// - ユーザ認証(id_token_hintに含まれるoid/tidを使ってユーザとの紐付け)
// - 認証応答を行う
// - redirect_uriへPOSTする
// - id_token
// - state : リクエストに含まれるstate(存在する場合)
// - id_tokenの中身
// - iss : idpのopenid-configurationで公開されているものと一致すること
// - aud : Entra IDに割り当てたclient_id
// - exp : 有効期限
// - iat : 発行時刻
// - sub : id_token_hintのsubと一致すること
// - nonce : リクエストに含まれるnonce
// - acr : リクエストのclaimsに含まれる値の一つと一致すること
// - amr : リクエストのclaimsに含まれる値と一致すること(配列)
結構やることはありますが、今回はまずはid_tokenを発行するところにフォーカスを当てますので、ユーザの認証や各種パラメータの検証は省略します。
id_tokenに含めるべき値にリクエスト内のid_token_hintに含まれる情報があるので、まずばid_token_hintのpayloadをデコードして値を取り出せる状態にパースします。
// とりあえずpayloadだけパースする(検証は後回し)
const id_token_hint_payload = req.body.id_token_hint.split('.');
const raw_id_token_hint = base64url.decode(id_token_hint_payload[1]);
const obj_id_token_hint = JSON.parse(raw_id_token_hint);
そしてid_tokenのpayloadを生成します。今回は検証もユーザ認証もしないので、acrやamrの値も決め打ちで設定しています。ちなみにハマりポイントにも記載した通り、amrは配列で値を設定する必要がありますが、値は単一でなければなりません。(今回はfidoを設定)
const date = new Date();
const raw_id_token = {
iss: 'https://' + req.headers.host,
aud: req.body.client_id,
exp: Math.floor((date.getTime() + (1000 * 60 * 10)) / 1000),
iat: Math.floor(date.getTime() / 1000),
sub: obj_id_token_hint.sub,
nonce: req.body.nonce,
acr: 'possessionorinherence',
amr: ['fido']
};
そして、このpayloadを署名してJWTと作ります。
const id_token = await utils.generateJWS(raw_id_token);
こちらも面倒だったので、opensslで作った秘密鍵をベタ打ちでコードに埋め込んでいますし、kidも生成したものをベタで指定しています。この辺はおいおい直します。
const jwt = require('jsonwebtoken');
const privatekey = `-----BEGIN RSA PRIVATE KEY-----
MIIEowIBAAKCAQEAmbjCIt20NKwMrH78TGOA8w9LS/R6B81RNHoJ/J+7UljRUfMN
sGC+RR6bqtDxeLgLjmt4s7CccbVf380DQx4b2XtCvoP0QW4GsJm7b13XkwCD6fWT
xSX5RTqXyTrLFk0ifOhRZ09QxZnAOGgUN12HeKjWj24XLQKFOROi8BhwHxLLSd+n
-- 省略 --
52EKdTgNOrVFGV/1qACGuDgLNDss+z2f2HgO/pk5UtS0EaXltv62IV6izkZh7f9O
aPXrS0BclfaGBZ0RcQIt0lJ2UMSTd8CFKX+k5efoFthX2ddWY24A
-----END RSA PRIVATE KEY-----`
exports.sign = async function(payload){
return jwt.sign(payload, privatekey, {
algorithm: "RS256",
keyid: "Vzy3LDbuzSrt0cQldElZp5R92etQvOCENEu5aOOppYs"
});
}
あとはid_tokenとstateをPOSTしてあげるだけですが、node+express+ejsを使っているのでres.renderで値をejsへ渡してあげます。
// form postするためのページをレンダリング
res.render("./form_post.ejs",
{
redirect_uri : req.body.redirect_uri,
id_token: id_token,
state: req.body.state
}
);
フォーム側はこんな感じです。実際は値はhiddenにして、JavaScriptで自動POSTするようにしますが、今回はステップバイステップで進めたかったので一旦フォームを表示するようにしています。
<html>
<body>
<div id="login_div" >
<form name="id_token" method="POST" action="<%= redirect_uri%>">
<input name="id_token" type="text" value="<%= id_token%>">
<input name="state" type="text" value="<%= state%>">
<input type="submit" value="POST">
</form>
</div>
</body>
</html>
これでうまくいけば上記の動画のように認証が完了します。
一旦、今回はここまでです。
0 件のコメント:
コメントを投稿