ラベル jwt の投稿を表示しています。 すべての投稿を表示
ラベル jwt の投稿を表示しています。 すべての投稿を表示

2024年5月12日日曜日

JWTヘッダのメディアタイプの大文字・小文字問題でハマった話

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

id_tokenを作るときにJWTヘッダのメディアタイプ(typパラメータ)の値として'JWT'を指定すると思いますが、この値の大文字・小文字ではまった話です。

ちなみに、こんな感じでjoseのライブラリを使ってJWTを作っていたのですが、下から2行目のオプション指定のところで{ typ: 'jwt' }という形で小文字指定をしていました。

// JWSの作成
exports.generateJWS = async function(payload) {
const ks = fs.readFileSync(path.resolve(__dirname, keyStoreFile));
const keyStore = await jose.JWK.asKeyStore(ks.toString());
const [key] = keyStore.all({ use: 'sig' });
   const opt = { compact: true, jwk: key, fields: { typ: 'jwt' } };
return jose.JWS.createSign(opt, key).update(JSON.stringify(payload)).final();
}

JWTの仕様を見る限りnot case sentisiveとあるので、その後にあるレガシー実装との互換性のために常に"JWT(大文字)"を使うことを推奨という文言を舐めていました。

If present, it is RECOMMENDED that its value be "JWT" to indicate that this object is a JWT.  While media type names are not case sensitive, it is RECOMMENDED that "JWT" always be spelled using uppercase characters for compatibility with legacy implementations.

仕様)

https://datatracker.ietf.org/doc/html/rfc7519#section-5.1


ということで、MicrosoftのRPに小文字'jwt'で作ったid_tokenを投げ込むとこんな感じで怒られます。

仕方なく、コードを修正しました。

exports.generateJWS = async function(payload) {
const ks = fs.readFileSync(path.resolve(__dirname, keyStoreFile));
const keyStore = await jose.JWK.asKeyStore(ks.toString());
const [key] = keyStore.all({ use: 'sig' });
const opt = { compact: true, jwk: key, fields: { typ: 'JWT' } };
return jose.JWS.createSign(opt, key).update(JSON.stringify(payload)).final();
}


確かに軽く他社の実装(Microsoft以外だとAuth0とかGoogleとか)を見るとちゃんと大文字になってますね。

2024年3月2日土曜日

なぜSAMLの脆弱性は今でも報告されるのか。そしてOIDCやVCは大丈夫なのか

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

故Craig Burtonが”SAML is Dead”という名言を放ってから永らく経つわけですが、まだまだ現役のSAMLには今でもたまに脆弱性のレポートが出てきます。

* SAML is Dead


昨日もSilver SAML Attackに関するレポートが出ていました。

(図は記事より)

非常にざっくり要約すると、以下のようなことが書かれています。

  • 例としてEntra IDを挙げている
  • Entra IDではSAML Responseへの署名を行う秘密鍵として外部で生成した鍵を利用することができる(BYOK)
  • この鍵が漏洩するなどして不正に利用されるとSAML Responseの偽造ができてしまう
  • 署名に使う鍵はIDシステムの内部で発行したものを使った方が良い
当たり前じゃん。秘密鍵が奪われているんだから。

と言いつつ、何でこう言うことが起きるのかをちゃんと見ていきたいと思います。

脆弱性の原因

SAMLに限らず、ではありますがこの手の仕組みの基本は「デジタル署名を施したデータ(SAML AssertionやOpenID Connectにおけるid_tokenなど)」をIdentity ProviderからRelying Partyからの要求等に応じて送出する、という仕組みになっています。
こうなってくると当然のことながらデジタル署名を確実に実施・検証する、そして先日から順番に読んでいるOAuth2.0 Security Best Current Practiceにも要所要所でててくる、送信元・送信先をいかに限定するか、やり取りの過程の中でCSRFなど攻撃者によるインジェクションをいかにして防ぐか、などがポイントになってきます。
  • 署名・検証の不備を防ぐ
  • 通信過程における攻撃者の関与を防ぐ
後者についてはOAuth2.0 Security Best Current Practiceで見ていくとして、今回は先のSAMLの件もあるのでデジタル署名について見ていきたいと思います。

