2020/11/19 に 「idcon vol.28 DID特集その2」 やります

こんにちは、ritouです。

あの #idcon がオンラインで復活!?しかもテーマはDecentralized Identity(DID)!? と言うことで誰かの意識が高まっているのを観測したのでやります。

idcon.connpass.com

今のところの発表案です。

  • 「MSのDID/VC概要」by phr_eidentity : MSのDID/VCを触ってみたので解説します。
  • 「Credential Handler API(仮)」by kg0r0
  • 「DIDCommとかSIOP」by ken5
  • 「Email, Messaging, and SSI/DID: Round2」by s01 : Email, Messaging, and SSI/DID の日本語再放送 + DIDCommで本当にいけるのかという話

順番はテキトーに決めましたが、最初に概要から入り、Credential Handler APIとかDIDCommとかSIOPとか、個別のトピックについて解説されると一気に理解が進むのではないかと期待しています。

そして、大体は こんな感じだよ -> へー、なるほどー終わりーとならない(!)のが idcon でもあります。

  • MSはわかった。他に試せるところって?
  • Credential Handler APIって必要?SIOP拡張したったらこんなのいらなくね?
  • DIDCommってもっとシンプルにならんの?

と言ったあたりはいつもの idcon だと発表の後で盛り上がるところなので、オンラインでも白熱した議論が行われると良いですね。

配信どうしようかまだ決めていませんが、なるようになるでしょう。11月になったら考えます。

と言うことで、参加申込み募集中で〜す。ではまた!

(そろそろ iddance もやらないとなぁ...)

f:id:ritou:20201012185719p:plain

Self-Issued OpenID Connect Provider DID Profile v0.1 とは

f:id:ritou:20200922040730p:plain

おはようございます。ritouです。

今日はこれを読んでみます。

Self-Issued OpenID Connect Provider DID Profile v0.1

何のための仕様か

This specification defines the "SIOP DID Profile" (SIOP DID) that is a DID AuthN flavor to use OpenID Connect (OIDC) together with the strong decentralization, privacy and security guarantees of Decentralized Identifiers (DID) for everyone who wants to have a generic way to integrate Identity Wallets into their web applications.

  • WebアプリケーションにIdentity Walletを統合するために
  • DIDとOpenID Connectの組み合わせるための
  • SIOP DID Profileじゃ!

と言うことです。

SIOPについてはもう大丈夫でしょうか?七年前の記事で良ければどうぞ。

ritou.hatenablog.com

Qiitaにもいくつか記事があります。

qiita.com

  • OpenID Providerとして特定のWebアプリケーション(Googleとか)ではなく、モバイル端末内で動作するアプリケーションなどを利用する
  • openid:// と言うカスタムURIスキームを用いた認可リクエストにより呼び出され、鍵ペアを生成/管理してIDToken を返す
  • ユーザーの同意のもとで端末単位で保存している情報のやりとりや、使い方次第では端末の識別やリクエストが同端末から送られたことの検証などにも使おうと思えば使える

と言うものであり、仕様としてはモバイルアプリ、ブラウザのプラグイン、デバイスの機能など特定のURI呼び出しを検知して起動できる仕組みで使えるように汎用的に作られています。

この仕様では、特定のサービスではなく端末自体をOPにすると言うSIOPをDID/SSIのIdentity Walletとして使うために必要な差分なりが定義されています。

Protocol Flow

RPはWebアプリ/ブラウザベース(SPAなど)のアプリであり、モバイルもしくはデスクトップブラウザからIdentityWalletアプリを立ち上げて使う想定です。 公式の図を見てみましょう。

https://identity.foundation/did-siop/assets/did_authn_siop_profile_flow.png

流れをざっくりまとめると、

  1. RPは "Sign-in with SSI" ボタンみたいなのを用意し、ユーザーが押すと SIOP Request を生成
  2. openid://?<SIOP Request> として何やかんやで Identity Walletアプリが起動する
  3. Identity Walletアプリは OIDC/DID AuthN に従って SIOP Request を検証する
  4. 必要なら認証をして、その後に SIOP Response を作成する
  5. response_mode の指定にしたがってRPにSIOP Responseが渡される
  6. RPはOIDC/DID AuthN に従って SIOP Response を検証する

となります。

基本的にこのSIOPの流れに、DID用の処理が追加されるイメージです。

  • SIOP Request : RPは自身のDIDを含み、DIDドキュメントから検証可能な秘密鍵で署名を生成
  • SIOP Response : SIOPは自身のDIDを含み、DIDドキュメントから検証可能な秘密鍵で署名を生成

それでは見ていきましょう。

Generate SIOP Request

SIOP Request は次のように定義されています。

  • 互換性のため、response_type, scope, client_id を文字列で指定する
  • それ以外のパラメータは request もしくは request_uri として指定する。RPのDIDもこっちで指定。

例を引用します。

openid://?response_type=id_token
    &client_id=https%3A%2F%2Frp.example.com%2Fcb
    &scope=openid%20did_authn
    &request=<JWT>
openid://?response_type=id_token
    &client_id=https%3A%2F%2Frp.example.com%2Fcb
    &scope=openid%20did_authn
    &request_uri=https%3A%2F%2Frp.example.com%2F90ce0b8a-a910-4dd0

いわゆるリクエストオブジェクトを指定、なんてのはFAPIなどのProfileでも使われているので驚きはないでしょう。

SIOPのフローでは、registration パラメータによってRPのメタデータを登録できます。

  • アルゴリズムRS256 に加えて ES256KEdDSAをサポートしなければならない
  • Request ObjectはDIDドキュメントに記載されている検証方法で直接/間接的に検証可能であり、RPのJWKSにより直接検証可能である必要がある
  • JWKSは jwks_uri もしくは jwks パラメータで指定され、jwks の場合は kid が一致する必要がある。jwks_uriの場合はHTTP(S) DID Resolution Bindingが必要とか...
  • RPはSIOP Responseを暗号化して受信することもできる

リクエストオブジェクトについても細かく書いてありますが、眠いので例示に留めます。

#header
{
    "alg": "ES256K",
    "typ": "JWT",
    "kid": "did:example:0xab#veri-key1"
}

# payload
{
    "iss": "did:example:0xab", # RP's DIID
    "response_type": "id_token",
    "client_id": "https://my.rp.com/cb",
    "scope": "openid did_authn", # did_authn を指定
    "state": "af0ifjsldkj",
    "nonce": "n-0S6_WzA2Mj",
    "response_mode" : "form_post",
    "registration" : {
        "jwks_uri" : "https://uniresolver.io/1.0/identifiers/did:example:0xab;transform-keys=jwks",
        "id_token_signed_response_alg" : "ES256K"
    }
}

SIOP Request Validation

SIOPは scopedid_authn が指定されていたら、OIDCで定義されている検証に加え次のような検証を行います。

  • iss クレームで指定されたRPのDIDからDIDドキュメントを取得
  • jwks_uriが存在する場合、iss とDIDが一致することを検証
  • RPのDIDドキュメントから、kid と一致する検証方法を取得
  • SIOP Requestの検証

Generate SIOP Response

SIOPは以下のような SIOP Response を作成してRPに戻します。

  • sub_jwkkid を含む
    • kid はSIOPのDIDドキュメントの検証方法を参照する DID URL
  • SIOPのDIDを含む : did クレーム
# Header
{
    "alg": "ES256K",
    "typ": "JWT",
    "kid": "did:example:0xab#key-1"
}

# Payload
{
    "iss": "https://self-issued.me",
    "nonce": "n-0S6_WzA2Mj",
    "exp": 1311281970,
    "iat": 1311280970,
    "sub_jwk" : {
        "crv":"secp256k1",
        "kid":"did:example:0xcd#verikey-1",
        "kty":"EC",
        "x":"7KEKZa5xJPh7WVqHJyUpb2MgEe3nA8Rk7eUlXsmBl-M",
        "y":"3zIgl_ml4RhapyEm5J7lvU-4f5jiBvZr4KgxUjEhl9o"
    },
    "sub": "9-aYUQ7mgL2SWQ_LNTeVN2rtw7xFP-3Y2EO9WV22cF0",
    "did": "did:example:0xcd" # SIOPDID
}

(ちょっと追記)通常のSIOPは鍵を生成して端末なりに保存しておくだけのイメージですが、ここではDIDを含みDIDドキュメントから検証できるようにする必要があるわけなのでこれまでのSIOPの感覚で言うと鍵の登録処理も発生しそうです。もう少し細かく追記してもらうのが良いのかなと思ったりします。

