2024年1月23日火曜日

OpenID Providerを作る)トークンエンドポイントにクライアント認証を実装する

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

そろそろトークンエンドポイントに手を入れていきましょう。今回はクライアントの登録状態の確認と認証を行ってみます。このあたりから登録情報を持つ必要が出てきます。(といっても まだデータベースを使うまでもないのでjsonファイルを使っていきます)

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


クライアントに関する情報として何を管理すべきか

今回の実装においてはクライアント認証さえできればいいので最低限client_idとclient_secretがあれば問題ありませんが、本来はclient_idと紐付けて管理されるべき情報としてredirect_uriや同意画面を出そうと思うとクライアント名やクライアントに関する説明やロゴ画像、利用規約などの情報も必要になりますし、登録状態の管理も行おうと思うと登録日や更新日、有効・無効などのステータス、管理者の連絡先などの情報も必要になってくると思います。

といってもそこまで必要になるのはもう少し先の話なので、まずは最低限+αということで名称、ID、シークレット、redirect_uriをファイルに保存しておきます。なお、当然複数のクライアントを登録できるようにするためJSON配列でデータを保存しておきます。
/database/clients.json
[
{
"client_id": "123",
"client_secret": "secret",
"redirect_uris": [
"https://jwt.ms",
"http://localhost:3000/cb"
]
},
{
"client_id": "456",
"client_secret": "secret",
"redirect_uris": [
"https://rp.example.jp/cb",
"https://rp.example.com/cb"
]
}
]

redirect_uriも複数登録できる必要があるので配列にしています。(今回は利用しませんが)

クライアント認証方式を決定する

仕様の9章にclient_authenticationの定義があります。
  • client_secret_basic
    • いわゆるBASIC認証を行う
  • client_secret_post
    • リクエストのBodyにclient_idとclient_secretを入れて送信する
  • client_secret_jwt
    • client_assertionパラメータにJWTを入れて送信する。署名アルゴリズムはHMAC
  • private_key_jwt
    • client_secret_jwtとの違いは署名に秘密鍵を利用すること
  • none
    • クライアント認証を行わない
ベーシックな実装では基本的にclient_secret_basicとclient_secret_postくらいを実装しておけば問題ないので、まずはこの2つを実装しておきます。

クライアント認証を実装する

対象はタイトルの通りトークンエンドポイントなので、当該のエンドポイントにexpress-basic-authを使っても良かったのですが、client_secret_postもサポートするためにミドルウェアを使わずに実装します。といってもAuthorizationヘッダに"BASIC client_id(base64エンコード):client_secret(base64エンコード)”という形で値が渡ってきているだけなので、パースしてデコードするだけです。
今回は簡易実装なのでAuthorizationヘッダがあればclient_secret_basic、そうでなければclient_secret_postとして判別しています。
// - クライアントの認証
let client_id, client_secret;
if(typeof req.headers.authorization === "undefined"){
// client_secret_post
client_id = req.body.client_id;
client_secret = req.body.client_secret;
}else{
// client_secret_basic
const b64auth = req.headers.authorization.split(" ")[1];
[ client_id, client_secret ] = Buffer.from(b64auth, "base64").toString().split(":");
}

ちなみにclient_secret_postの場合はそのままclient_idとclient_secretがボディに入ってくるだけなので値を取得しています。

次は認証処理です。といっても先のjsonファイル内のclient_id/client_secretとマッチしているかどうかを確認するだけです。
// クライアント情報の読み取り
const clients = JSON.parse(fs.readFileSync(path.resolve(__dirname, "../../database/clients.json")));
// クライアント登録状態の確認
const client = clients.find(i => i.client_id === client_id);
if(typeof client === "undefined"){
// クライアントが未登録
res.statusCode = 400;
res.json({
errorMessage: "client not found"
});
}else{
// クライアント登録確認、シークレットの検証
if(client.client_secret !== client_secret){
// クライアント認証エラー
res.statusCode = 400;
res.json({
errorMessage: "client authentication was failed"
});
}else{
// クライアント認証成功

シンプルです。
ファイルを読み込んで、client_idで情報を検索、登録されているclient_secretの値を比較するだけです。

ということでこれでトークンエンドポイントのクライアント認証が実装できました。

client_secret_basicの認証エラーです。

client_secret_postの認証エラー(未登録)です。


今回はこんなところです。






0 件のコメント: