2024年1月12日金曜日

OpenID Providerを作る)UserInfoエンドポイントを作る

こんにちは、富士榮です。

OpenID Connectを理解するにはOpenID Providerを作るのが一番、ということで各種エンドポイントを実装してきていますが、一応今回で基本的な部分はおしまいです。

その前にこれまでのポストはこちらです。


今回はUserInfoエンドポイントなので④が対象です。
なお、本日紹介する部分のコードはこちらで公開しています。

ということで始めていきましょう。

UserInfoエンドポイントの役割と処理

前回までの処理でOpenID Connectの最大の目標であるIDトークンをクライアント(Relying Party)に発行する、という処理は終わっていますので、今回のUserInfoエンドポイントについては必ずしも実装する必要はありません。
UserInfoエンドポイントはユーザの属性情報を提供するための標準化されたAPIで、OpenID Provider(のベースとなるOAuth2.0の認可サーバ)の保護対象リソースとなります。そのためOpenID ProviderからIDトークンに加えてアクセストークンの発行を受ける必要があり、クライアントはアクセストークンをAuthorizationヘッダに付加した状態でUserInfoエンドポイントへアクセスすることでユーザの属性情報を取得します。

なお、OpenID Connectではユーザ情報をIDトークンの中に含めることも可能なので、UserInfoエンドポイントとの使い分けをどうするのか?はID基盤全体としての設計上のポイントとなります。
IDトークンはユーザ認証のイベントに連動して発行される一方でUserInfoは認証イベントとは非同期で(様するに後からでも)ユーザ情報を取得することができるのでクライアントの利用シーンによって使い分けることが重要です。例えばモバイルアプリケーションなどは毎回ログインするわけではないのでユーザの属性情報を取得するのにIDトークンの利用はできませんが、リフレッシュトークンを使ってバックエンドでアクセストークンを更新しつつUserInfoエンドポイントへアクセスすることでログインとは連動せずに最新のユーザ情報を取得することが可能となります。
IDトークンUserInfoエンドポイント
認証との連動性同期非同期
情報の鮮度認証時点API実行時点

なお、今回は実装を簡素化するためにバックエンドなしの構成としており、アクセストークンの情報を元にUserInfoエンドポイントからの情報返却をするため上述の使い方はできません。この辺りは今後の拡張としていきたいと思います。

UserInfoエンドポイントの実装

実装すべき仕様はこちらに記載されています。
先に述べた通り簡易実装のためバックエンドでユーザDBを検索して属性を取得して、、という部分は実装せずにIDトークンと同じ内容をアクセストークンに入れておき、トークンの検証と中身の取り出しを行うことでユーザ情報を返却するAPIとして実装しています。

こちらがコードとなります。
// userInfoエンドポイント
router.get("/userinfo", async (req, res) => {
const access_token = req.headers.authorization.replace("Bearer ", "");
const decodedPayload = await utils.verifyJWS(access_token);
const decodedTokenJSON = JSON.parse(decodedPayload);
// 有効期限確認
const date = new Date();
if(decodedTokenJSON.exp < (Math.floor(date.getTime() / 1000))){
res.statusCode = 403;
res.json({
errorMessage: "access token is expired."
});
} else {
// 不要な要素を削除する
delete decodedTokenJSON.iss;
delete decodedTokenJSON.aud;
delete decodedTokenJSON.exp;
delete decodedTokenJSON.iat;
res.json(decodedTokenJSON);
}
});

処理としては、まずアクセストークンの署名検証と有効期限の検証を行います。
const access_token = req.headers.authorization.replace("Bearer ", "");
const decodedPayload = await utils.verifyJWS(access_token);
const decodedTokenJSON = JSON.parse(decodedPayload);
// 有効期限確認
const date = new Date();
if(decodedTokenJSON.exp < (Math.floor(date.getTime() / 1000))){
res.statusCode = 403;
res.json({
errorMessage: "access token is expired."
});

アクセストークンは通常Authorizationヘッダに「Bearer xxxx」という形でAPIに送信されることが多いので今回もヘッダからアクセストークンを取り出しています。その上でJWSの署名検証を前回までと同様にnode-joseのライブラリを使って実施しています。
その上で、トークンのexpの値と現在時刻を比較して有効期限が切れていないことの確認を行なっています。

検証に成功したら、アクセストークン(今回はIDトークンと同じものを使うEntra方式を取ります)から不要な属性(iss、aud、exp、iat)を取り除いた上でJSONとしてクライアントへ返却しています。
} else {
// 不要な要素を削除する
delete decodedTokenJSON.iss;
delete decodedTokenJSON.aud;
delete decodedTokenJSON.exp;
delete decodedTokenJSON.iat;
res.json(decodedTokenJSON);
}
});


PostmanでUserInfoエンドポイントを叩くとこんな感じで値が返ります。

ということでUserInfoエンドポイントについても実装ができました。
これで最低限のプロトコルの流れは実装できましたので、次回以降はこれまで実装を飛ばした部分を確認しながら実装していきたいと思います。


0 件のコメント: