OAuth for Native Apps

GREE Advent Calendar 9日目は @nov が担当します。

僕は GREE ではセキュリティ部に所属しており、社外では OAuth や OpenID Connect などの Identity 関連技術についての翻訳や講演などを行ったりもしています。

今日は GREE Advent Calendar ということで、Native App コンテキストでの OAuth の話を少し書いてみようと思います。

はじめに

Native App を開発していると、Backend Server とのやりとりや Facebook Login や Google Sign-in などで、必ずと言っていいほど OAuth 2.0 というのが出てきます。

OAuth 1.0 と異なりリクエストに署名が不要だったり、Client Secret (a.k.a Consumer Secret) 無しでも Access Token が取得できたりと、シンプルで Native App でも使いやすくなったと言われる OAuth 2.0 ですが、実はまだまだ実装に不備のあるケースが散見されます。

そこで、以下では特に実装上の問題を多く見かける Native App での OAuth 2.0 利用ケースについて、良く見かけるパターンから本来あるべき姿へと少しずつ改善して行くことにしましょう。

想定ユースケース

まず最初に、以下のようなフローを想定ユースケースとします。

Native App w/o PKCE

このユースケースでは、End-User のデバイス上にインストールされた Native App が同一 Developer により開発・運営されている Backend Server (App Backend) と連携してサービスを提供しています。

また Facebook や Google などの外部 OAuth Server (ID Provider) を使ったログインに対応し、Native App は ID Provider が発行した Access Token を App Backend に POST することで、App Backend から該当ユーザー向けの Access Token を取得します。

その後は以下3つのパターンの API リクエストが繰り返され、それぞれで OAuth 2.0 が利用されることになります。

  • Native App と App Backend
  • Native App と ID Provider
  • App Backend と ID Provider

特に明示的なユーザー登録を必要としないことの多いゲームアプリでは外部 ID Provider と連携したユーザー認証を行うケースは少ないですが、ゲーム以外のアプリでは良く見かけるパターンですね。

注意点1: Custom URL Scheme 上書き攻撃

上記のようなケースでは、ID Provider から Access Token を受け取る際に、Custom URL Scheme を利用することが多いでしょう。しかしながら、同じ Custom URL Scheme を複数のアプリが登録可能であることから、該当 Custom Scheme 経由で開かれたアプリが本来期待されたアプリと異なる可能性があります。

この穴を突くと、他の Native App が利用している Custom URL Scheme を乗っ取る攻撃アプリが成立します。攻撃者は、そのような攻撃アプリを被害者にインストール & ログインさせることで、ID Provider および App Backend が発行した被害者アカウント向けの Access Token や、App Backend に保存されている被害者のデータへのアクセスを得ることができます。

解決策1-1: Custom URL Scheme の利用をやめる

iOS9 および Android M 以上であれば、それぞれ Universal LinksApp Links といった HTTP/HTTPS リンクへのアクセスをトリガーにして OS が自動的に特定アプリを開いてくれる機能が提供されているので、こういった環境では Custom URL Scheme を利用せずに HTTP/HTTPS リンクを用いることで、Custom URL Scheme 乗っ取り攻撃を防ぐことができます。

この解決策は非常にシンプルで良いのですが、iOS9 はまだしも Android M がひろまるにはまだまだ時間がかかりそうなので、しばらくは古い OS 用に該当 HTTP/HTTPS URL から Custom Scheme を開く Fallback HTML Contents を返却するなどの対策が必要で、完全に Custom Scheme URL の利用をやめることは難しいでしょう。

解決策1-2: OAuth PKCE 拡張を利用する

実は Custom URL Scheme の乗っ取り対策として、先日 (2015年9月) IETF OAuth WG より OAuth PKCE (RFC7636) という拡張仕様が RFC 化されています。

PKCE では、Authorization Request 送信毎に Code Verifier と呼ばれる Secure Random を生成し、その SHA256 ハッシュ値を Authorization Request に添付します。また ID Provider からのレスポンスでは、直接 Access Token を渡すのではなく、Authorization Code を経由して Access Token の発行を行います。OAuth PKCE では、Access Token 発行時に元の Code Verifier を Authorization Code と同時に送らせることにより、最初に Authorization Request をスタートしたアプリ (その時点まで Code Verifier を知っている唯一のアプリ) だけが Access Token の発行を行えることを保証しています。

PKCE 拡張を採用した場合、当初のフローは以下のようになります。Implicit Flow ではなくなりましたね。

Native App w/ PKCE

Custom URL Scheme 上書き攻撃への対策としては、上記2つの解決策を併用し、Universal Links & App Links を用いた上で、Fallback の Custom URL Scheme 利用時にも PKCE 拡張により攻撃アプリに ID Provider の Access Token が渡らないようにしましょう。

PKCE をサポートしていない ID Provider では、Fallback 時の Custom URL Scheme 上書き攻撃に対しては対処しきれませんが、PKCE は「OAuth Client が PKCE 対応していない OAuth Server に PKCE パラメータを投げた場合、PKCE パラメータをつけていないのと同じように動く (= エラーにはならない)」ように設計されているため、Native App としてはとりあえず PKCE パラメータをつけておいて、ID Provider 側が PKCE サポートしてくれるよう要望を投げるといったことも可能です。

注意点2: Embedded Browser の利用によるフィッシングリスク増大

iOS では Apple が外部 Safari を立ち上げるアプリをリジェクトしていたこともあり、Authorization Request 送信時に System Browser ではなく Embedded Browser (WebView 等) を利用するアプリが多いです。しかしながら ID Provider 側としては、3rd-party App で WebView 内にログイン画面を開くケースが増えてくると、(おそらくはあなたのアプリではなく他の悪意ある誰かのアプリにより) フィッシング被害に繋がるユーザーが増えることが懸念されます。

WebView などの Embedded Browser では、開かれている URL や SSL 証明書の確認ができなかったりするため、ユーザーが Embedded Browser 内のログインフォームに慣れてしまうと、偽の Facebook Login ページを開く攻撃アプリによってフィッシング被害にあったりするケースが増大します。

解決策: SFSafariViewController / Chrome Custom Tab を利用する

iOS9 や Chrome v45 以上を搭載した Android では、
SFSafariViewControllerChrome Custom Tab といった、アプリコンテキストを外れることなく System Browser 相当の User Agent が利用可能になっています。これらの User Agent では、URL や SSL 証明書の確認が可能なこと以外にも、System Browser との Cookie 共有によりユーザーが ID Provider にログイン済な状態で Authorization Request が行えることによる Login CVR 向上などのメリットも期待できるため、いま Embedded Browser を利用しているアプリも、徐々に SFSafariViewController / Chrome Custom Tab に移行することをお勧めします。

当初のフローは、これにより以下のように変わります。ただこれは Native App 開発者からすると、WebView が SFSafariViewController に変わっただけで、大きな変化とは感じないかもしれませんね。

Native App w/ PKCE + Browser View

なお、この問題については Google Identity Team の William Dennis を中心に、IETF OAuth WG で OAuth 2.0 for Native Apps という Best Practice 仕様が提案されています。この仕様書では、前述の PKCE についても触れられているので、Native App で OAuth 2.0 を利用する Developer の方は一度読んでみることをお勧めします。

なお、SFSafariViewController / Chrome Custom Tab が利用できない環境では、外部 System Browser を立ち上げることになるでしょう。この Fallback が必要な過渡期では、この問題により引き起こされる社会的問題と1アプリ単体の UX との兼ね合いで WebView を採用せざるをえないケースもあるかとは思いますが、徐々に SFSafariViewController / Chrome Custom Tab の採用が進むことを期待します。

注意点3: App Backend への Token 置換攻撃

上記すべてのフローで、App Backend は ID Provider が Native App 向けに発行した Access Token を受け取り、ID Provider が提供する Profile API から該当ユーザーの User ID を取得し、ユーザーを認証しています。

しかしながら、Native App の世界では、App Backend といえども「そのリクエスト送信者が正規の Native App であること」を保証するのは非常に困難です。そのため、攻撃者が直接 App Backend にリクエストすることを禁止するのも、また困難となります。

この弱点を突き、「ID Provider の Profile API にアクセスできる "他のアプリ向けの" Access Token を App Backendに送りつける攻撃」が存在します。

OAuth 2.0 の Access Token は Bearer Token と呼ばれ、「Token 所有者が誰であろうが、その Token を持っていることのみが Token を利用する条件」となります。よって、別アプリ向けの Token を渡された App Backend も、その Token を ID Provider の Profile API に送りつけさえすれば、その Token に紐づく User ID を取得することができてしまいます。攻撃者の運営するサービスに被害者が与えてしまった Access Token が App Backend に送られてきた場合でも、ID Provider の Profile API は被害者の User ID を返すのです。

当然その User ID を元にユーザーを認証すると、攻撃者は「あなたが運営するサービス上で、被害者のアカウントにログイン」できてしまいます。開発元があやしげな占いアプリなどに発行された Access Token が大量に存在することを考えれば、この攻撃パターンは決して無視できるものではありません。

解決策3-1: Token Introspection API を利用する

Token 置換攻撃は、「他のアプリに発行された Access Token を受け付けない」ことで対策が可能です。しかし OAuth 2.0 の Access Token は、基本的には Token を見るだけではどのアプリに発行されたものかは不明です。そのため、大手 ID Provider はその Token がどのアプリ向けに発行されたものかを調べるための API を提供していることが多いです。

こういった API のことを、"Token Introspection API" と呼びます。

この Token Introspection API を利用し、App Backend に送られてきた Access Token が本当に App Backend (or Native App) 向けに発行されたものなのかをチェックすることで、Token 置換攻撃を防ぐことができます。

Token Introspection のステップを追加すると、以下のようなフローになります。Backend での API リクエストが増えましたね。

Native App w/ PKCE + Browser + Token Introspection]

Token Introspection に関しては、OAuth WG で先日 RFC 化された RFC7662: OAuth 2.0 Token Introspection という仕様も参考になるでしょう。(少し RFC本来の想定ユースケースが異なりますが)

解決策3-2: Bearer Token を利用した ID 連携をやめる

Bearer Token とは、そもそも発行相手 (audience) が誰かを気にせず使うものです。しかし、Token 置換攻撃の例をみてもわかる通り、ID 連携を行う場合「その Token がどのユーザーに紐付いているか」と同様に「その Token がどのアプリに対して発行されたものか」というは非常に重要です。

ID 連携のコンテキストでは、Token に紐付いたユーザーのことを Subject、Token 発行対象の Client を Audience、Token 発行者を Issuer と呼び、Token 検証時にはこの3つをチェックする必要が有ります。Token 置換攻撃は、Audience のチェック (Audience Restriction) の不備を付いた攻撃の1例です。

すでに OpenID Connect に触れたことがある方はお気づきでしょうが、OpenID Connect が OAuth 2.0 に追加している ID Token というものは、Token 自体に Issuer、Audience、Subject を署名付きで含んでいます。これはまさしく Bearer Token を用いた ID 連携の本質的問題を解決する手段となります。

残念ながら、最大の ID Provider である Facebook は OpenID Connect をサポートしていませんが、OpenID Connect が利用できる Google や Yahoo! Japan などの ID Provider と連携する場合には、OAuth 2.0 単体ではなく OpenID Connect を利用すると良いでしょう。

OpenID Connect を利用すると、Token Introspection API を利用する代わりに ID Token の署名検証を行えばよいです。フローとしては少し単純になるのがお分かりでしょう。(詳しい ID Token の検証方法については OpenID Connect Implicit Client Implementer's Guide 1.0 (翻訳版) に従ってください)

Native App w/ PKCE + Browser + ID Token

User ID 自体は ID Token に含まれているため、User ID 以外のプロフィール情報が不要な場合は Profile API にアクセスする必要すらなくなります。

なお、Token Introspection API も提供しておらず、OpenID Connect もサポートしていない OAuth Server は、(少なくとも ID Provider としては) Native App の世界では利用できないでしょう。

一旦まとめ

いかがでしょう、思ったより Native App で OAuth 2.0 を利用する際に注意すべきことが多いなと感じられた方が多かったのではないでしょうか?

今回取り上げたケースでは、本来 OAuth 2.0 が想定していた End-User、OAuth Client (Native App)、OAuth Server (ID Provider) という3者以外に App Backend という4者目が存在するため、OAuth Core に従っているだけでは Native App と App Backend の間で脆弱性が生まれることが多いです。特に最後の Token 置換攻撃は、2012年から知られている問題であるにもかかわらず、いまでも Facebook Login を採用している Native App に非常に多く見受けられるパターンですので、もしみなさんの会社でそのようなアプリがある場合には、一度実装をチェックしてみることをお勧めします。

最後に、近い未来の話をもう少し

今回の想定ユースケースであった Native App x App Backend というケースは、IETF OAuth WG や OpenID Connect の仕様策定を行っている OpenID Foundation でも非常に注目度の高いターゲットです。

今回は詳しくは触れませんが、現状の OAuth 仕様では、上記で挙げた問題に対応しつつ以下のような要望を全て満たせるようなセキュアな仕様は策定されていません。

  • App Backend <-> ID Provider 間では Client 認証を行いたい
  • Client 認証なしで Access Token を取れる Native App と Client 認証必須の App Backend では Access Token の Scope や有効期限に差をつけたい
  • Native App and/or App Backend で有効期限の長い Token (Refresh Token) が欲しい

いま現在は、OAuth WG と OpenID Foundation で策定されている各種拡張仕様を見ていると、以下のような方向性に進んでいるようです。

  • Client 認証なしでも Native App には Refresh Token を発行する
    • これは今でも可
    • Refresh Token 発行には PKCE 必須としても良いでしょう
  • Native App -> App Backend には ID Token を送信する
    • 該当 ID Token は PKCE Verifier と紐づけられている ([ACDC Extension](https://bitbucket.org/openid/napps/src/c22a2adb3f66f7a34fb599285720498782390f7d/draft-acdc-01.txt?at=default&fileviewer=file-view-default) 参照)
  • Refresh Token を発行するため App Backend でも Token Endpoint にアクセスする
    • ID Token と Access Token の交換には [Token Exchange](https://tools.ietf.org/html/draft-ietf-oauth-token-exchange) を利用
    • Onetime-only & Client 認証必須になるでしょう

Native App w/ Backend (future)

このようなケースについても、いつかどこかで議論できる場所を用意したいなと思います。例えば #idcon とか。

では、明日の Liang さんの記事もお楽しみに!

References