👓

OAuth ClientのWebアプリ,APIのエンドポイントの設計

2021/02/17に公開

ritou です。

今回はOAuth Client側のエンドポイントの設計について書きます。
この話です。

https://twitter.com/ritou/status/1360473751331033089

世の中に溢れている "Rails + Devise + OmniAuth 入れてこうやってこうすれば動きますよ(ドヤ" な感じでも困らないなら別に良いと思いますが、 今回は少し細かくみていきましょう。

実装する必要がある機能

OAuthのClientが実装する必要がある機能は今までも何度か書いています。

  1. 認可リクエストを作成してセッションと紐付ける
  2. Authorization Server にリダイレクトして、Clientが要求するリソースアクセスに対するリソースオーナーの許可を得る
  3. Client に戻ってきて Access Token/Refresh Token を取得する
  4. リソースアクセス

みたいな感じです。

ここで、ソーシャルログインとかが頭にあると「未ログイン状態でセッションに紐付け?」とか考えがち ですが、基本的なところから理解したいのであれば 認証用途を考えずに「リソースアクセスのための紐付け」を純粋に実現する 設計から考えてみる方が良いでしょう。

その場合、現在ログイン中のユーザーとの紐付け を行うことになるので、

  • セッションに紐付けて認可リクエストを生成し、Authorization Serverにリダイレクト : 1, 2の処理
  • Authorization Serverからリダイレクトされてきた認可レスポンスを検証して処理する : 3の処理を行い、DBなどに保存する(必要ならば4もやる)

という2つのエンドポイントを ログイン状態 で提供する必要があります。

ここまででもう、ほとんど終わった感がある。

対象となるアプリケーションの種類とその違い

上記のエンドポイントを実装する対象として、次の3つを想定します。

  • Webアプリ : 直接User-Agentとお話しする
  • SPAのバックエンドサーバー : User-Agentとお話しするSPAとお話しする
  • ネイティブアプリのバックエンドサーバー : ネイティブアプリとお話しする

お話する相手によって分類した理由としては、例えばリダイレクトの表現について

  • WebアプリケーションではHTTP Response として表現(302でリダイレクトみたいに言われるやつ)
  • ネイティブアプリの場合、エンドポイントは単純にURLを返しアプリがそのURLを適切に処理する
  • SPAは両方を扱えたりする

あたりの違いがあるでしょう。
セッションの表現も同様に

  • HTTP Cookie
  • HTTP Cookie以外のトークン文字列

とか細かい違いがあります。

この辺りを柔軟に扱えるようにするためには、コアな機能であるURL生成、セッションに保存するデータの生成と薄く切り離して考えていくことが大事でしょう。 というところであえて分類してみました。

各機能の設計

認可リクエスト生成、セッション紐付け、リダイレクト

ここまでの流れから、「認可リクエストのURLを生成してセッションに紐づけ、認可サーバーにリダイレクト」 という要件を満たす設計としては

  • リダイレクト先のURLとセッションに紐づける値を生成
    • OAuth 1.0系のRequestToken取得、OAuth 2.0でも事前にAuthZServerと通信が必要な拡張を使ってる場合はここでやる
  • 引回す値をセッションに紐づける
    • OAuth 1.0系のRequestToken、OAuth 2.0のstateやPKCE関連の値など
  • アプリケーションの種類によって、認可リクエストのURLにHTTP Responseでリダイレクト or JSONなりのレスポンスに含む

という3段階になるでしょう。

一番最初の部分をライブラリなり自作するなりして、残りは想定するアプリケーションの種類に応じてよしなに実装すれば良さそうです。

認可レスポンスの検証、トークン取得、ユーザーとの紐付け

次に、「Authorization Serverからリダイレクトされてきた認可レスポンスを検証して処理する」 部分ですが、

  • 認可レスポンスの値を取得
    • 利用するWAFなどに依存する
  • セッションに紐づけられている値を取得
    • ここも利用するセッションの仕組みに依存する
  • 認可リクエスト/レスポンスが同一セッションに紐づけられているかの検証
    • CSRF対策、リプレイアタック対策など
    • WAFにおける いわゆるコントローラ側の善意に任せるのではなく、仕組みとして必須にしておくべき
  • 各種トークン取得
  • ユーザーとの紐付け
  • セッションとの紐付け解除

という処理が行われます。

認可レスポンスの値の取得やセッションに紐づけられている値の取得については書いてある通りです。

それらの検証はライブラリでサポートされていないことも多いですが、非常に重要なので自分でこの辺りのモジュールを作成したり、各種ライブラリを使うにしてもラッパーを用意するなどして確実に行われるようにしておくのが良いでしょう。

ログイン状態でのエンドポイントと最初に書いた通り、ログイン中のユーザーを把握できている状態なのでそれに各種トークンを紐づけるのはそれほど難しくはないでしょう。(本来は細かい設計が必要な部分ではありますが、別の記事として書きたいと思います。)

登録や認証用途に利用する場合との違い

最初の方で

基本的なところから理解したいのであれば認証用途を考えずに...

と書きましたが、どういうことかを書いておきます。

単純にログイン状態のユーザーとOAuthで取得した各種トークンを紐づけることに対し、外部アカウントを用いた新規登録やログインでは次のような違いがあります。

  • 新規登録
    • 未ログイン状態でのセッション管理
    • **取得した各種トークンが既にユーザーと紐づけられていないかの確認
    • 新規に作成したユーザーとの紐付け
    • ログインセッション生成
  • ログイン
    • 未ログイン状態でのセッション管理
    • 取得した各種トークンと紐づけられているユーザーの参照
    • ログインセッション生成

みなさんは「Googleアカウントで登録/ログインする機能を実装します」みたいなこと軽々と言っちゃうわけですが、実際はOAuthの機能以外にいろいろやることがあります。
大手のライブラリを使ってこの辺をすっ飛ばすのは楽で良いですが、仕組みを理解したい場合はまずは単純なユーザーとの紐付けの部分がどう実装されているかというところから見ていくのが良いでしょう。

リソースアクセスについて

これはまた別の記事として書きます。

まとめ

  • OAuthのClientが実装する必要がある機能を説明した
  • コアな機能と、アプリケーションの種類に依存する機能を整理した
  • どのように実装すべきかを書いた
  • 新規登録、ログインに使う場合は追加で考えることがあって複雑なんだよってのを書いた

自分のところのアプリケーションがソーシャルログインやAPI連携しているって人は一度振り返って、どんな仕組みで動いているのかを見てみるのも良いでしょう。
ではまた。

Discussion