OAuth認証とは何か?なぜダメなのか - 2020冬

f:id:ritou:20201108013816p:plain

こんばんは。ritouです。

Digital Identity技術勉強会 #iddance Advent Calendar 2020 1日めの記事です。

qiita.com

初日なのでゆるふわな話をしましょう。

何の話か

もうだいぶ前ですね。9月のお話です。こんなTweetを見かけました。

このbotに対する思うところはもう良いです。

今回は、「OAuthの仕様に沿ってID連携を実装するいわゆる"OAuth認証"の実態に迫り、なぜダメなのか、そして代わりにどんな方法があるのか」 を振り返ります。

(追記)長いので結論から先に

  • OAuthとOIDCで 目的が違う ことはよく知られている
  • OAuthのリソースアクセスの仕組みだけを利用してクライアント側がID連携の実装を試みても最低限の要件を満たせず、クライアントの構成によっては認可サーバー、リソースサーバー側の拡張が必要となる場合がある
  • ID連携のためには「●●でログイン」と言う認証イベントを実施したユーザーの情報を "安全に" やりとりする仕組みが必要
  • TwitterGithub, Facebook安全に利用できるフローを示したり、不足している部分を独自に拡張したりしてOAuth認証を可能にしている
  • OpenID ConnectではOAuth認証で独自実装になり得る部分を標準化仕様としてサポートしているのでご利用ください

それでは始めましょう。

OAuth認証とは?そもそもOAuthとは?

OAuthは異なるサービス間での「APIアクセスのためのアクセストークンの発行(取得)」や「アクセストークンを用いたAPIアクセス」と言う、APIの保護を目的として標準化された仕様です。

雰囲気でOAuthやっちゃってた勢に人気の本を意識しながら、OAuthの登場人物から振り返ってみましょう。

f:id:ritou:20201127103729j:plain

  • 認可サーバー : アクセストークンを発行するサービス(例:Google アカウント)
  • リソースサーバー : アクセストークンを利用したリソースアクセスの対象なるサービスで、認可サーバーと一緒でも別でも良いが実際のサービスでは同一サービス内の別機能的な立ち位置が多い。(例:有料化が話題となったGoogle フォト)
  • クライアント : 認可サーバーからアクセストークンを取得し、リソースサーバーにリソースアクセスを要求するサービス(例:画像編集アプリ)
  • リソースオーナー : リソースサーバーが提供するリソースの所有者(例:Googleのユーザー)。ユーザーの場合もあるし、クライアント自身がリソースオーナーになるパターンもある。

処理の流れを見ていきます。

f:id:ritou:20201127103748j:plain

  1. クライアント(画像編集アプリ)は認可サーバー(Google アカウント)にリソースサーバー(Google フォト)で管理されているデータへのアクセスを要求
  2. 認可サーバーがリソースオーナー(Googleアカウント)を認証し、クライアントへのリソースアクセス許可についての同意を得る
  3. 認可サーバーはクライアントにアクセストークンを発行
  4. クライアントは、アクセストークンを用いてリソースサーバーにあるリソースオーナーの画像を返すAPIにアクセスし、取得した画像を編集対象として利用

OAuthの特徴としては

  • APIはアクセストークンにより保護される
    • 3rdパーティーなクライアントでも有効なアクセストークンを持っていたらリソースオーナーの代理としてアクセスOK
  • 認可サーバーがリソースオーナーの認証とアクセストークンの発行を行うため、3rdパーティーなクライアントは直接リソースオーナーのクレデンシャル(パスワードなどの認証に必要な情報)を扱う必要がない。よって安全!

となります。ここまでがOAuthの説明です。

そして、OAuth認証とは、 クライアントが「●●でログイン」のようないわゆるID連携、ソーシャルログインをOAuthの仕組みを利用して実現すること です。

f:id:ritou:20201127103809j:plain

  1. クライアントは認可サーバーに リソースオーナーの情報へのアクセス を要求
  2. 認可サーバーがリソースオーナーを認証し、クライアントへのユーザー情報提供についての同意を得る
  3. 認可サーバーはクライアントにアクセストークンを発行
  4. クライアントは、アクセストークンを用いてリソースサーバーにあるアクセストークンからリソースオーナーの情報を返すAPIにアクセスし、取得したリソースオーナーの情報に含まれるユーザー識別子など を自サービスの認証に利用する

このような手順を踏むことで、クライアントは認可サーバー/リソースサーバーから 「ログインしようとしたのは誰か」 を知ることができ、自身の認証に利用できるでしょうという考えですね。

何がいけないのか?

たまに 「OAuthは認可の仕組みであり、認証の仕組みではないのでOAuth認証はダメ」 という言い方を目にしますが、もう少し踏み込んで整理しましょう。

ID連携の最低限の要件

「●●でログイン」しようとしたユーザーの識別子を取得する ことはID連携における、最低限の要件です。 もっと複雑な用件については後述するとして、ここではOAuthの仕様を利用してその要件を満たす実装ができるのかどうかを確認しましょう。

繰り返しになりますが、OAuth 2.0の最も基本的な仕様では

  • アクセストークンをこうやって取得できます(RFC 6749)
  • アクセストークンをこうやって利用できます(RFC 6750)

というのが定義されており、逆にいうとそれしか定義されていません。 よって、クライアントがOAuthを用いてID連携をしようと思うと

  • 「●●でログイン」しようとしたリソースオーナーのアクセストーク を取得しよう
  • アクセストークンを使ってプロフィールAPIなどからユーザー識別子を取得して認証に利用しよう

という考えになるわけです。

(認可サーバーがアクセストークンと一緒にユーザー識別子を返すような実装も見かけましたが、拡張となるので今回はおいておきます。)

アクセストークンの役割とのミスマッチ

ここで、”OAuth認証" を用いたWebアプリケーションのID連携でよくある実装を例示します。 よく知られているOAuth 2.0の認可コードフロー を利用します。

f:id:ritou:20201128231132j:plain

  1. Webアプリであるクライアントがサーバサイドのライブラリなどでアクセストークンを取得する
  2. リソースサーバーにあるプロフィールAPIからリソースオーナーの情報を取得する
  3. 取得したリソースオーナーのユーザー識別子を用いて認証する

この流れを安全に実装することは、可能です。

  • OAuth 1.0であったり、OAuth 2.0の「認可コードフロー」を用いたアクセストークン取得までの一連の流れで 「●●でログイン」しようとしたリソースオーナーのアクセストークンであること が担保される
  • アクセストークン取得後にリソースアクセスを行い、 「●●でログイン」しようとしたリソースオーナーのユーザー識別子であること が担保される

と言うことで、「●●でログイン」しようとしたリソースオーナー の情報をハンドリングできる場合は問題ありません。

次に、クライアントはモバイルアプリとバックエンドサーバーで構成されている場合の実装を例示します。

f:id:ritou:20201128231146j:plain

  1. モバイルアプリがSDKなどでアクセストークンを取得する
  2. モバイルアプリはバックエンドサーバーにアクセストークンを送信
  3. バックエンドサーバーはリソースサーバーにあるプロフィールAPIからリソースオーナーの情報を取得する
  4. バックエンドサーバーは取得したリソースオーナーのユーザー識別子を用いて認証する

(これ以外に 「モバイルアプリ」はただのリダイレクタとなり「バックエンドサーバー」がアクセストークンを取得し、プロフィールAPIにアクセスする という実装も考えられますが、OAuth 2.0ベースの仕組みだと割と一般的にモバイルアプリケーションやSPAなどでアクセストークンの取得まで可能なSDKが用意されている場合が多く、そこから拡張しやすい上記の例のような実装を見かけることが多いため取り上げました)

先ほどのフローにモバイルアプリとバックエンドサーバーのやりとりが紛れ込んだだけなので問題なさそうに思える実装ですが、まさにそのモバイルアプリとバックエンドサーバーのやりとりにおいて、「●●でログイン」しようとしたリソースオーナーのアクセストークンであること」 が担保されなくなります。

具体的には、第3者によって次のようなアクセストークンがバックエンドサーバーに送られる可能性があります。

  • 別のクライアントに対して発行されたアクセストーク
  • 同一クライアントを利用する別のユーザーに対して発行されたアクセストーク

RFC6750で定義されているOAuth 2.0のBearer Tokenの仕組みが利用されている場合、リソースサーバーはリクエストに指定されたアクセストークンが有効であればレスポンスを返します。

このような構成において、「●●でログイン」しようとしたリソースオーナーであること」をアクセストークンだけで担保することはできるのでしょうか?

OAuthにおいてアクセストークンの役割は

  • リソースオーナーは誰か
  • リソースオーナーの代わりにアクセスするクライアントは誰か
  • どのリソースにアクセスできるか
  • いつまで有効か

ぐらいの情報を リソースサーバー が理解し、リソースアクセスの要求の検証、リソースアクセスのレスポンス生成に利用することです。 JWT形式のアクセストークンとかいう仕様もありますが、基本的にはクライアントに情報を伝えるためのものではない ため、クライアントがこれらの情報を取得するためには、リソースサーバーがプロフィールAPIもしくはアクセストークンに紐づけられた情報を返す専用のAPIとして提供する必要があります。このAPIのレスポンスを検証することで、 "別のクライアントに対して発行されたアクセストークン" であることを検知可能です。

しかし、"同一クライアントを利用する別のユーザーに対して発行されたアクセストークン" を検知することは困難です。

これを検知するためには「クライアントのどのセッションと紐づいているか」という部分を認可サーバー側がアクセストークンに紐づけておき、さらにAPIとして提供する必要があります。

このようにクライアントだけではなく認可サーバー、リソースサーバーも独自拡張などの工夫が必要となることから、ID連携の最低限の要件でもOAuthにおけるアクセストークンによりカバーできる範囲を超えたものであり、OAuthの仕様を独自に拡張するなどの対応が必要です。

複雑な要件への対応

繰り返しになりますが、ここまで紹介してきた 「ユーザー識別子が取れたら認証に利用できる」 という要件、これはあくまでID連携を最低限の要件と言えます。

実際に異なるサービス間のID連携を考えていくと、いくらでも要件が出てきます。 例えば、様々なサービスが多要素認証を導入している中でID連携を行う際に、ID連携と自サービスで認証を要求するタイミングや認証強度を揃えたい場合もあるでしょう。

  • クライアントからの認証要求
    • 多要素認証必須で頼む
    • 多要素認証したかどうか、方法の連絡を頼む
    • このリソースオーナーに対して再認証を頼む

みたいな要件が出てくることもあるでしょうし、よりサービス同士が密なユースケースでは

  • セッション関連の要求
    • 自動でセッション同期させたい
    • 手動でセッション同期してるか確認したい
    • 一緒にログアウトさせたい

みたいなセッションに関する話も出てくるかもしれません。

認可サーバー上でのリソースオーナーの認証処理 について、OAuthではノータッチですし、クライアントが認証処理を行った後の認可サーバー-クライアントのセッション管理 についても当然触れられていません。これらを実現するためには、またまた認可サーバー、クライアントの両方が独自で拡張する必要があるでしょう。

オレオレプロフィールAPI

ここまでの流れで "OAuth認証" には リソースオーナーの情報を返すプロフィールAPIが必要 でした。 しかし、OAuthで定義されているのはAPIアクセスのリクエスト/レスポンスヘッダぐらいであり、このようなユーザー識別子を含むリソースオーナーの情報のやりとりは定義されていません

今までも様々な仕様でユーザー識別子、メールアドレス、電話番号などを返す標準化されたAPIが定義されてきましたが、その仕様策定における思惑ってものがあるのです。わかりますよね?

標準化の隙間が引き起こす事態

上記の複雑な要件への対応や独自のプロフィールAPIについて、標準化されていない状態のままでは各サービスが似て非なる実装を行ったりして互換性のないID連携方法の乱立を引き起こします。(現実は標準化された仕様があってもなかなか足並み揃わなかったりしますけどもなかったらもっとカオス)

某OmniAuthなどのようにそこを吸収するライブラリで解決するという方法もありますが、そもそも標準化されたOAuthを使ってる意味が...ってなってきますし、最悪の場合は脆弱性を作り込んでしまう場合があります。

そんなに面倒なら "OAuth認証" を使わせてるサービスはどうしてるの?

例として、TwitterはOAuth 1.0ベース、GitHubFacebookはOAuth 2.0ベースでID連携の仕組みを提供(認可サーバー、リソースサーバー側)しています。Twitterはサービスの規模からもはやTwitter認証として居座っていますし、GitHubは開発者が使うサービスが多いので実装する側も割と意識して作っている印象です。

FacebookはOAuth 2.0以前からFacebook Connectと題してID連携の仕組みを提供しておりそれ自身がOAuth 2.0のベースとなった経緯もあり、上記の課題についても独自路線を貫いていますが、新たにOAuth認証を提供したいサービスが真似しようとしてももはや無理なところまで行っちゃってます。

このような大きなサービスであれば監視の目も多く、脆弱な点についての指摘が行われ、常々対応されていく感じですがそこまでの規模ではないサービスがOAuth認証の認可サーバー、リソースサーバーをなんとなく実装すると逆に指摘されるタイミングが遅れ、脆弱な点が残る可能性もあるでしょう。

ID連携を目的としたプロトコル

ここからは、"OAuth認証" と比較されがちな、最初からID連携を目的としたプロトコルについて見ていきましょう。

  • OpenID Connect : OAuth 2.0がID連携のために進化したぞ!OAuth 2.0で定義されているAPI保護の仕組みに ID連携に必要な機能を追加したプロトコルです。
  • SAML : エンタープライズで実績抜群!XMLだしプロトコルの詳細はめんどいんだけどそれを埋めるプロダクトもたくさんあるので要は金で解決できるやーつ

大人の事情により ここではOpenID Connectについて上記の課題にどう対応しているかを整理します。

OpenID Connect はどうなのか

ここからは上記の "OAuth認証" が持つ課題について、OpenID Connectがどう解決するかを紹介します。用語は OAuth 2.0 あたりのものを利用します。

"認証イベントの情報" をIDトークンとしてやりとり

OpenID Connectではアクセストークンとは別に、IDトークンというものを発行します。

IDトークンには

  • 誰が : リソースオーナーの識別子 "sub"
  • いつ : リソースオーナーが自身の情報へのアクセスに同意した日時 "iat"
  • どこで : 認可サーバーの識別子 "iss"
  • 誰に : クライアントの識別子 "aud", "azp"
  • どれぐらいのリソースに対して : アクセス対象となるユーザー情報の範囲 "scope"

と言った「●●でログイン」しようとしたイベントに関する情報、さらに

  • リプレイアタックを防ぐためのクライアントのセッションに紐づく値 "nonce"
  • リソースオーナーがいつ認証したか、認証方式、どの基準の認証レベルか "acr", "amr"

という値や、

  • 氏名
  • 生年月日
  • 住所
  • メールアドレス、確認済みかどうか
  • 電話番号、確認済みかどうか

と言った、クライアントが要求したリソースオーナーのプロフィール情報を含められます。

IDトークンは(OAuth認証に欠けている)認証イベントに関する情報を JWT(JSON Web Token) と言う仕組みで署名をつけたり暗号化してクライアントに渡すための仕組みであり、署名検証にいよる改ざん検知、暗号化による情報漏洩対策もやろうと思えばできます。

クライアントはこのIDトークンを用いて、"ログインしようとしたのは誰か" を把握し、認証処理を安全に実装できます。 "●●でログイン" を実現するだけであれば、アクセストークンを用いたAPIアクセスを行う必要もありませんし、モバイルアプリとバックエンドサーバーの構成でもこのIDトークンを用いることで安全に実装できます。

f:id:ritou:20201128234715j:plain

Webアプリなクライアントでは、リソースサーバーへの問い合わせなしで認証処理が可能です。 細けぇ話で言うと認可レスポンス(OIDCでは認証レスポンスと呼ぶ)もしくはアクセストークンと共に取得できます。

f:id:ritou:20201128234729j:plain

モバイルアプリ+バックエンドサーバーなクライアントでは、IDトークンを検証することで 「●●でログイン」をしようとしたリソースオーナーであること を担保できます。 一連の流れになっていることを担保するために、nonce と言う値が利用できます。このあたりは去年のアドカレで書いた気がするので良かったらどうぞ。

ritou.hatenablog.com

拡張された認可(認証)リクエス

上記IDトークンで認証イベントの情報を含めるために、認可サーバーへのリクエストも拡張できるようになっています。

  • 認証方式や認証強度を要求したり
  • メールアドレスや電話番号への個別の情報単位で要求するリソースを表現したり
  • それらを(リソースオーナー自身でも)改ざんできないようにJWSで署名つきにしたり

と言ったことが可能です。

OAuth 2.0では様々な拡張仕様が検討されており、これらと重複しているものもありますが、 OIDCでは 最低限のものからある程度複雑化した要件を持つID連携に対しても、必要な機能 が定義されています。

よって、OIDCではIDトークンの利用を含めてと合わせてOAuth認証ではできなかった複雑な要件への対応が可能です。

標準化されたUserInfo API

OIDCではプロフィールAPIも定義されています。

OAuth認証ではクライアントがユーザー識別子をぶっこぬいて認証に利用するためのAPIと言う立ち位置でしたが、IDトークンにその用途を譲り、UserInfo APIではプロフィール情報の同期などを目的としてアクセストークンを用いて最新のリソースオーナーの情報を返すAPI として定義されています。

豊富な拡張機能

それ以外も、セッションに関する拡張仕様や最近はよりセキュアな仕組みが求められる金融サービスに対するプロファイル(FAPI)などの仕様策定が続けられています。

いやいや、結局OAuth 2.0への後付けじゃねーの

それはそうです。 昔からID連携に求められる要件ってのは決まっている中で、どうやったら新規/既存のサービスに普及するかが重要です。

OpenID も前のバージョンなどでは OAuth と全く別の仕様で実装されてきたものの、最新の仕様であるOpenID ConnectはOAuth 2.0ベースで進められることになりました。

「Identityレイヤーを被せた」などとも表現されますが、OpenID Connectは OAuth 2.0を用いたOAuth認証で標準化が必要な部分を定義した仕様 と言えます。

まとめ

今回は "OAuth認証" について振り返りました。

  • OAuthのリソースアクセスの仕組みだけを利用してID連携の実装を試みても最低限の要件を満たせず、クライアントの構成によっては認可サーバー、リソースサーバー側の拡張が必要となる場合がある
  • ID連携のためには「●●でログイン」と言う認証イベントを実施したユーザーの情報を "安全に" やりとりする仕組みが必要
  • TwitterGithub, Facebook安全に利用できるフローを示したり、不足している部分を独自に拡張したりしてOAuth認証を可能にしている
  • OpenID ConnectではOAuth認証で独自実装になり得る部分を標準化仕様としてサポートしているのでご利用ください

と言うあたりで、開発者はどうすれば良いかと言うと

  • ID連携を提供したいサービスは OpenID Connect に対応する
    • オレオレ実装をする場合は "非互換性" を認識し、リスク分析をした上で、SDKやライブラリ、ドキュメントなどを用意して安全に利用してもらえるようにする
  • ID連携を利用したいサービスはそのサービスが OpenID Connect に対応しているかどうかを確認する
    • オレオレ実装の場合、意図せぬ脆弱性を踏まないように純正SDKやライブラリ、ドキュメントを確認する
    • OpenID Connectに対応している場合も、そのサービスがどれぐらい仕様に対応しているかを確認する

てな感じではないでしょうか。

今後も、"OAuth認証" というキーワードを聞くことがあるかと思います。 正しい理解を広めて平和な世の中を目指していきましょう。

私のアドカレ次回作は12/7です。ではまた!

qiita.com

ここで質問受け付けてるので気軽にどうぞ!

marshmallow-qa.com