SIOP Response Validation

OIDCのSIOP Responseの検証としては

  1. id_token を取得
  2. id_tokensub_jwk の鍵で署名されたことを検証

さらに DID AuthNとして

  1. id_token の SIOP DID(did) から DIDドキュメントを取得
  2. id_tokenのDIDドキュメントから sub_jwkkid と一致する検証方法を取得
  3. その検証方法で id_token を検証

SIOP Discovery

省略します。

UX Considerations

カスタムURLスキームの扱いについて。 モバイルブラウザの場合はアプリ立ち上げるけどAndroid/iOSそれぞれで今までもいろいろ言われてきたよねと。 デスクトップブラウザの場合、もしかしたらブラウザ拡張/プラグインでサポートされるかもしれないけどうまくいかなそうならIdentity Walletのアプリを開かせてQR読み込ませるとかが必要かも。

この辺りが改善されるにはもう少し時間がかかるかもしれませんね。

Security Considerations

まずはSIOP ResponseのIDToken漏れるかも問題

  • Interception of the Redirect URI : SIOP Responseを取られたらIDToken持っていかれるよ -> RP頑張れ(オープンリダイレクト作るな)
  • Identity Token Leak in Browser History : ブラウザの履歴からSIOP ResponseのIDToken漏れるよ -> IDTokenの有効期限短くしたりキャッシュされないようにしたりしよう。完全な対策は難しいかも。
  • Identity Token Leak to Third Party Scripts : SIOP Responseをを受ける時に3rd PartyのJSとかでIDToken持っていかれるよ -> RP頑張れ(信頼できるやつだけ使え)

対策として response_mode=form_post 使えるならう。無理な場合もあるけど。とあります。

次に Session Fixation in Cross-Device Flow として、異なるデバイス間の転送を含む場合の脅威が記載されています。 RPがQRコードからIdentity Walletを呼び出す仕様の場合、攻撃者は自らのセッションに紐付いたQRを読ませることで第3者の認証を攻撃者のセッションでできるかも。 対策としては同一セッションであることの確認、もしくは異なるデバイスでも同一ユーザーであることを別の認証方式などで確認することが必要。

Github

何かあったらここでやってるよと。

github.com

(追記)SIOP側の公開鍵登録についてもう少し説明必要では?みたいなことを書いてみました。

github.com

まとめ

  • SIOPの流れにDIDの検証処理を追加するProfile
  • RPが作るリクエストオブジェクトの部分がモリモリしてるがSIOPのフローは変わらない
  • カスタムURIスキームやQRコード使う部分はもうちょい改善されないとUXキツそう

と言うあたりでしょうか。引き続きウォッチしていきましょう。 ではまた!

フィッシング対策視点から考えるメールやSMS通知のあり方

こんばんは、ritouです。

令和の時代においても、フィッシング攻撃はメール、SMS共に存在します。 だいぶ普及してきたみんな大好きワンタイムパスワードでもフィッシングに対しては脆弱です(FIDOにしましょう)。 フィッシング攻撃への現状の対策はどうかというと、メール自体の検証、メールにあるURLの表示と実際のリンクの差異、URLが正規のものかを確認することは素人になればなるほど困難です。 SMSならばURLはテキストそのものだから安心かもしれませんが、そもそも正規のメッセージに偽物が混ざってきたらわかりません。

ということで、今回は「メールやSMSにできるだけURLや電話番号などを記載するのを避けよう。」という話です。

主張

例えばこの記事

keepmealive.jp

それならば対策は二つだけ 1. SMSに書かれているURLは原則としてクリックしない 2. どうしてもクリックする必要があるときは本物のURLか確認する ということになります。

ここではdocomoの正規のURL(ドメイン)のリストがずらっと並んでいますが、このような検証を幅広いサービスに適用していくことは困難です。 なので、「メールやSMSにできるだけURLや電話番号などを記載するのはやめよう。」という意見ですね。

某、徳●さんのTweetで見かけたんですが、

pc.watch.impress.co.jp

同社では、「正規の当選メールには、サイトのURLの記載やリンクはなく、申し込み時に登録する『マスク抽選販売サイト』にある『当選者専用サイト』から登録する設計になっている。偽の当選メールに記載されているサイトのURLに行かないようにしてほしい」としている。

まさにこれですね。

UX低下を防ぐ仕組み

この話をすると、URLや電話番号を載せなかったらUXが悪くなるのでは?と言われることがあります。 フィッシングサイトや業者まで一発で引っ張っていけるということで、それだけURLや電話番号を利用することは効率的なUXであるとも言えるでしょう。 正規の通知しか送られてこない前提になっている、例えばモバイルアプリの通知のような場合はそこからアプリを立ち上げて...というUXを変える必要はないでしょうけれども、フィッシング攻撃の可能性がある限りはその部分を取り除きつつ通知の詳細確認やその後の必要な処理への誘導について、代替手段を用意する必要があります。 通知に含まれたURLや電話番号に直接アクセスする代わりに、Webアプリやモバイルアプリに通知を確認できる場所を用意しておき、ユーザー自身でそこにアクセスしにいく方法が良いのではないかと。

  • 今まで
    • 通知の概要(メール/SMS) -> URLクリック -> 詳細確認や継続処理
  • これから
    • 通知の概要(メール/SMS) -> (自分でブックマークなどからWebアプリに移動) -> 詳細確認や継続処理
    • 通知の概要(メール/SMS) -> (手元の端末でモバイルアプリを起動) -> 詳細確認や継続処理

「これから」の方に書いたUXが定着していくと、フィッシング攻撃が来ても「このフロー変じゃない?」と気付けるような仕組みの上での「啓蒙活動」もやりやすくなり、世界の平和に向けて一歩前進できるのではないでしょうか。 サービスの種類にもよりますが、個人的にはこの方が通知を受けたデバイスと実際にサービスに到達しにいくデバイスが異なるパターンでも利用できそうですし、UXは向上するのではないかと思ったりします。。

まぁ、実際にプロダクトでやろうと思ってもなかなか受け入れられないかもしれませんが、さいきょうのアイディーを求めることは悪いことではないでしょう。 ではまた!

f:id:ritou:20200903010702p:plain

現在仕様策定中の OpenID Connect RP-Initiated Logout 1.0 とは

こんばんは ritouです。

むかーしむかし、あるところにOIDCのセッション管理に関する3つの仕様がありました。

(2015年の記事)

ritou.hatenablog.com

(Session Managementについてのさらに古い 2013年の記事)

ritou.hatenablog.com

いわゆるソーシャルログインと呼ばれる使い方ではRP/OP間のセッションの整合性をあまり意識しない感じになっていますが、 昔から使われている "SSO" 的な使われ方においてはそのあたりが意識されるかもしれません。 これらの仕様ではRP/OP間でセッションに変更がないか、ログアウトさせるいくつかの方法が定義されています。

これらは最近どうしてるのかな?というところで現状を見てみると、4つの仕様として策定が進められています。

[Openid-specs-ab] RP-Initiated Logout is now its own specification

また、既にOpenID Certificationにはテストも用意されており、パスしたOPも書かれています。

openid.net

f:id:ritou:20200819024520p:plain

というところで、今回はこのRP-Initiated Logoutについて概要(not 翻訳)を紹介します。

openid.net

This specification complements the OpenID Connect Core 1.0 specification by enabling the Relying Party to request that an End-User be logged out by the OpenID Provider.

これはOPによるエンドユーザーのログアウトをRPから要求できるようにするための拡張仕様です。

新しい用語

これぐらいです。

  • Logout Endpoint : RPがログアウトのリクエストを送るターゲットとなるOPのエンドポイント

RPはこの値を後述する metadata から取得したりドキュメントとかで把握する必要があります。

RPからOPへのログアウト要求

RPがOPにログアウトを要求するパターンとして、2つあります。

  • RP -> OP と遷移して終わるパターン
  • RP -> OP -> RP に戻ってくるパターン

前者の場合、必要なパラメータは

  • id_token_hint : OPからRPに過去に発行されたID Tokenの値をヒントとして指定。RECOMMENDED
  • ui_locales : OP上での優先言語、スクリプトの指定。OPTIONAL

となります。AuthZ(AuthN) Requestのように client_id パラメータを用いて明示的に Client を指定したりはありません。 id_token_hint パラメータがあった場合、事前に ID Token にセッション識別子である sid を入れておくことで現在のセッションと一致するかどうかの判定ができますが、ない場合や検証不可な場合はOPとしてはユーザーがどこから来たのかさえわからない(信用できない)状態になります。

セッションとIDTokenの関連というと今までも何度か記事を書いてきましたが、今回のようなセッション関連の仕様を読むときは、IDTokenをセッション識別という目的で一定期間保持することを想定しなければなりません。有効期限はサーバー時間のずれに影響でないぐらいになるべく短くしIDTokenを取得したタイミングで "ID連携" を行う、という本来のOIDCの考え方との違いについてよく頭の体操をしておく必要があるでしょう。(個人的には id_token_hintという使い方が大っ嫌いなのですが精一杯大人な書き方をしています。)

また、仕様中に突然出てくる sid ですが、これは OpenID Connect Front-Channel Logout で定義されています。仕様の最初の方で他のセッション関連の拡張仕様との関連について言及はありますが、sid がどこで定義されたものかが言及されていないと、初めて見た開発者は混乱するかもしれません。

Draft: OpenID Connect Front-Channel Logout 1.0 - draft 04

sid Identifier for the Session.

OPはログアウトURLでユーザーに対して「ログアウトしますか?」と尋ねるべき(SHOULD)とあります。これを行うとユーザーの意図せぬ挙動を防ぐのと同時に、後述するトラッキング判定に対しても効果があるかもしれません。

ここまでの流れを図にしました。

f:id:ritou:20200825010654p:plain

複数のRPに接続済みのOPはログアウト後に、SessionManagementでのセッション判定で "状態変更あり" を示したり、Front-Channel/back-Channel Logoutの仕様でRPにログアウト通知を送ります。

