怖くないネイティブアプリケーションにおけるID連携機能を実装するための考え方

f:id:ritou:20191202101535p:plain

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

qiita.com

3日目です。やっていきましょう。

ネイティブアプリのID連携

「今年の汚れ今年のうちに」なんていうフレーズがあります。

記憶が確かではないですが、たしか 7 月ぐらいにどこかの決済サービスによりネイティブアプリのID連携に注目が集まったことがありました。

  • バックエンドサーバーがあるネイティブアプリでID連携(ソーシャルログイン)の機能を実装してた
  • ネイティブアプリからバックエンドサーバーに Identity Provider の識別子、ユーザー識別子の組み合わせを送ることで認証状態にしてた
  • 推測したり総当たりなどで...可能性があった

というお話でした。

この記事では "各Identity Providerが提供している SDK などを使ったり使わなかったりしながら安全に認証機能を実現するための方法" を整理します。

Webアプリとの違い

昨日、こんな記事を書きました。

ritou.hatenablog.com

Webアプリではセッションと OAuth/OpenID Connect の各リクエスト/レスポンスを紐付けることで、安全なID連携機能を実装できます。

f:id:ritou:20191118004418p:plain

この考えをネイティブアプリにも適用していくことを考えましょう。

細かいところはおいといて、Clientをネイティブアプリとバックエンドサーバーに分割します。

f:id:ritou:20191203043602p:plain

ここではまず バックエンドサーバー -> ネイティブアプリ -> Identity Provider -> ネイティブアプリ -> バックエンドサーバー という処理の流れを意識することが重要です。

  1. ネイティブアプリとバックエンドサーバー間では独自でセッションが確立している
  2. バックエンドサーバーがID連携のための "チャレンジ" 的な値を生成し、セッションに紐付ける
  3. ネイティブアプリは Identity Provider に "チャレンジ" を含むID連携のリクエストを送る
  4. Identity Provider は "チャレンジ" の値を含むID連携のレスポンスを生成してネイティブアプリに送る
  5. ネイティブアプリはバックエンドサーバーにID連携のレスポンスを送る
  6. バックエンドサーバーは "チャレンジ" の値を含むID連携のレスポンスを検証する

汎用的な説明とするため、あえて "チャレンジ" という表現をしていますが、"チャレンジ" を引回すといえば最近だと WebAuthn の認証フローも構成要素が違うだけで一連の流れは似ています。認証認可のプロトコルでは基本となるやり方だと覚えておくのが良いでしょう。

ここからは、これを実現するための具体的な実装を整理していきます。

大手 Identity Provider が提示する方法

各 Identity Provider がドキュメントで示している実装方法は微妙に異なります。

自分はこれが最強だと思うという実装方法があっても、Identity Provider がサポートしていない機能は使えません。

特に複数の Identity Provider を相手にする必要がある場合はその差異を自分のところで吸収していく必要があるため、気をつけなければなりません。

この辺りの細かいことを考えたくない人は、最初から Auth0 / Firebase とかに魂を売り渡す方が平和に過ごせるでしょう。

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

LINE : Access Token / ID Token

LINE は Access Token, ID Token を用いた連携方法を案内しています。

developers.line.biz

LINE は各種SDKAccess Token / ID Token を検証するエンドポイントを提供しており、ドキュメントを図にするとこんな感じです。

f:id:ritou:20191203092005p:plain

このやり方では、最初に紹介した バックエンドサーバー -> ネイティブアプリ -> Identity Provider -> ネイティブアプリ -> バックエンドサーバー という処理の流れが実現できません。

Access Token を利用する場合、検証APIを叩くことで

  • Access Token の発行を要求した client_id
  • Access Token の有効期限である expires_in

を取得できるため、攻撃者が異なる Client 向けに発行された Access Token , 有効期限の切れている Access Token を指定しても検知可能です。 しかし、ID Token の nonce 相当の値を検証できないため、バックエンドに送られる Access Token が有効な間は同じリクエストで何度もID連携の認証が可能な状態になり得ます。

LINEはAccess Token を無効化するエンドポイントも用意しており、無効化することで Acces Token の検証に失敗するようになるのであれば、認証だけが目的の場合にはログイン成功後にそれを利用するのも一つの手かもしれません。

一方、ID Tokenには nonce の値を持っていますが、ドキュメントにあるやり方では nonce の値を SDK が作成しています。 これでは SDK を用いて連携フローを開始した際のセッションと ID Token が紐付けられていないため、nonce を用いて一度連携に利用した ID Token を再度利用できないように制限するぐらいしかできません。

より安全にするためには、事前にバックエンドサーバーが nonce の値を生成しセッションに紐づけておき、ネイティブアプリは SDK を利用する際にその nonce の値を指定し、最後にバックエンドサーバーが ID Token の nonce の値をセッションに紐付けられているものと比較します。

f:id:ritou:20191203094857p:plain

ちょっと前に調べたときは iOS Swift SDK あたりで nonce の指定ができなかったのですが、新しいバージョンでは外部で生成した nonce を指定できるようになった模様です。

LINE のID連携を利用しているサービスの開発者の方は参考にしていただければと思います。

Google : ID Token

Google のドキュメントではバックエンドサーバーとのやりとりにID Tokenを使えと案内しています。

developers.google.com

developers.google.com

Warning: Do not accept plain user IDs, such as those you can get with the ...

最初にユーザーIDだけでやるなと警告があります。

こちらも説明では一切 nonce に触れられていないため、LINE と同じ話になりそうですが、SDKsetIdTokenNonce という関数があるのでそれを使って バックエンドサーバー -> ネイティブアプリ -> Identity Provider -> ネイティブアプリ -> バックエンドサーバー という流れを実現できそうです。

Yahoo! JAPAN : Authorization Codeフロー

Yahoo! JAPANでは Authorization Code フローを使い、各種トークン取得をバックエンドで行うように案内しています。

developer.yahoo.co.jp

Authorization Code Grant を使う場合、バックエンドサーバー -> ネイティブアプリ -> Identity Provider -> ネイティブアプリ -> バックエンドサーバー という流れを実現できます。

f:id:ritou:20191203101213p:plain

良さそうですね。

まとめ

Binding の仕組みを生かしたネイティブアプリとバックエンドサーバーのID連携の実装について紹介しました。

Sign In with Apple も ID Token あたりでどうにかできそうですし、この辺りを押さえとけば大丈夫かと思います。

これから同じことを考えるときは

  • Identity Provider がどのようなID連携方法を提示しているか
  • SDK に nonce の値をセットできるか
  • Authorization Code Grant が利用できるか
  • ID Token を取得できるか

あたりを調べて、できるかぎり安全なID連携の実装方法を探っていくのが良いと思います。

認証機能でやらかすとサービスが終わる可能性もあります。我々はあのPayの悲劇を繰り返してはいけません。

認証認可技術 Advent Calendar 2019 明日は @authyasan が OAuth 2.0 の Device Flow について書いてくれそうです。楽しみですね。

意識高めすぎて3日やるとか言ってちょっと後悔しましたが穴開けなくて良かったです。

ではまた。