2024年1月31日水曜日

OpenID Providerを作る)ユーザ情報をデータベースから取得する

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

まだまだ実装すべき点はたくさんありますが、そろそろユーザを固定で埋め込むのではなくデータベースに保存されたユーザ情報を元にIDトークンなどを生成していきたいと思います。

ただデータベースと言っても、前回紹介したJSONBin.ioを使います。


その前にこれまでのおさらいです。


実装する内容

ユーザ情報を取得する、と言ってもまだユーザ認証画面などを作るところまでは手を出しませんので、これまで固定で埋め込んでいたユーザ情報を止めるところからです。

とりあえずはユーザのログインID(preferred_username)を指定するとJSONBinからユーザ情報を取得してくる仕組みを作り、ハードコード部分を少しだけ減らしていきたいと思います。

今回使うJSONBinのAPIは以下の2つです。

    • コレクションに入っているBinの一覧を取得するAPI
    • 最初の10個のBinを取得してくるので、本来は必要に応じてページングをしなければなりませんが、今回は10ユーザも作らないのでページングの考慮はしません
    • APIの仕様としては「https://api.jsonbin.io/v3/c/{コレクションID]/bins」をGETするだけです
    • 結果、Binの一覧がこんな感じで返却されますので、この中でsnippetMeta.nameがpreferred_usernameと一致している要素のrecordの値を持つbinの中にお目当てのユーザの情報が入っている、という仕掛けです。※このsnippetMeta.nameにユーザ名を入れるためにbinを作る際のname指定をしていたわけです

[
{
"private": true,
"snippetMeta": {
"name": "test2@example.jp"
},
"record": "65b4dfcfdc746540189c4daf",
"createdAt": "2024-01-27T10:49:51.572Z"
},
{
"private": true,
"snippetMeta": {
"name": "test@example.jp"
},
"record": "65b4dacd266cfc3fde81ca50",
"createdAt": "2024-01-27T10:28:29.692Z"
}
]
  • Read a Bin
    • 単体のBinの中身を読み取るAPI
    • 仕様としては「https://api.jsonbin.io/v3/b/{BinのID}」をGETするだけです
    • 結果、指定したBinの中身がこんな感じで返却されてきます
{
"record": {
"sub": "test",
"name": "taro test",
"given_name": "taro",
"family_name": "test",
"middle_name": "",
"nickname": "",
"preferred_username": "test@example.jp",
"profile": "https://twitter.com/phr_eidentity",
"picture": "https://1.gravatar.com/avatar/25eee85430bd0bbdcb9cff75655afa43cc9f69bc8730aec852d8538179646ef1",
"website": "hhtps://idmlab.eidentity.jp",
"gender": "male",
"birthdate": "1900-01-01",
"zoneinfo": "Japan",
"locale": "jp_JP",
"updated_at": 1704034800,
"email": "test@example.jp",
"email_verified": true,
"address": {
"formatted": "Kokyogaien, Chiyoda-ku, Tokyo 1000002 JAPAN",
"street_address": "Kokyogaien",
"locality": "Chiyoda-ku",
"region": "Tokyo",
"postal_code": "1000002",
"country": "JP"
},
"phone_number": "+81-3-1234-5678",
"phone_number_verified": true
},
"metadata": {
"id": "65b4dacd266cfc3fde81ca50",
"private": true,
"createdAt": "2024-01-27T10:28:29.692Z",
"collectionId": "65b474351f5677401f2691de",
"name": "test@example.jp"
}
}

これを上手く組み合わせて実装していきましょう。

ユーザ情報を取得する関数を定義する

utils/user.jsを前回までの実装でも用意していましたが、ここに一つ新しい関数を追加してみます。まず必要な引数はユーザ名です。これは最終的には利用者が画面で入力したユーザIDを利用することなりますが今回は呼び出し側でログインユーザ名だけはハードコードします。
また、これは前回までのコードにも書いていますがIDトークン等にどこまで情報を載せるか、についてスコープを使って制御するため、もう一つの引数はスコープとなります。

こんな関数になります。
exports.getUserIdentityByLoginId = async function(login_id, scopes) {

まずは、JSONBinを実行するための準備です。
今回はX-Master-Keyにマスターキーをセットします。環境変数などへ仕込んでおくことができます。ちなみにJSONBinではマスターキーとアクセスキーの2種類のキーを発行・管理しています。マスターキーは名前の通りなんでもできるマスターキーなので本来は用途によって権限を絞り込むことができるアクセスキーを使うべきなのかもしれません。
// JSONBin用のヘッダ
const headers = new Headers({
"X-Master-Key": process.env.JSONBIN_MASTER_KEY,
"Content-Type": "application/json"
});

いよいよJSONBinのFetch Binsを使ってbinの一覧を取得していきます。
// JSONBinのユーザCollectionからユーザbinのidを取得する
const collectionUrl = new URL(`${process.env.JSONBIN_BASEURL}/c/${process.env.JSONBIN_USERCOLLECTION_ID}/bins`);
const collectionResponse = await fetch(collectionUrl, {
headers: headers
});
const userCollection = await collectionResponse.json();

先ほどのsnippetMeta.nameが関数の引数に指定したlogin_idと一致しているものを抽出します。該当がなければエラーなのでコンソールにメッセージを出しておきます。この辺りはエラーハンドリングやページングの考慮もそのうち必要になりますが今回はスキップしておきます。
const userBin = userCollection.find(i => i.snippetMeta.name === login_id);
if(typeof userBin === "undefined"){
console.log("user not found");
}else{

ここまでで取得できた当該ユーザのBinのID(record)をベースに実際のBinの中身を取得し、userIdentityというオブジェクトにセットしておきます。一応ここまでで前回までハードコードしていたユーザの属性情報をJSONBinから取得できた状態になりました。
// 当該ユーザのBin idからBinの中身を読み出す
const userBinUrl = new URL(`${process.env.JSONBIN_BASEURL}/b/${userBin.record}`);
const userBinResponse = await fetch(userBinUrl, {
headers: headers
});
const userJson = await userBinResponse.json();
const userIdentity = userJson.record;

なお、PPIDへの対応をするためにlocal_identifierをユーザのオブジェクトに指定しておきたいので、subの値を一旦local_identifierの値に待避しておきます。
// subをlocal_identifierへセット
userIdentity.local_identifier = userIdentity.sub;

あとは、前回のスコープに応じた処理を行うという意味で全く同じコードとなります。
// スコープによって返却する属性の絞り込み
if(!scopes.includes("profile")){
delete userIdentity.name;
delete userIdentity.given_name;
delete userIdentity.family_name;
delete userIdentity.middle_name;


ユーザ情報を取得する関数を呼び出す

もともと認可エンドポイントでユーザ情報を固定で呼び出す処理を書いていたので当該部分を書き換えます。
oauth2/oauth2.js
// scopeに応じたユーザの情報を取得する
// let payload = userIdentity.getUserIdentity(scopes);
// ユーザ名を指定して属性情報を取得する
let payload = await userIdentity.getUserIdentityByLoginId("test@example.jp", scopes);

こんな感じです。元のユーザ情報をハードコードしていた関数をコールする部分をコメントアウトして、今回新しく作った関数にログインIDを指定して呼び出すように変更しています。

これで完了です。

JSONBinに入ったユーザ情報がちゃんと取れました。

ということで今回はここまでです。

0 件のコメント: