"悪い" OpenID Connect OP を作る


ちょっとテスト用にOpenID Connect OPを実装する必要があったので、ついでに"異常な"ふるまいをするOpenID Connect OPを作ってみた。

やっぱり userinfo が無いとあんまり役に立たないな。。今回作ったものは、GitLabとかGiteaのようなOpenID Connectをユーザ認証に使えるツールでの認証に使えない。これらのツールはuserinfoエンドポイントからemail等をfetchしなおしてアカウントの作成に使用しているため。

デモ

これどうやってデモを用意するかも難しいな。。

デモサイトでアクセスしてみる

デモURL にアクセスすると、Glitchの起動画面の後、エラーの種類を選ぶページが出る。

ここで選べるエラーは今のところ3種類で、

  1. Valid - 誰でも 特定のユーザ(dummy)として正常に認証できる。
  2. Expired - 常に期限切れのJWTを返す
  3. Algnone - 認証部分をカットした正常なJWTを返す

Validをクリックすると id_token を含むJSONが、それ以外をクリックするとクライアントで使っている openid-client のエラーが直接表示される。

id_tokenは例えば JWT.io ( https://jwt.io/ ) でデコードして中身を見ることができる。

他所のRPから使ってみる

Note: このOPを登録したRPは自動的に脆弱になるので注意 。世間のRPのエラー時のふるまいは OpenID Connect、OAuthプロバイダの調査 の記事で調べてフォローするつもり。

まだuserinfoを実装していないので、大抵のOpenID Connect RPでは正常にテストできない。

純粋にID tokenの取得のみを行うRPであれば使うことができる。例えば https://openidconnect.net/ に必要事項を入力してExpiredAlgnoneをテストしてみると、

THERE WAS AN ERROR VERIFYING YOUR TOKEN. TRY AGAIN.

と表示されて、正常に却下されることがわかる。

他のRPで今回実装したOPを使うには、以下のパラメタを使う:

ディスカバリを実装していないRPや、OAuth2では、

の設定も追加で必要かもしれない。

(もっとも、今回の"悪い"OPは id_token の内容が悪いだけなので、OAuth2では単に"常に認証が通るOP"というだけになり悪さを発揮できない。)

実装したエラーケース

OpenID Connect自体にはコンフォーマンステストは存在するものの https://openid.net/wordpress-content/uploads/2018/06/OpenID-Connect-Conformance-Profiles.pdf 、基本的には仕様を正確に実装していれば安全なため、明示的にいじわるなOP / RPを用意したテストを実施しているわけではないようだ。

例えば、今回で言えば Expired はJWTのライブラリ側で担保されるべき内容で、コンフォーマンスではこれは前提にしている。( exp JWT claimに関するテストはコンフォーマンステストには無い)

今のところエラーケースは2個しか実装していない。もっと充実させたい気持ちはあるが。。

常に期限切れのJWTを返す

    if(ctx.oidc.route == "token"){
        let token = ctx.response.body;
        const valid_token = token.id_token;
        if(valid_token){
            let q = Jose.JWT.decode(valid_token);
            switch(q.modify){
                case "expire":
                    q.exp = q.iat - 120;
                    token.id_token = 
                        Jose.JWT.sign(q, keystore.get({alg: "RS256"}));
                    break;

Expireを iat claimよりも120秒前に設定して署名しなおす。

ちょっと気になったのは、 openid-client ライブラリは 発行日時(iat)が期限切れ時刻(exp)よりも未来であるTokenを受け入れる ことで、JWTが期限切れかどうかを判別する際に発行日時の情報は使用していないということになる。 openid-client はデフォルトでは時刻誤差 clock_tolerance を 0 秒に設定しているため、設定を変更しない限りはこの現象は発生しない。

例えば、 clock_tolerance パラメタが十分に大きければ、 発行時点では既に期限切れとなっているようなJWT を受け入れてしまう。JWTやOpenID Connect仕様としてはこれが正しい挙動に見えるが、暗黙に iat < exp となるような仮定を持っていると不味いかもしれない。

署名部分の無いJWTを返す

function base64jwt(obj){
    const str = JSON.stringify(obj);
    const orig = Buffer.from(str).toString("base64");
    return orig.replace(/=/g,"");
}
...
                case "algnone":
                    token.id_token =
                        base64jwt({typ: "JWT", alg: "none"})
                        +
                        "."
                        +
                        base64jwt(q)
                        +
                        ".";
                    break;

JWTを手作りしているだけ。つまり、JSONをstringfyし、ヘッダ {typ: "JWT", alg: "none"} を付ける。

このようなJWTは署名部分がまるごと存在しないため、 ピリオドで終わるJWT となる。

デモでAlgnoneを選択した場合にはエラーメッセージとして

unexpected JWT alg received, expected RS256, got: none

が表示されるように、今回のOPでは nonehttps://oidcbadop.glitch.me/op/.well-known/openid-configurationid_token_signing_alg_values_supported に含めていないためRPサイドで弾かれることになる。

常識的なOPはnoneは含めていないものの、HS256は含めていることがそれなりに有るため、 JOSEは、絶対に避けるべき悪い標準規格である で言われているようなバグは依然踏める可能性がある。もっとも、これらの問題は既にとても有名になっていて、その辺の実装を見る限りではこの問題を持っているものは見つけられなかった。

oidc-provider でリダイレクトURIの検証を無効にする

明示的なエラーではないが、今回 oidc-provider でリダイレクトURIの検証を無効にしている。いわゆるOpen redirectorに相当するのでこれをやる簡単な方法は用意されていないが、プロトタイプに含まれる redirectUriAllowed を直接overrideして常に true を返却するようにしてしまった。

// Disable client redirect uri check
oidc.Client.prototype.redirectUriAllowed = (bogus) => true;

リダイレクトURIはRP側が設定するもので、このような挙動をするOPが世間に存在してもそれを信頼しなければ良いだけなので大きな問題にはならない。(もちろん真剣なOPであればリダイレクトURIの検証は必須になる。)

何の役に立つのか / 感想

個人的にはコレを "エラーが発生したらどう見えるのか" の調査に使っている。スクリーンショットを収集するとか、エラーログが届くかどうかチェックするとか。。例えば、 expired ケースはサーバ間の時刻差ができている時に現実的に発生する可能性もあるため、事前に起こしておくことで訓練ができると考えている。

エラーケースだけではなく、正解ケースも返せるものにしておいた方が良いかもしれない。

oidc-provider はちょっとこのような目的に使うには大きすぎるので、もうちょっと簡単なライブラリに纏められないものかと思っている。。passport.jsとのブリッジに特化したものがあっても良いんじゃないか的な。