2024年1月14日日曜日

OpenID Providerを作る)Hybridフローを実装する

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

認可コードフロー(code flow)の流れの実装に加えてresponse_typeの実装を行いインプリシットとハイブリッドフローの入り口を実装してきました。


前回のインプリシットおよびハイブリッドフローの実装はあくまで入り口でしたが、今回はハイブリッドフローの主たる目的にフォーカスしてみましょう。

前回ハイブリッドフローの説明として、以下を述べました。
認可コードとアクセストークンとIDトークンが同一のトランザクションで発行されていることを確認したいような高いセキュリティレベルが求められるようなケースではハイブリッドフローが用いられることがあります。
仕様としてはこちらに記載されている通りですが、特徴的なのは認可エンドポイントが認可コードやアクセストークンと共にIDトークンを返却し、返却されたIDトークンが認可コードやアクセストークンと同一のトランザクションであること、つまりコード置き換えなどの攻撃が行われていないことを検証可能としている点です。
この実装を行うためにデジタル署名されたIDトークンの中に認可コードやアクセストークンのハッシュの値がc_hash(認可コードのハッシュ)やat_hash(アクセストークンのハッシュとして埋め込みます。
このことにより各種トークン(認可コードを含む)を発行するサーバ(OpenID Provider)側でトークンが一つのトランザクションの中に紐づいていることを保証するわけです。

ということで早速実装していきましょう。
仕様を確認すると、認可コードの検証するためにはc_hash、アクセストークンを検証するためにはat_hashという属性値を生成する必要がありそうです。
認可コードのハッシュは以下の仕組みで実装する必要があるようです。
  • code の ASCII オクテットのハッシュ値を計算する. ハッシュアルゴリズムは, ID Token の JOSE Header に含まれる alg Header Parameter で利用されるものを利用する. 各 alg Header Parameter に対応するハッシュアルゴリズムは JWA [JWA] で定義されている. たとえば, alg が RS256 であれば, 使用されるハッシュアルゴリズムは SHA-256 である.
  • ハッシュの左半分を取得し, base64url エンコードする.

同じくアクセストークンのハッシュは以下通り生成する必要があるようです。

  • Access Token のハッシュ値. この値は, access_token の ASCII オクテット列のハッシュ値の左半分を base64url エンコードしたものであり, ハッシュアルゴリズムは ID Token の JOSE Header にある alg Header Parameter で用いられるハッシュアルゴリズムと同じものを用いる. 例えば alg が RS256 であれば, access_token の SHA-256 ハッシュ値を計算し, その左半分の128ビットを base64url エンコードする. at_hash は大文字小文字を区別する文字列である.

 

要するに、それぞれの値の半分に割った左側の値をIDトークンの署名アルゴリズムに沿ってハッシュを取得してBase64Urlエンコードした値をIDトークンの中に含めれば良いと言うことです。

ということで実装していきます。

exports.createHash = function createHash(plainText){
// SHA256でハッシュを取る
const h = crypto.createHash('sha256').update(plainText).digest('hex');
// 左半分をBase64Urlエンコードする
return Buffer.from(h.slice(0, h.length /2)).toString('base64')
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=+$/g, '');
}

今回はIDトークンはRS256で署名するのでハッシュはSHA256なので、こんな感じの実装になりそうです。

上記に記載した通り、

  1. SHA256でハッシュを取る
  2. ハッシュの前半をBase64Urlエンコードする
というロジックです。

これを認可コード、アクセストークンに適用します。
認可コードはこんな感じ。

if(response_types.includes("code")){
// code flow
// 認可コードの作成
payload.exp = Math.floor((date.getTime() + (1000 * 30)) / 1000); // 有効期限は30秒
const code = await utils.generateJWE(payload);
responseArr.push("code=" + code);
// c_hashの作成
c_hash = utils.createHash(Buffer.from(code));
}

アクセストークンはこんな感じです。

if(response_types.includes("token")){
// implicit/hybrid
// access_tokenの生成
payload.exp = Math.floor((date.getTime() + (1000 * 60 * 60)) / 1000);
payload.iat = Math.floor(date.getTime() / 1000);
const access_token = await utils.generateJWS(payload);
responseArr.push("access_token=" + access_token);
isImplcitOrHybrid = true;
// at_hashの作成
at_hash = utils.createHash(Buffer.from(access_token));
}


これらの値(c_hash、at_hash)の値をIDトークンに含めていきます。

if(response_types.includes("id_token")){
// implicit/hybrid
// id_tokenの生成
payload.exp = Math.floor((date.getTime() + (1000 * 60 * 10)) / 1000);
payload.iat = Math.floor(date.getTime() / 1000);
// codeがある場合
if(response_types.includes("code")){
payload.c_hash = c_hash;
}
// access_tokenがある場合
if(response_types.includes("token")){
payload.at_hash = at_hash;
}
const id_token = await utils.generateJWS(payload);
responseArr.push("id_token=" + id_token);
isImplcitOrHybrid = true;
}


これであとはRelying Partyが受け取った認可コードをc_hashで、アクセストークンをat_hashで検証することでトランザクションが同一で合ったことの検証を行います。



どうしてもOAuthやOpenID Connectはプロトコル上行ったり来たりするのでこのようにトランザクションを確認する仕組みが必要になりますね。

0 件のコメント:

コメントを投稿