さらに、RP->OP->RPと戻る場合はパラメータが変わります。

  • id_token_hint : OPからRPに過去に発行されたID Tokenの値をヒントとして指定。REQUIRED
  • post_logout_redirect_uri : OPのログアウト後にRPに戻したい時に指定する戻り先URL。このURLが指定されるときは id_token_hint の値が必須となる。検証の際は完全一致。
  • state : 戻り先がある時のCSRF対策のための...(略。必須でいいだろ(OPTIONAL)!
  • ui_locales : OP上での優先言語、スクリプトの指定。OPTIONAL

f:id:ritou:20200825010923p:plain

この場合は id_token_hint も必須になりますが、ここまでやるならAuthZ Requestのように client_id などを利用する方が良いのではないかと個人的には思います。

この仕様の概要としてはこんな感じです。

Metadata

OP/RPのURLの扱いが書かれています。

OpenID Provider Metadata

OpenID Provider Metadata では RP が Logout Endpoint を Discovery できるように、end_session_endpointという値を定義します。

Client Registration Metadata

OP が RP-Initiated LogoutとDynamic Registrationをサポートする場合、post_logout_redirect_uris という値を事前に登録します。

ITP, SameSite Cookieとセッション関連仕様への影響

ちょうど先日、OpenID TechNight vol.17 ~ with コロナ編でiframeを使うユースケースへの影響について説明されていました。

https://www.youtube.com/watch?v=0856pHMoTDs

その中で、iframeを使っているために影響が考えられる仕様として2つが紹介されていました。

  • Session Management
  • Front-Channel Logout

それぞれの仕様にもこの影響については言及があります。

  • Session Management - 5.1. User Agents Blocking Access to Third-Party Content「RPコンテキストで呼び出したOPのフレームがCookieにアクセスできず、ログイン状態が変更されたという応答を返し、再認証されまくるかも」
  • Front-Channel Logout - 4.1. User Agents Blocking Access to Third-Party Content「frontframe_logout_uriがOPによってレンダリングされた時にRPのログイン状態にアクセスできなくてログアウトされないかも」

今回のRP Initiated Logoutでは、OPはLogout EndpointへのアクセスでGET/POSTをサポートしなければならない(MUST)とあります。 よって、POSTパラメータとなると2つのパターンの前者ではOP、後者ではOP/RP両方でログアウト機能に関連するHTTP CookieのSameSite属性を意識する必要があります。

そう言えばチャチャッとSameSite属性毎の挙動を確認するためのツールを用意しましたので良かったら記事を読んでみてください。 medium.com

また、ログアウトしますか?のようなユーザーインタラクションを省略すると今度はトラッカー判定の方にも関連してきそうなので、この辺りの仕様とブラウザのHTTP Cookie, トラッカー判定事情の関係は今後も注意深くみていく必要がありそうです。

まとめ

  • RPからOPへのログアウト要求を送る方法が定義されている
  • OPからRPに戻るかどうかで2種類の挙動がある
  • POSTの場合はHTTP CookieのSameSite属性に気を付けろ

昔SessionManagementあたりのサンプル実装をしたことがあったのですがどっか行ってしまったので、またどこかのタイミングで動作確認ができる環境を用意したいと思います。

ではまた!

f:id:ritou:20200821032656p:plain

認証機能を独自実装する代わりにIDaaSのREST APIを使うアプローチ

こんにちは、ritou です。

最近のあれこれでIDaaSと呼ばれる機能に注目が集まっているような気がしますが、どうしてもフロントエンドでの導入部分が目に付きます。 「新規サービスで使っていこう」ならまだしも「既存のを何とかしたい」みたいな場合にフロントエンドまでごっそり変えるのなんて腰が重くなって仕方ない感じでしょう。

そこで今回は、REST APIを用いた新規導入、移行というアプローチもあるのかなという話を書いておきます。

IDaaS の REST API

この辺りをみてみてはどうでしょう。

APIベースで登録/ログイン/パスワード変更など結構揃っている印象です。 Firebaseだとソーシャルログインも実現できそうですし、Auth0のはまだあんまりじっくりみてないですがMFAとかも書いてあります。 あとはCognitoなんかもAPIいっぱいありますよね。

導入方法

このようなREST APIでは

  • UIを自前で用意
  • 受け取った値をさばくバックエンドの処理をREST APIで実現

という感じになりますね。それこそDeviseでやってたところをこのREST APIを使うパターンです。 サービスの種類は違いますがAuthleteはこれで認可サーバーを作っていく形ですね。

SPAかどうかにもあまり依存せずに使えそうですし、フロントにIDaaSのSDKを入れてガッツリUIレベルまでIDaaSに従う実装よりも自然に導入できそうな気がします。 私自身、自前でいくつか認証機能、基盤の開発をやってきた中でまだガッツリこの方式で作り込んだことはないんですが、やるとしたらこのアプローチかなと考えています。

IDaaS導入における不安要素にIDaaSが落ちたらどうする問題があるかと思うんですが、例えばREST APIを用いて取得したユーザー情報を "外部サービスであるIDaaSからの認証結果" と捉えてセッション管理などを自前にする設計パターンもあるんじゃないかと。 万が一IDaaSが落ちてたとしてもエラーハンドリングというかConfig設定とかで一時的にIDaaSに流さないようにし、登録済みメールアドレスなどを用いて自前でリカバリーというか暫定的な認証機能を提供するような感じならそれほどリスクなくできそうかなー、いやでもやっぱめんどくせーなーなどと思いながら引き続き検討してみるつもりです。

IDaaSにとってそれを使うサービスはどんな役割なのかを意識しよう

例えばOpenID Connectでいうところで ”IDaaSがOP、利用サービスがClient" という立場であるように考えがちです。 今回紹介したREST APIを使うイメージとあっているでしょう。

しかし、フロントエンドのSDKなどを使う場合、セッション管理のあたりまでJSでできてしまいます。 IDaaSの責務でセッション管理までする場合、利用するサービスが行うべきことは「現在ログイン中のユーザーを把握してそれに対するリソースアクセスを提供する」ということになり、役割としては利用するサービスのSPAがClient、バックエンドサーバーがResource Serverとなります。

IDaaSを効果的に使っていくためにはこの辺りも意識して設計/実装を進める必要があるでしょう。

まとめ

  • REST API結構ある
  • フロントエンドをあまり使わずにバックエンドで導入していくアプローチもあるのではないか
  • 自分はClientを作るのかResource Serverだけにするのかというレベルの検討も必要そう

おまけ

Firebase AuthnのAPIに「OAuth認証情報でサインインする」というのがあります。

https://firebase.google.com/docs/reference/rest/auth#section-sign-in-with-oauth-credential

いわゆるソーシャルログインをAPIベースでやろうとするものですね。

過去にネイティブアプリとバックエンドサーバーをOAuth 2.0のClient/Serverとして作り込まれたプラットフォームに関わっていたときに、ネイティブアプリでのソーシャルログイン実装でこのような独自拡張を実装したことがあります。

メンテナになってるPerlのOAuth 2.0 Serverライブラリである OAuth::Lite2 にはその痕跡が残されています。やりたい放題でした。

Changes - metacpan.org

0.09 2014-07-22T07:36:43Z
    - rename grant_type to external_service
    - rename params for grant_type=external_service
 
0.08 2014-06-03T07:54:41Z
    - add support for new grant types
        - "urn:ietf:params:oauth:grant-type:federated-assertion" : obtain access_token from external service assertion

GrantTypeにソーシャルログイン用のものを作成して、例えばGoogleなどから受け取ったAT/RT/AuthzCode/IDTokenなどを送ってログインさせようというものです。 オレオレ拡張仕様のメモ:

social_login_bearer_profile_for_oauth2.md · GitHub

このように、APIベースでもソーシャルログインなど実装できるものはたくさんありますので、今回紹介したREST APIを使ったアプローチを検討してみてはいかがでしょうか。

ではまた!

f:id:ritou:20200819131651p:plain

Capability URLsをBearer Tokenと捉えた場合のJWT適用の可能性

f:id:ritou:20200812192943p:plain

こんばんは。ritouです。 少し前に、このようなスライドを見かけました。

docs.google.com

今回はこのCapability URLsにJWTを使ってみてはいかがかなというお話をします。

Capability URLs as a Bearer Token

挙げられている特徴や要件として

  • 推測できてはいけない
  • URLとして適切な長さ(ブラウザによって処理できないやつが出てこない)

ぐらいで、"えいち、てぃー、てぃー、ぴー、..." とラジオのDJが番組内で紹介できるぐらいの短さを求められたりしないのであれば、OAuth 2.0のBearer Tokenの要件と同等に捉えることもできそうです。

qiita.com

JWTの適用

JSON Web Token、正確には JSON Web Signature を使うことで

  • 構造化されたデータを文字列にできる : リソースに一意に紐づけられるだけでなく、ちょっとしたパラメータを加えられる
  • 改ざんされていないことを検証できる : 第3者が有効なURLを生成しにくい

という特徴を利用できます。

JWTといえばステートレス信仰が湧き上がって来るわけですが、ここでは一旦おいておきます。 JWTをCapability URLsに利用する際のポイントを整理すると

  • 元々設計されているリソース識別子のエントロピーに影響しないURLが作成可能 : 任意のURLにアクセスできる確率の話は署名の方に依る
  • 同じリソースに対して細かいパラメータを加えてやれば有効期限などのちょっとした制御もできる
  • 無効化などの管理もJWT自体の識別子や全体のハッシュ値などを使えば それなりにできる

となるでしょう。 当然、

  • JWTのPayloadに含まれる情報には気をつける必要がある(見られたくないデータは含まない)
  • Payloadにたくさん入れすぎるとURLが長くなる(CWTワンチャン?)

というあたりには気をつける必要があるわけですが、十分実用に耐えうるのではないかと思います。

まとめ

  • Capability URLs も Bearer Token の要件に近いのではないか
  • JWTを適用することで得られるもの、気をつけるべき点をざっくり整理した

という感じです。 この手の機能の設計を行う際に、JWTの適用も選択肢に加えてみるのもいかがでしょうか?

それにしても私が数年前から罹患しているこの「何でもJWTで解決しようとしてしまういわゆるeyJ病」なかなか厄介です。 ではまた。

bosyuが実装したメールアドレスでの登録/ログイン機能とは!?

f:id:ritou:20200727191917p:plain

こんばんは、ritouです。

今日はこの機能を使ってみましょう。

何の話?

medium が前から採用していた方式です。

medium.com

これを新しい方式と捉えるかどうか、個人的にはそれほど新しくは感じません。

メアドでリカバリー手段を確保しつつパスワードを設定させるのが現状一般的な「パスワード認証を用いた登録フロー」だとしたらそこからパスワード設定を抜いたものがメアドによる新規登録フローと言えるでしょう。 また、パスワードを忘れた際にメアドでリカバリーする処理を認証フローとして使うことで「パスワード認証を用いた認証フロー」からパスワードの検証を取り除いたものと考えることができます。いつだったかのbuildersconでもそんな話をしたことがある気がしますね。

f:id:ritou:20200727190404p:plain

当然、この方式ではメールを受信できる環境が第3者の管理下に置かれた場合などが問題となるわけですが、それは既存のパスワード認証でもだいたい同じことが言えるでしょう。 一時的に利用できない場合もあるかもしれません。その場合は、SMSだったり他の認証方式と組み合わせる必要が出てくるでしょう。 この辺りを頭に入れつつ、見ていきましょう。

新規登録 from PC画面

PCから挙動を見てみましょう。メアドを入れます。

f:id:ritou:20200727183359p:plain

新規登録用のリンクを送りましたと出ます。 いや、出ませんね。ログイン用のリンクって書いてあります。 テンプレート間違えてるのでは疑惑がありますがまぁいいでしょう。

f:id:ritou:20200727183429j:plain

送られたメールにはボタンとリンクで新規登録を続けるためのURLがついています。 HTMLメールの扱いなどもあるかもしれませんが、メールを開く環境とbosyuを利用する環境が別だった場合はコピペしてメアドを入力した環境で処理を続けることができます。

f:id:ritou:20200727183448j:plain

アクセスすると新規登録を続けるための画面になります。

f:id:ritou:20200727183501p:plain

新規登録はこれで完了です。

新規登録 from Modile画面

次にモバイルのブラウザで開いた場合の挙動を確認しましょう。

f:id:ritou:20200727184435j:plain

メアドを入れた後の挙動が変わっています。 新規登録コードを送ったと表示され、入力フォームがあります。

f:id:ritou:20200727184449j:plain

これはメールを確認した後に再びモバイルの画面で処理を続けるために、コード入力という方式を選択しているということでしょう。 当然ながら送られてくるメールの内容もPCからの場合とは異なります。 コードを表示しつつ、リンクをコピペすることでもURLにアクセスできます。

f:id:ritou:20200727185039j:plain

mediumでも同様にPC/Mobileのブラウザ(もしくはアプリかどうかも?)を判別しているようですが、現状ではこのやり方がスタンダードと言えるでしょう。

ログイン

スクショ撮りまくりましたが、だいたい一緒っぽいので省略します。

登録状態の返答

パスワードを利用しないにせよ、メールアドレスを入力する機能となると、登録済みかどうかをどう扱うかが気になるところです。

qiita.com

bosyuの場合は、エラーを表示した後にメアドをフォームから消してくれる仕様となっております。

f:id:ritou:20200727162011p:plain

f:id:ritou:20200727161813p:plain

攻撃者視点で言うと、仮にメアド(とパスワード)のリストがあるときに登録済みかどうかを確認することで、生きているメールアドレスを抽出できるとも言えます。bosyuではパスワードを預かっていないと言うことで直接それが別の攻撃に繋がることはないため、ユーザーフレンドリーに結果をお伝えしていると言うところでしょうか。

この辺りはバランスが難しいですね。個人的には上記Qiitaの記事に書いたとおり画面ではエラーを返さない仕組みにしたいところです。

まとめ

bosyuが実装したメアドで登録/ログインの機能を確認しました。 もっといろいろな機能を持つサービスであれば「メールが急に届かなくなった」場合のリカバリーをもう少し考える必要が出てきて悩ましいところですが、比較的ライトなサービスであればこのぐらいの実装で問題ないと思います。

たくさん使われると良いですね。 ではまた!

(追記 : 前からこの辺の話はQiitaに書いてたので参考までに貼っておきます)

qiita.com

qiita.com

ユーザー名にURLやドメインを含むSNSのアカウントとメール通知による拡散行為について

こんばんは。ritouです。

f:id:ritou:20200614004856p:plain

なんの話?

ちょっと前にQiitaでこんなことがありました。

qiita.com

最近はこんなのも見かけました。

ということで、表題の通りSNSのユーザー名にURLやドメインを含み、通知用のメールが送られることで可能となる拡散行為について取り上げます。

拡散行為のターゲットは?

既存ユーザーをターゲットにした拡散行為

Qiitaの例では

  1. 悪意のある者は拡散させたいURL=ドメインを含む名前のアカウントを用意する
  2. 悪意のある者は既存ユーザーを次々とフォローしていく
  3. フォローされた人は、「〜さんからフォローされました」というような文言がメールで送られる
  4. メーラーによってドメイン部分がリンクとなりそこから誘導可能な状態となる

という流れのように見えます。

フォローやいいねを繰り返すことで、ドメインを拡散できる可能性があります。既にたくさんのユーザーを抱えるサービスなどでは注意が必要でしょう。

未登録ユーザーをターゲットにした拡散行為

後者のサービスの例では

  1. 悪意のある者は新規登録時にメールアドレスとURLを含む名前を入力する
  2. メールアドレス向けに「こんにちは ~ さん」のように名前を含むメールが送られる
  3. メールアドレスを持っている人のメーラーによってURL部分がリンクとなりそこから誘導可能な状態となる

新規サービスだからと言って安心してはいけません。 ターゲットのメアドリストを突っ込んでいくことで、URLやドメインを拡散できる可能性があります。

対策

いくつかあるでしょう。

メールで送られる可能性のある名前にURLやドメインが含まれないようにする

現実的に、こんにちは~さん、というメールはよく送られています。 登録時のユーザー名のバリデーションを厳密にやろうと思うとなかなか困難そうです。

URLやドメインを含む可能性がある"名前"を含むメールを送らない

新規登録時には、先にメールアドレスの確認を行うことで、登録完了してから名前入りのメールが行われるようになるなど、頻度は減らせるかもしれません。

URLやドメインがリンクとして扱われないようなメールにする

これも簡単ならやるべきかもしれませんが各メーラーの挙動はどうなんでしょう。

まとめ

URLの拡散行為への対策についてもう少し考えてみても良さそう(雑)

ではまた。


6/17追記

自作サービスがDDoS攻撃された話 - 週休7日で働きたい

「リンクを拡散できる」ことと「たくさんアカウントが作られたこと」のそれぞれの事象について

  • URLを弾く
  • reCAPTCHA や js などで大量アクセスを防ぐ

という考察が行われていますが、それぞれについてより深い考察が行われることが他のサービスで似たような被害が行われることを防ぐことに繋がるでしょう。

2020年版 チーム内勉強会資料その1 : JSON Web Token

おはようございます。ritou です。

5月下旬ぐらいにチーム内勉強会としてJSON Web Token(JWT)についてわいわいやりました。 その際に作成した資料に簡単な説明を添えつつ紹介します。 このブログではJWTについて色々と記事を書いてきましたが、その範囲を超えるものではありません。

ちょっとだけ長いですが、ちょっとだけです。お付き合いください。それでは始めましょう。

JSON Web Token boot camp 2020

JWTBootCamp2020 001

JWTBootCamp2020 002

今回の勉強会では、JWTについて概要、仕様紹介という基本的なところから、業務で使っていくにあたって気をつけるべき点といったあたりまでカバーできると良いなと思っています。

JSON Web Token 概要

JWTBootCamp2020 003

まずは概要から紹介していきます。

JWTBootCamp2020 004

JSON Web Tokenの定義とはということで、RFC7519のAbstractの文章を引用します。

JSON Web Token (JWT) is a compact, URL-safe means of representing claims to be transferred between two parties.

JSON Web Token (JWT) は、2者間で転送されるクレームを表現するためのコンパクトで URLSafeな方法です。

補足します。

JWTBootCamp2020 005

JWTはとにかく色々なデータ、例えば構造化されたものからバイナリデータまでを複数のサービス、システム間でやりとりするため、URLセーフな文字列にエンコードする仕組みです。 また、そのエンコード結果の文字列自体をJWTと呼ぶこともあります。

さらに、JSON Web Signatureという署名をつける仕組みを利用することで改ざん検知が可能になります。JSON Web Encryptionという暗号化を施すことにより、センシティブなデータのやりとりが可能になります。 この2つのうち、よく使われているのを見かけるのがJSON Web Signatureの方でしょう。 ということでここからはJSON Web Signatureについての解説を行います。 (ここからの文中でJWTと書いているものは署名つきのJWSを含みます。)

JWTBootCamp2020 006

そもそも今回、なぜJWTを取り上げたのかにも触れておきます。 誕生のきっかけということで、JWTはOpenIDファウンデーションのWGで行われたID連携やソーシャルログインを実現するための仕組みであるOpenID Connectの仕様策定に合わせ、IETFのJOSE(読みはホゼ) WGにて仕様策定が開始されました。

GoogleAppleでサインインの裏側で動いているOpenID Connectの中で、ID Tokenというユーザーの属性情報や認証時の情報などをやり取りする際にJWTが利用されています。 もともとSAMLというXMLベースのID連携の仕組みで使われていたXML署名という仕組みがあらゆるユースケースに対応できるようにした結果複雑になってしまったという経緯から、JWTはより容易に実装できてコンパクトに表現できるセキュリティトークンを目指しました。 私はこのOpenID Connectの仕様策定にContributorとして参加していたため、この後説明しますが2015年に複数のRFCになるまでの経緯を横目でチラ見していて最近のJWT単独の普及についても注目しております。 ということで今回、ネタとして取り上げました。

JWTBootCamp2020 007

ここからはJWTのユースケースをいくつか見ていきましょう。 サービス、システム間のやりとりに使うという点を意識して、まずはJWTの発行者、そして受信者(利用者)という観点で同一か別かで整理していきます。

JWTBootCamp2020 008

発行者、受信者が同一のユースケースとして、WebアプリケーションにおけるセッションCookieへのJWT適用があります。 (厳密にはmonorithなサービスであれば発行者、受信者が同一となりやすいしょうけれども、規模が大きくなれば発行者、受信者が別になるケースもあり得るでしょう。)

以前からHTTP Cookieにセッション情報を詰め込むという、Cookie Storeとか呼ばれてきたものがあり、以前から様々なWebApplicationFrameworkにてオレオレ署名つきエンコーディングなどで実装されていることはご存知の方が多いかもしれません。これにJWTを利用することができるでしょうというお話です。 さらに、セッションIDをHTTP Cookieに持ってDBなりでセッション情報を管理する実装についてもJWTを適用可能です。セッションIDと発行者、発行日時、有効期限などを合わせて署名つきのJWTに含むことで、ブラウザ上で手元にあるHTTP Cookieの内容をいじって挙動を調べてみるようなことを困難にできます。 処理の流れとしては、Webサーバーがセッション情報に更新があるたびにHTTP Responseとして発行されたものをブラウザがハンドリングし、次のHTTPRequestにてWebサーバーに送ります。

JWTBootCamp2020 009

さらに、WebアプリケーションのHTML Formを用いたPOSTリクエストなどで利用されるCSRF対策トークンにもJWTを適用できます。 この例の方が発行者、受信者が同一である感じが出そうですね。

CSRF対策トークンで重要なのはセッションとの紐付けです。セッションIDそのものではなく鍵を用いて生成したハッシュ値などを格納しておくのが良いでしょう。

JWTBootCamp2020 010

発行者、受信者が別であるユースケースとしては、WebAPIを利用する際の認可用トークンがあります。OAuthのアクセストークンで、発行者であるユーザー認証やアクセス管理を司るサーバー、受信者であるサービスを提供するサーバーが分かれている場合を想像してください。

JWTBootCamp2020 011

よりシンプルなユースケースとして、

  • 署名つきのAPIリクエストにJWTを適用
  • 外部サービスにユーザー情報などを渡す際に署名つきJWTを利用

というものがあります。

JWTBootCamp2020 012

ここまでゆるゆるとユースケースを紹介してきましたが、一旦署名つきのJWTを利用するメリット、デメリットを整理します。 柔軟なデータを文字列にしてやりとり可能なところ、署名とメタデータを組み合わせることで、発行者、受信者、有効期限の検証が可能となる点はとても魅力的だと思います。 一方で、暗号化ではないため、どんな値を含むのかを誰でも知ることができるという点、含まれる情報によるデータサイズの増加には気をつける必要があります。

JWTBootCamp2020 013

JWTが使われている理由として、やはり標準化の強みがあるでしょう。

仕様はRFCになり、ライブラリも各種言語で出揃っています。 OAuth/OpenID Connectをはじめとした標準化プロトコルでの採用実績もありますし、ここまで紹介したように単体で使われるユースケースも増えています。これまで独自の署名つきエンコードを使っていたところからの移行などで利用するケースもあるでしょう。

さらに、最近RFC化されたJWT BCP(Best Current Practice)のように、良くも悪くも枯れてきたと言うか、ここ抑えると安全な実装を実現できると言うのが明確になっている点も様々なところで使われている理由だと思います。

JWTBootCamp2020 014

JWTのユースケースでセッションCookieAPIアクセス時のトークンの例を紹介しました。 このあたりで混乱を呼んでいるものの一つに、Single Page Application のログイン状態の実現方法があります。

この文脈では JWT はエンコードフォーマットだけではなく WebStorage にJWT形式のトークンを保存してAPIアクセスする方式 というような解釈がされがちです。 そのような解釈のまま、HTTP CookieにセッションIDを保存してHTTP Request時に送られるやり方 と比較されたりするんですが、比較の軸が増えてしまって話がまとまりません。

  • セッションIDだけなのか色々詰め込む内包型とするのか
  • 文字列自体に情報を詰め込むのか、ブラウザである程度編集可能なHTTP Cookieの属性値を使うのか

のような比較軸の細かい整理をすることが重要です。

ritou.hatenablog.com

JWTBootCamp2020 015

そして、このCookieとの比較でもよく言及される点として、「JWT=ステートレスでなければならない」「JWTを使う場合は必要な情報を詰め込み、その内容だけで機能を実現させる必要がある」という一種の信仰のようなものがあるのではないかと考えています。

これはとても勿体無いなと感じていて、確かに情報を内包できるという特性は持っていますが、データストアなどを参照するためのキーを持ってはいけないというわけではありません。

例えば単純な識別子を用いたやりとりだったものにJWTを適用するだけで、有効期限や検証という機能を追加できる のです。この考えは既に色々なユースケースで使われているものではありますが、ステートフルなユースケースへも適用できるということを今一度意識してもらえると良いのかなと思っています。

ritou.hatenablog.com

ritou.hatenablog.com

JWTの概要は以上です。

仕様解説

JWTBootCamp2020 016

ここからは、JWTの仕様について簡単に説明していきます。

JWTBootCamp2020 017

JWTに関するコアな仕様が定義されているRFCは5個あります。 その内訳についてですが、

  • 「いつ、誰が、誰に」発行したものか、というようなサービス/システム間のやりとりで必要なメタデータを標準化したもの : RFC7519 JSON Web Token
  • 署名をつけ、検証するために必要なパラメータや実装方法 : RFC7515 JSON Web Signature
  • (今回は紹介しませんが)暗号化周り : RFC7516 JSON Web Encryption
  • 暗号化や署名利用時の鍵の種類、表現 : RFC7517 JSON Web Key
  • アルゴリズム : RFC7518 JSON Web Algorithms

となっています。全てを細かく読んでいくのは時間がかかって大変なので、よく使われている署名つきJWT、JSON Web Signatureについて要点をかいつまんで紹介します。

RFC7515 JSON Web Signature

JWTBootCamp2020 018

JWTBootCamp2020 019

この文字列をご覧ください。皆さんには普通の文字列に見えるかもしれませんが、私にはこう見えます。

JWTBootCamp2020 020

先頭が "eyJ" から始まり、"." で連結されています。

JWTBootCamp2020 021

この文字列の正体としては、RFC7515で定義されている "JWS Compact Serialization" という単一の署名を持つフォーマットであり、世の中で広く使われているのはこのフォーマットです。

仕様ではより複雑なこともできるようになっており、複数の受信者に向けてそれぞれ向けの署名を含むことができる "JWS JSON Serialization" というのも定義されているのですが、使われているのをほとんど見たことがありません。

JWTBootCamp2020 022

改めて先ほどの文字列に戻ります。 最初の eyJ から始まる文字列ですが、Encoded Headerと呼ばれています。

JWTBootCamp2020 023

これはBase64URL EncodeしたJWS Header であり、JWS Header は JSON 形式で表現されます。このJSONの開始タグのあたりで {" から始まる文字列を Base64URL Encodeした時に "eyJ" となるわけです。 内容は自身がどのような種類なのか、署名に関するパラメータを含みます。

JWTBootCamp2020 024

次にPayloadです。2番目の文字列が Encoded Payloadと呼ばれています。

JWTBootCamp2020 025

Base64URL EncodeされたJWS Payloadです。 JWS Headerとは異なり、JWS Payloadはやりとりしたいデータそのものであり、必ずしもJSON形式である必要はありませんが、「誰が」「いつ」「誰に」というような標準的なクレーム(パラメータ)の値が RFC7519 で定義されています。 これと署名検証の仕組みを組み合わせることで「改ざんされていないことが保証できる」文字列となるため、ユースケースはグッと広がります。

JWTBootCamp2020 026

最後にSignatureです。これもEncoded Signatureと呼ばれています。

JWTBootCamp2020 027

Header, Payloadと同様にBase64URL EncodeしたJWS Signatureであり、署名のアルゴリズムはRFC7518、鍵の表現がRFC7517にて定義されています。

JSON Web Signatureの特徴として、署名生成時に利用するBase Stringに対してパラメータのソートなどの正規化が不要であるという点があります。 Twitterなどで使われている OAuth 1.0 という仕組みでは署名つきのリクエストが定義されているのですが、Base Stringを生成するまでの正規化の処理はなかなか面倒です。 JSON Web SignatureではEncoded Header と Encoded Payloadを "." で連結させた値をBaseStringとして利用します。

RFC7519 JSON Web Token

JWTBootCamp2020 028

RFC7519 JSON Web Tokenについて解説します。

JWTBootCamp2020 029

Payloadに含まれるキーバリューの値をJWTクレームと呼びます。

  • JWT自身の識別子である jti
  • host名やサービス内の識別子など、JWT発行者の識別子である iss
  • JWTの受信者、利用者の識別子である aud

という「誰が誰に...」という値が定義されています。

JWTBootCamp2020 030

さらに、「いつ発行され、いつからいつまで有効なのか」を表現するための

  • 発行日時である iat
  • 有効期限 exp
  • この日時以降、有効となる開始日時である nbf

という値が定義されています。

ここまで紹介した値は「汎用的なクレームとして、識別が必要ならば使うと便利だよ。」という意味合いでこのRFC内では基本的にオプショナルです。 JWTを利用するプロトコルや各種プロファイルで「この値を必須として含む」などの定義がされます。

JWTBootCamp2020 031

最初にJWT誕生のきっかけとなったと紹介した「Googleでログイン」を実現するためのプロトコルである「OpenID Connect」でユーザー情報などを伝達するために使われるID TokenはJWTクレームを利用します。

  • issGoogleのアカウント周りを管理するドメイン
  • audGoogleアカウントを受け入れるサービスに client_id として割り当てられた識別子
  • subGoogleアカウントの識別子
  • iss, exp で有効期限を表現

という値がOpenID Connect独自で定義された値と一緒に含まれています。

JWTBootCamp2020 032

ここで紹介した値は全て利用必須なものではなく、これらを利用するプロトコルなどにより決まります。 JWTを扱うライブラリによっては、ここで定義されている値の検証を行うものもあります。 検証の粒度や利用方法については利用するライブラリのドキュメントや実装を追う必要があることに注意は必要でしょう。

RFC7518 JSON Web Algorithms

JWTBootCamp2020 033

RFC7518 JSON Web Algorithmsについて解説します。 ここではJSON Web Signatureの署名生成に利用されるアルゴリズムについて取り上げます。

JWTBootCamp2020 034

一番上にある none については、JWT自体とは別のところで署名生成/検証や暗号化が行われるようなケースで利用します。これではBase64URL Encodeぐらいしかしてないことはここまでの説明でお分りいただけるかと思います。

HSXXX となっているものは、SHA-256, 384, 512といったハッシュ関数を秘密の共有鍵と組み合わせて署名を生成し、同じ計算をして比較することで検証します。 鍵配送が不要な、JWTの発行者、検証者が同一の場合に使いやすいアルゴリズムです。

JWTBootCamp2020 035

RSXXX, PSXXXRSA公開鍵暗号方式ハッシュ関数を組み合わせたデジタル署名を利用するアルゴリズムです。 秘密鍵を用いて署名を生成、公開鍵を用いて署名の検証ができるため、発行者から受信者で公開鍵を渡したり、発行者が公開鍵を提示することで発行者/受信者が別のケースで利用できます。

JWTが使われ始めた頃は、「公開鍵暗号だったら RS256」 という感じで使われていましたが、最近はその状況も変わりつつあります。

JWTBootCamp2020 036

ESXXX公開鍵暗号として楕円曲線暗号を用います。RSAよりも鍵長の短さや処理速度の面で優れているため、最近のプロトコルではこちらを選択するものも見受けられます。

JWTBootCamp2020 037

アルゴリズムと鍵管理の関係は密です。 秘密鍵を公開して後悔しないように気をつけて利用しましょう。

RFC7517 JSON Web Key

JWTBootCamp2020 038

RFC7517 JSON Web Keyについて解説します。

JWTBootCamp2020 039

鍵に関する仕様として、鍵の表現と鍵のセットの表現が定義されています。 アルゴリズムに対応した鍵はどのように表現されるのか、そして鍵のローテーションやアルゴリズム変更時のスムーズな移行を考慮する際に鍵のセットをどう表現するかが重要となります。

JWTBootCamp2020 040

鍵表現のためのパラメータがいくつか定義されています。 このうち重要なのは、鍵の種類である kty, 対応するアルゴリズムである alg, 鍵の識別子である kid です。 他には鍵の利用用途(署名 or 暗号化)である use やさらにその鍵の使い方まで踏み込んだ key_ops があったり、X.509形式の証明書に関連するパラメータがあります。

JWTBootCamp2020 041

HSXXX で利用されるバイナリの秘密鍵kty=octとして表現されます。

JWTBootCamp2020 042

RSXXX で利用されるRSA秘密鍵kty=RSAとして表現されます。長いです。

JWTBootCamp2020 043

公開鍵は少しすっきりと表現されます。alg, kid の値も含まれていますね。

JWTBootCamp2020 044

ESXXX で利用されるECDSAの公開鍵はkty=ECとして表現されます。

JWTBootCamp2020 045

鍵のセットは keys として鍵のリストを持つことで表現されます。 これは先ほども紹介したOpenID ConnectでGoogleから受け取ったJWTの署名検証を行うために公開されている、公開鍵のセットの値です。Appleなども同様に公開しています。

JWTBootCamp2020 046

これらのユースケースとして、有効な鍵情報をjwks_urlとして公開したり、設定ファイルにこの形式で保存するといったものがあります。 弊チームでは設定ファイルなどでPEM形式などを利用していますが、コンパクトに表現できるのであればこのような形式での保存も使っていきたいと思っています。

実装のポイント

JWTBootCamp2020 047

仕様の解説は以上にして、実装のポイントを紹介します。

JWTBootCamp2020 048

ここまで紹介した仕様がRFCになってから5年ぐらいが経ち、良くも悪くも枯れた技術となってきました。 そして最近、RFC8725 JSON Web Token Best Current PracticesとしてJWT実装のベストプラクティスがRFCとなりました。

Qiitaで解説記事を書きましたのでお時間がある方はみてください。

RFC 8725 JSON Web Token Best Current Practices をざっくり解説する - Qiita

JWTBootCamp2020 049

個人ブログにも少し書いたのですが...

JWTBootCamp2020 050

JWTを安全に使っていくためのポイントとして、難しい暗号化処理以外のものとして

  • Payloadに含む情報はよく考えて決める
  • 署名検証処理を確実に
  • 複数のJWTを利用する際は、用途を指定/判別して排他的に検証する

というものが書いてあります。

JWTBootCamp2020 051

Payloadに含む情報について、チーム内の勉強会ではせっかくJWTを使っていてもPayloadに含む情報が足りず、そもそもの目的を満たせていない例を紹介しました(が割愛します)。 これはユースケースに依存するというか、必要な情報が決まるものなのでよく考えましょう。

署名検証については「ちゃんとした」ライブラリを使うことで問題は回避できますが、簡単にこんな攻撃があったよというのを紹介します。

個人的には、最後の用途のあたりが一番重要かと思っているのでスライドのページを割いて紹介します。

JWTBootCamp2020 052

JWTBootCamp2020 053

JWTの署名検証時の脆弱性として有名なのが、alg とSignatureの値を改ざんして検証をパスさせようとするものです。 JWT側の alg に沿って署名検証をしてしまうと、alg=noneを指定してSignature部分を取り去って送られたものを受け入れてしまったり、alg=RS256からHS256に変えられて公開鍵を共有秘密鍵としてハッシュ値をSignatureに指定されたりしたものを受け入れてしまう例が報告されています。

これらを回避するためには、JWTに含まれるalgをそのまま署名検証に使うのではなく、kidに紐付く鍵に紐付くalgを署名検証に利用することが重要です。 その両者が異なる場合はJWTの改ざんが考えられますが、これは署名検証で検知できます。

JWTBootCamp2020 054

ここからはJWTの用途と署名検証について説明します。

ポイントは

  • 用途の表現と指定
  • 鍵の管理
  • 検証

です。

JWTBootCamp2020 055

JWTで用途を表現するために使えるパラメータはいくつかあります。

まずはHeaderのtypです。 例として、Googleが連携済みのサービスにユーザーのセキュリティイベントをJWT形式で通知する仕組みがあって、そこでは secevent+jwt という値を指定しています。 次に、Headerのkidで用途ごとに鍵を分けるという運用もあります。 他には、Payloadにuseusageのような値を含んで用途を表現するというやり方があります。

ritou.hatenablog.com

JWTBootCamp2020 056

これらのどれを使うかについては、利用するサービスの設計やライブラリの状況から柔軟に判断すべきです。

JWTBootCamp2020 057

例えば機能単位で完全に鍵をわけられるような場合はkidに用途を含むことで排他的に検証が可能でしょう。

JWTBootCamp2020 058

鍵の管理には手を出せないがHeaderを自由に指定できる場合はtypの値を利用できるでしょう。ちなみに、ライブラリでこの検証をするものもあります。

JWTBootCamp2020 059

Payloadしか指定できない場合はPayloadに独自の値を指定する必要があるでしょう。 ライブラリでもサポートされていない部分なので、実装漏れがないように気をつける必要があります。

最後に紹介しますが、JWTの署名検証では Header -> Signature -> Payload という順番に扱っていくため、署名検証の処理回数が多い場合はHeaderの参照だけで判断できる方がPayloadの値を参照するよりも負荷がちょっとだけ抑えられるのではと思います。

JWT生成/検証デモ

JWTBootCamp2020 060

最後にJWTの簡単な生成と検証のデモをやってみます。 (予想通り時間もなかったのでざっと流しました。)

JWTBootCamp2020 061

目的としては、簡単なやつでもこれまで紹介した仕様について手を動かすことで理解が深まるかなと言うところです。 業務では、ライブラリを使いましょう。Elixirでは KittenBlue っていうのが便利ですよきっと。

JWTBootCamp2020 062

ここではJWT用のライブラリを使わず、これらの3つの機能を使ってやってみます。 Base64URL Encode/Decode は padding のオプションがあったりするので注意が必要です。

生成

JWTBootCamp2020 063

生成の流れを説明します。 一言で言うと、Header, Payload の値を生成した後、Signature の値を生成して連結します。

1つずつ見ていきましょう。最初はHeaderです。

JWTBootCamp2020 064

ここでJWTに含むHeaderパラメータは3つです。

  • typ ではハンズオンっぽい文字列を用意して、用途を判別できるようにします
  • alg 署名アルゴリズムにはHMAC SHA-256を使います
  • kid 鍵管理を意識しましょうと言ったはずだ

JWTBootCamp2020 065

まずは JSON Encode します。 それを Base64URL Encode すると Encoded Header が出来上がりです。

JWTBootCamp2020 066

次にPayloadです。 ここでは送りたいデータとしてこんな感じのを用意します。 せっかく紹介したんだからRFC7519で定義されている iss とか aud とか exp の値を使えばよかったなと後から思いましたがとりあえずこれでヨシ!

JWTBootCamp2020 067

Headerと同様に、JSON Encode と Base64URL Encode したら Encoded Payload の出来上がりです。 次はSignatureを生成します。

JWTBootCamp2020 068

Signature生成のもとになる文字列は、Encoded HeaderとEncoded Payloadを "." で連結させたものです。正規化不要!簡単ですね。

JWTBootCamp2020 069

今回は鍵として "THIS_IS_SAMPLE_KEY_FOR_JWT_HANDSON" とかを用意して、それで HMAC-SHA256 した値をBase64URL EncodeするとEncoded Signatureの出来上がりです。

JWTBootCamp2020 070

最後に連結させます。 Elixirだとこんな感じです。

iex(1)> encoded_header = %{"alg"=>"HS256", "kid"=>"handson01", "typ"=>"handson+JWT"} |> Jason.encode!() |> Base.url_encode64(padding: false)
"eyJhbGciOiJIUzI1NiIsImtpZCI6ImhhbmRzb24wMSIsInR5cCI6ImhhbmRzb24rSldUIn0"

iex(2)> encoded_payload = %{"Foo"=>"Bar", "Hoge"=>"Fuga"} |> Jason.encode!() |> Base.url_encode64(padding: false)
"eyJGb28iOiJCYXIiLCJIb2dlIjoiRnVnYSJ9"

iex(3)> signature_base_string = encoded_header <> "." <> encoded_payload
"eyJhbGciOiJIUzI1NiIsImtpZCI6ImhhbmRzb24wMSIsInR5cCI6ImhhbmRzb24rSldUIn0.eyJGb28iOiJCYXIiLCJIb2dlIjoiRnVnYSJ9"

iex(4)> encoded_signature = :crypto.hmac(:sha256, "THIS_IS_SAMPLE_KEY_FOR_JWT_HANDSON", signature_base_string) |> Base.url_encode64(padding: false)
"Tp0zcg2nEA1r94EijoymQTTVMwH6iaLoOpxEZf3KcVM"

iex(5)> jwt = encoded_header <> "." <> encoded_signature <> "." <> encoded_signature
"eyJhbGciOiJIUzI1NiIsImtpZCI6ImhhbmRzb24wMSIsInR5cCI6ImhhbmRzb24rSldUIn0.Tp0zcg2nEA1r94EijoymQTTVMwH6iaLoOpxEZf3KcVM.Tp0zcg2nEA1r94EijoymQTTVMwH6iaLoOpxEZf3KcVM"

公開鍵暗号系のalgを利用する場合は、Headerのパラメータで指定してSignature計算のところで適切な署名生成関数を利用します。

生成については割と簡単なので、Qiitaなどで生成を自前でやるのを見かけますが、複数箇所でハードに使っていくとなると共通処理がまとめられ、結果ライブラリを使うのと変わらなくなると思います。

検証

生成ができたので検証もやってみましょう。

JWTBootCamp2020 071

検証の場合、順番が変わります。

Signatureの検証のために先にHeaderの値を参照する必要があり、Payloadの内容を参照するのはSignature検証の後となります。

JWTBootCamp2020 072

生成と逆の手順で、Base64URL DecodeしてJSON Decodeして Header パラメータを取得します。ここで含まれる値であれば、

  1. typ パラメータの値で用途の検証
  2. kid の値が有効なものかどうか
  3. alg の値が kid に紐づいた値と一致しているかどうか

と言う検証を行います。

先ほど述べたように、生成時に alg の値を含まず、省略して3もスキップしても良いでしょう。

JWTBootCamp2020 073

次にSignatureの検証をします。 Base StringはEncoded Header と Encoded Payload を "." で連結したものです。

JWTBootCamp2020 074

kid に紐づく鍵で生成時と同様に値を計算し、Base64URL Encodeした値と Encoded Signature の値を比較します。

JWTBootCamp2020 075

Signatureの検証を終えたら、Payloadの値を参照します。 ここからはJWTを利用するサービスごとによしなにと言うところです。

RFC7519で定義されている iss, aud, exp などのクレームを含む場合はここで検証します。

検証の手順は以上です。

外部サービス

jwt.io というJWTの生成/検証を行う外部サービスがあり、JWTのデバッガがついています。

jwt.io

ここまでで生成したJWTを入れて鍵情報も入れてやると、署名検証を行い成功したことがわかります。

JWTBootCamp2020 076

公開鍵暗号を使うalg=RS256も紹介したかったですが時間がなさそうなのでやめました。 興味がある方は試してみてください。質問などありましたらいつでもどうぞ。

JWTのデバッガは、他にもmicrosoft製のjwt.msなどがあります。 jwt.ms: Welcome!

外部サービスなので、本番環境で単体でAPIアクセスに使えるようなJWTを入力したり、業務で扱う秘密鍵を指定しての署名検証などは避けましょう。

まとめ

  • JSON Web Tokenの概要を紹介しました
  • 様々なサービスで使われているJSON Web Signatureと関連する仕様を紹介しました
  • 実装で気をつけるべきポイントを紹介しました
  • ハンズオン的にJWTの生成/検証を行う方法を紹介しました

初学者向けにまとめたつもりでしたが、もっとユースケース毎にこんな情報をこんな感じで含むといいよねみたいな話や実装についてももうちょっと紹介できると良かったかなとちょっと反省しています。


と、こんな感じでした。 今後もユーザー認証とかリソースアクセスとか、おじさんの守備範囲でその3ぐらいまではやってみたいと思っています。

speakerdeck.com

ではまた!

「ID TokenをAuthorization Headerにつけて送る」というお作法について思うところ

こんばんは、ritouです。

f:id:ritou:20200517193756p:plain

ID Tokenがやりとりされている背景

ちょっと前にこんな話がありました。

blog.ssrf.in

この id_token が JWT になっていますので、これを Authorization: Bearer $ID_TOKEN というヘッダにして oauth2-proxy で保護されているアプリケーションへ送信するだけです。

docs.aws.amazon.com

Authorization ヘッダー (または、オーソライザー作成時に指定した別のヘッダー) に ID トークンを含めます。

この「ID TokenをAuthorization Headerに指定して保護されているっぽいリソースにアクセスする行為」は一体何なのかというお話です。

ある有識者はOAuth 2.0のProtected ResourceをID Tokenで保護することについての投稿をしました。

medium.com

いわゆる3legged(ユーザー、AuthZ/Resource Server, Client)でこういう実装をすると上記リスクがあるのは当然でしょうが、世の中にはセッショントークンとしてID Tokenと名乗る物を使うユースケースが存在します。

ritou.hatenablog.com

IDaaSのような認証基盤を利用するとき、

  • (セッショントークンである)ID TokenをID Token発行元のサービスに送り、何らかの操作を行う

という処理が発生するのだろうと考えています。

OSSなものでそのような実装が行われる際には「これってリソースアクセスでは?ID TokenじゃなくAccess Tokenでやるべきでは?」なんて意見が出るようですが、「いや、こっちはリソースサーバーじゃねーし(セッショントークン相当のID Token送るでヨシ!)」みたいな感じになってるようでした。

カオスなままで良いのか?そもそもセッショントークンの扱いとは?

上記のような現状について「リソースサーバーじゃないからそうか。仕様で定義されていない物ならしょうがないか」となっている意見も見受けられます。 仕様がないからしょうがない。 特定の小さな領域でのみの作法に止まるならばそれでも良いでしょう。 しかし、この話はIDaaSを使わない、単純な1st PartyなNativeApp/SPAとバックエンドサーバーの実装にも影響するのではないかと気になっています。

例えばですが、

これはどうでしょうか。

1st Party なアプリとバックエンドでやりとりにOAuth 2.0を適用!ってのをやったことがある開発者であれば、その間は Access Token を用いたAPIアクセスとなるでしょう。 しかし、今後上記のようなIDaaSのお作法から入った開発者の場合、ID Tokenを使ってAPIアクセスを行うという可能性もあるのではないでしょうか? 暗黙的にこれらの2つの実装が広く使われ始めると、開発者の混乱を生みそうです。そうでもない?

この辺りで自分の認識はどうかというと、むかーし、こんな記事を書きました。

ritou.hatenablog.com

この時は今よりも舌ったらずなので内容もいまいち何言いたいかわからんし、今では説明に使うべきではないと思っている「認証」「認可」なんていうワードを軽々しく使っているため皆さんの反応もパッとしない感じでしたが、ブラウザとWebサーバーとのやりとりもざっくりいったら特殊なクライアントとリソースサーバーとのやりとりと同じような意味合いであり、セッションクッキー/セッショントークンは「凄まじい権限を持った」アクセストークン相当であろうというお話です。 当時はブラウザはUserAgent何だからClientではない、みたいなご指摘をもらったりもしましたが、SPAの場合はClient相当がちゃんといるのでなおさら意識しやすくなったのではないかと思ったりもします。

現状に対する自分の考えるあるべき姿としては「どんなユースケースであれ、ID TokenじゃなくAccess Tokenでやれ」という考えであり、中身に含まれる情報の話や処理についてなど、これから色々足りない部分を補足していきたいところであります。