こんにちは、富士榮です。
そろそろトークンエンドポイントに手を入れていきましょう。今回はクライアントの登録状態の確認と認証を行ってみます。このあたりから登録情報を持つ必要が出てきます。(といっても まだデータベースを使うまでもないのでjsonファイルを使っていきます)
その前にこれまでのおさらいです。
- まずは全体像から
- まずOpenID Providerの情報をRelying Partyに提供する
- OpenID Providerを作る)認可エンドポイントを作る
- OpenID Providerを作る)トークンエンドポイントを作る
- OpenID Providerを作る)UserInfoエンドポイントを作る
- OpenID Providerを作る)response_typeを実装する
- OpenID Providerを作る)Hybridフローを実装する
- OpenID Providerを作る)Pairwise識別子を実装する
- OpenID Providerを作る)scopeの定義と返却する属性
- OpenID Providerを作る)定義済み属性の値として何を返却すべきか
クライアントに関する情報として何を管理すべきか
今回の実装においてはクライアント認証さえできればいいので最低限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の認証エラーです。
今回はこんなところです。
0 件のコメント:
コメントを投稿