デジタル署名の前提

デジタル署名を安全に行うための前提は言うまでもなく署名に使う鍵を安全に管理すること、につきます(もちろんアルゴリズムの安全性の話は言うまでもありません)。
今回挙げたケースは鍵を適切に管理できない状態が起きると危ないですよ、というレポートなので、改めて秘密鍵の管理の重要性を説いています。
(もちろん、安全に管理してくださいね、と言ったところで安全に管理できない人たちが多いのは理解していますが、管理方法について深掘りするのはここでは避けます)

なお、今回は鍵管理の話にフォーカスが置かれていましたが、実際の脆弱性はデジタル署名する対象となるデータの生成に依存することが多いと思います。いわゆる「正規化」の問題です。

実際過去に当ブログでも紹介したDuo SecurityのレポートはSAML Assertionを生成する際のXMLの正規化がポイントでした。

正規化の問題

SAMLにおけるXMLは非常に柔軟なデータ表現である一方で「どうやって同一性を担保するか」という問題を抱えています。
例を挙げると、
  • 空白値のトリミング
    • <attribute name="email">test@example.jp</attribute>
    • <attribute name="email"> test@example.jp </attribute>
    • を同一のものとして扱うか
  • コメントの取り扱い
    • <!-- これはコメントです -->
    • <attribute name="email">test@example.jp</attribute>
    • <attribute name="email">test@example.jp</attribute>
    • を同一のものとして扱うか

    と言う問題です。
    これらを解決するために行われるのが「正規化」です。

    XMLの正規化はW3CのCanonical XML Version 2.0で定義されています。

    実際に正規化をする際はこの仕様に従いXMLを処理していくことになるのですが、この過程においてバグが混入することがある、というのが問題の原因になっています。
    実際、先に挙げたDuoのレポートでは値の間にコメントがあった場合の処理に問題があり、別のユーザで認証され生成されたSAML Assertionを使って別のユーザになりすますことができてしまう、というものでした。


    OIDCやVCではどうなのか?

    こう考えるとJSONを使うOpenID Connectや、JSONやJSON-LDを使うVerifiable Credentialsはどうなのか?という疑問が湧いてきます。

    まずOpenID Connectですが署名付きJSONトークンにはRFC7515 JWS(JSON Web Signature)を使っています。

    崎村さんが経緯をブログで紹介されていますが、SAMLの正規化との戦いの経験から「JWSでは正規化を行わない」のがポイントの一つになっています。

    Verifiable Credentialsについてはどうなのか、というとIETFのSD-JWT VCをパターンではOpenID Connectと同じくJWSなので正規化は行いません。
    しかしながらW3C VCはJSON-LDも許容するのでこちらは考えないといけません。JSON-LDはRDF(Resource Description Framework)に基づくLinked Dataを表現するフォーマットですので、正規化をするには、
    を使っていく必要があります。
    しかしながらこの正規化で使われるアルゴリズムであるUniversal RDF Dataset Normalization Algorithm 2015(URDNA-2015)をJSON-LDに適用する際の注意点についてレポートが上がっていたりしますので、実装者はかなり気を遣う必要がります。
    実際、昨年のIIWでもJSON-LDのプロパティ名を変えても署名が崩れないというデモ(Linked Dataの性質を考えると当然の動きではありましたが)もありましたが、VCとして使うにはこのようなことが起きないように注意深くデータ構造を設計する必要があります。

    しかし、この辺りの議論を見ているとJWSが「正規化を行わない」という判断をしたのは非常に重要なことだったことがわかりますね。


    2024年1月27日土曜日

    JWTのデコードをターミナル上で行う

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

    皆さんIDトークンなどのJWT(JSON Web Token)の中身を確認するのに何を使っていますか?

    私はOkta(Auth0)が提供しているhttps://jwt.ioやMicrosoftが提供しているhttps://jwt.msをよく使うのですが、いちいちブラウザを立ち上げるのが面倒な場合もありますよね。

    もちろんjwt-cliを使ってコマンドラインで見るのもいいのですが、もうちょっとUIも凝ったものも欲しいよね、というVZ Editorが忘れられない人たちもこの界隈にはいるはずです。


    ということで今回紹介するのはjwt.ioと同じくOkta(Auth0)が提供するjwt-uiです。

    図)githubより


    早速導入してみます。(私はMac環境ですが、Windowsでも使えるみたいです)

    インストール

    brewでインストールするようです。

    brew tap jwt-rs/jwt-ui
    brew install jwt-ui

    これだけです。

    起動

    jwtui

    これだけです。立ち上がりました。

    デコード

    UIを起動した状態でEnterを押下すると入力フィールドにフォーカスが当たるので、デコードするJWTをペーストできるようになります。
    こんな感じで使えます。
    もちろんコマンドラインから直接JWTを引数に指定してもデコードができます。
    % jwtui eyJ・・・・という感じです。
    するとUIが起動してデコードされた状態が表示されます。

    jwt-cliと同じように標準出力にデコードされたものを出力することもできます。
    % jwtui -sn eyJ・・・・という感じで使います。(-sは標準出力への出力、-nは署名検証をしない、というオプションです)

    他にも色々とオプションがあるので使ってみてください。
    • -S, --secret <SECRET> Secret for validating the JWT. Can be text, file path (beginning with @) or base64 encoded string (beginning with b64:) [default: ]
    • -s, --stdout Print to STDOUT instead of starting the CLI in TUI mode
    • -n, --no-verify Do not validate the signature of the JWT when printing to STDOUT.
    • -j, --json Format STDOUT as JSON
    • -t, --tick-rate <TICK_RATE> Set the tick rate (milliseconds): the lower the number the higher the FPS. Must be less than 1000 [default: 250]
    • -h, --help Print help
    • -V, --version Print version






    2024年1月5日金曜日

    バックエンドが充実していない環境でOpenID Providerを作る

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


    昨日こんなことを書きましたので、ちょっと深掘りしてみようと思います。

    この辺りは別ポストで詳しくお話しようと思いますが、最後までステートレスにこだわったMicrosoftは認可コードをJWTにして認証済みユーザの属性情報を暗号化して入れることでトークン要求時に認可コードからセッションをLookupしてid_tokenを生成する必要をなくしていたり、VittorioがAuthorのRFC9068/JWT Profile for OAuth 2.0 Access Tokenの様にIntrospectionエンドポイントが無くてもトークンの有効性検証ができる仕組みを実装したり、Hybrid Flowでもないのにフロントで取得できるid_tokenにはほとんど情報を入れず、userInfoやGraph APIへのアクセスをしないとユーザ属性が全く取れなかったり、、、とグローバルスケールの認証基盤を運営する上での苦労が滲み出ていたりします


    要するにEntra IDの話なんですが、良いか悪いかは置いておいてOpenID Providerを作るときにバックエンドにDBをなるべく持たずに認可コード、アクセストークン、IDトークンのやりとりを一貫性を持って実行するための工夫の話です。


    そもそも何が課題なのか?

    ご存知の通りOpenID ConnectやOAuthはIDトークンやアクセストークンを取得するためにユーザエージェント(ブラウザ)とRelying Party(クライアント)が認可エンドポイントとトークンエンドポイントとの間のアクセスを行ったり来たりする必要があります。(いわゆるOAuth Dance)

    これを実装しようとすると認可エンドポイントへのアクセスとトークンエンドポイントへのアクセスが本当に同一トランザクションの中でのやりとりなのかを確認しないといけませんし、サーバ(OpenID Provider)側としてはトランザクション内での情報の保持をしないといけなくなります。ただHTTPは本来ステートレスな仕組みなので、通常のWebアプリケーションを作る場合と同じ様にサーバ側でのセッション管理をどうするか、しかも認可エンドポイントはユーザエージェントから直接アクセス、トークンエンドポイントはクライアントからのバックエンドアクセスという形なのでcookieで、というわけにも行きません。

    通常の実装では何の疑問もなくバックエンドにデータベースを置いてステート管理をサーバ側で実装することになるのですが、グローバルでスケールしている認証サービスの様に極めて高い可用性が必要とされるサービスでその様なインフラを作るのは非常にコストが高い行為といえます。(Entra IDの様に基本無償で提供される様なサービスなら難しい判断になると思います)


    そうだ、クライアントにステート情報のハンドリングを任せよう!

    そこで考えられたのが全てのやりとりの中で最終的にIDトークンやアクセストークンを発行するのに必要となる情報をもと回れば良いではないか、という考え方だと思われます。(Entra ID以外で見たことはありませんが)

    簡単に流れを説明するとこんな感じになっているんだと思います。

    認可エンドポイント

    1. ユーザを認証する→最終的にIDトークンに含めるユーザの属性情報を取得する
    2. nonceやaud(クライアントID)など、同じく最終的にIDトークンに含める属性をクエリパラメータから取得する
    3. 1,2で取得した情報を暗号化してJWTにして認可コードとしてクライアントへ戻す
    トークンエンドポイント
    1. クライアントからPOSTされた認可コードの検証、復号を行う
    2. 復号した情報をもとにIDトークンを生成してクライアントへ返却する


    この辺りをミニマムで実装してみるとこんな感じのコードになると思います。(テスト実装なので細かいところは気にしないでください)


    
    const router = require("express").Router();
    const jwt = require("jsonwebtoken");
    
    // jwt署名に使う共有シークレット
    const jwtSecretForCode = "this_is_a_secret_for_code_signing";
    const jwtSecretForToken = "this_is_a_secret_for_token_signing";
    
    // 認可エンドポイント
    router.get("/authorization", async (req, res) => {
        // 本来なら実装する処理
        // - ユーザの認証
        // - response_typeによるフローの振り分け
        // - client_idの登録状態の確認
        // - redirect_uriとclient_idが示すクライアントとの対応確認
        // - scopeの確認
        // - 属性送出に関する同意画面の表示
    
        // codeの生成(本来は暗号化しておく)
        // 最終的にid_tokenに入れる値をDBに保存する代わりに暗号化してcodeに入れておくことでバックエンドを持たずにすませる
        const jwtPayload = {
            iss: "myOpenIDProvider",
            aud: req.query.client_id,
            sub: "authenticatedUserIdentifier",
            email: "test@example.jp",
            given_name: "taro",
            family_name: "test",
            nonce: req.query.nonce
        };
        const jwtOptions = {
            algorithm: "HS256", // 面倒なのでHS256にする
            expiresIn: "30s" // コードの有効期限なので短め
        };
        const code = jwt.sign(jwtPayload, jwtSecretForCode, jwtOptions);
        // redirect_uriへリダイレクト
        res.redirect(req.query.redirect_uri + "?code=" + code + "&state=" + req.query.state);
    });
    
    // トークンエンドポイント
    router.post("/token", (req, res) => {
        // 本来なら実装する処理
        // - クライアントの認証
        // - grant_typeの検証
        // - codeの検証(有効期限、発行先クライアント、スコープ)
        // - access_tokenの発行
        // - id_tokenの発行
        const code = req.body.code;
        jwt.verify(code, jwtSecretForCode, (err, decoded)=> {
            if(err){
                res.json({
                    err: "counld not verify code"
                })
            };
            if(decoded){
                // 本来は検証が終わったらcodeを無効化する
                // codeの中身を取り出してid_tokenを作る
                const jwtPayload = {
                    iss: decoded.iss,
                    aud: decoded.aud,
                    sub: decoded.sub,
                    email: decoded.email,
                    given_name: decoded.given_name,
                    family_name: decoded.family_name,
                    nonce: decoded.nonce
                };
                const jwtOptions = {
                    algorithm: "HS256", // 面倒なのでHS256
                    expiresIn: "10m" // id_tokenの有効期限なので少しだけ長め
                };
                const token = jwt.sign(jwtPayload, jwtSecretForToken, jwtOptions);
                res.json({
                    access_token: token, // ちなみにEntra IDの場合はaccess_tokenもid_tokenとほぼ同じものが使われるケースもある。
                    token_type: "Bearer",
                    expires_in: 3600,
                    id_token: token
                });
            }
        });
    });
    
    module.exports = router;
    


    実際に動きを見てみます。
    • 認可エンドポイントへのアクセス
    http://localhost:3000/oauth2/authorization?client_id=111&redirect_uri=https://localhost:3000/cb&state=hoge

    • コールバックへのリダイレクト
    https://localhost:3000/cb?code=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJteU9wZW5JRFByb3ZpZGVyIiwiYXVkIjoiMTExIiwic3ViIjoiYXV0aGVudGljYXRlZFVzZXJJZGVudGlmaWVyIiwiZW1haWwiOiJ0ZXN0QGV4YW1wbGUuanAiLCJnaXZlbl9uYW1lIjoidGFybyIsImZhbWlseV9uYW1lIjoidGVzdCIsImlhdCI6MTcwNDQyMDk1MCwiZXhwIjoxNzA0NDIwOTgwfQ.XKi6JLc5IwDNyZG1C7Lrk1UrBiWpOV5EC2NgpqWSXjk&state=hoge

    • 認可コードの中身(面倒なので今回は暗号化はしていません)
    • トークンエンドポイントへのPOST 

    • 取得できたIDトークンの中身



    本当にこれでいいのか?

    まぁ確かにスケーラビリティを考えると非常にエコなので実装としてはありだと思いますが、色々と割り切りをしないといけません。

    例えば、

    • 認可コードのサイズが大きくなる
    • アクセストークンはJWT形式(内包型トークン)が前提となり、Introspectionエンドポイントの実装はできない
    などが代表的なところです。
    特に認可コードのサイズが大きくなる問題は結構深刻で、認可エンドポイントからクライアントへのリダイレクト時のクエリパラメータの長さがものすごいことになりますので、クライアントの実装によっては認可コードが受け取れない、ということが結構あります。ここはMSALを使え!というMicrosoftの理屈なんだと思いますが、既存のアプリケーションとEntra IDを繋ぐ際にはしばしば問題になる可能性があるので、Entra IDの導入を行う際は要注意のポイントの一つです。

    まぁ、いずれにしてもID基盤の導入はアプリケーション開発側との認識合わせ・仕様合わせが一番大きなポーションを占めると思うので、この辺りも念頭におきながらEntra IDライフを楽しむべきでしょう。

    2017年10月17日火曜日

    jwt.msとjwt.io。JWTオンライン・デコーダを比べてみる

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

    eyJ・・・とくると脊髄反射してしまうID厨の方々は既にご存知だと思いますが、JWT(Json Web Token)の中身を眺めてデバッグをする時にとっても役に立つのがオンライン・デコーダです。

    個人的には、Auth0が提供しているjwt.ioをずっと使っていたのですが、最近マイクロソフトが提供し始めたjwt.msも意外と便利なので比べてみます。

    使い方はjwt.ioもjwt.msも共通で、eyJ・・・の文字列を張り付けると自動的にデコードされ、JSONが表示されます。

    ◆jwt.io

    まずはjwt.ioです。

    左側に張り付けると右側にデコードされたヘッダ、ペイロードが表示されます。
    単純にペイロードの中身にどんなクレームが飛んでいるのかを見るには十分です。

    jwt.ioの一番の特徴は各言語毎のJWTのハンドリング用のライブラリと対応状況を掲載しているところでしょう。Ruby用、PHP用ライブラリとして、OpenID Foundation Japanのnovの作品も紹介されています。


    ◆jwt.ms
    次にjwt.msです。

    ちなみに画面構成がものすごくシンプルなので、誰が運営しているのか見た目からは全く理解できませんが、whoisで調べるとちゃんとマイクロソフトがドメインの持ち主であることがわかります。

    基本機能はjwt.ioと全く同じです。
    画面上部に張り付けると下にデコードされた結果が表示されます。

    jwt.msの一番の特徴は各クレームの情報が細かく確認できることだと思います。
    Claimsタブを開くと各クレームの意味など細かい情報が出てきます。


    まとめると、
    ・ライブラリを調べたい時はjwt.io
    ・各クレームの意味・仕様を細かく知りたい時はjwt.ms
    といったところでしょうか。

    まぁ、最終的には好みですね。