Help us understand the problem. What is going on with this article?

色々な OAuth のフローと doorkeeper gem での実装

More than 5 years have passed since last update.

OAuth って言われたら、大体の人は Facebook でログインとかを選ぶと、「〜があなたの以下の情報にアクセスしようとしています」みたいな文章が出てきて OK するやつを想像すると思います。
実は OAuth には、上記のフロー以外にも、アプリケーションにユーザのリソースへのアクセスを認可するためのフローが存在します。

今回 OAuth と doorkeeper についていろいろ調べたので、備忘録も兼ねて各認可フローを簡単に説明しつつ、doorkeeper gem を使って OAuth の各認可フローを試してみます。

補足:
- 本投稿中の OAuth とは、すべて RFC 6749 で定められている OAuth 2.0 のことです。
- 理解不足&説明を簡単にするために正確でないことがあるかもしれませんので、詳細については参考に挙げたページをご確認ください。OAuth に詳しい方はぜひいろんな突っ込みください。
- doorkeeper gem の wiki には各認可フローの実現方法が書いてあります。
- initializers/doorkeeper.rb を書き換えた時はアプリケーションを再起動してください。

参考:
- RFC 6749 The OAuth 2.0 Authorization Framework
- 上記の日本語訳

用語

用語を定義しておきます。

用語 説明
認可 保護されたリソースへのアクセスを許可すること
認証 ユーザ認証の認証
リソースオーナー 保護されたリソースへのアクセスを許可するもの(要するにエンドユーザのこと)
クライアント リソースオーナーの許可を得て、保護されたリソースをリクエストするアプリケーション(要するに Facebook とかに対する自分のアプリ)
認可サーバ リソースオーナーの認証と、アクセストークンの発行を行うサーバ(要するに自分のアプリに対する Facebook とか)
リソースサーバ リソースオーナーの保護されたリソースを持っており、クライアントからのリクエストに応じてリソースを渡すサーバ。認可サーバと同じでも良いし違っても良い。

そもそも OAuth とは?なにがうれしいの?

例えばあるアプリケーションが Facebook からリソースオーナーの情報を取得したい場合、そのアプリケーションはリソースオーナーの Facebook アカウントとパスワードを預かることになります。そうすると以下の様な問題が生じます。(RFC にはもっと色々書いてあります。)

  • アプリケーションが後の利用のためにアカウントやパスワードを平文、あるいは複合可能な形で保存することになる
  • リソースオーナーは、アプリケーションが Facebook 上でやれること、取得できる情報を制限できない

OAuth では、Facebook がクライアント(アプリケーション)とリソースオーナーの認証を行い、リソースオーナーが許可したらクライアントにアクセストークンを与えます。クライアントはアクセストークンを用いて Facebook にリソースオーナーのリソースを要求します。なのでリソースオーナーのパスワード等をクライアントに渡す必要がありません。またクライアントにアクセス権を与える範囲をリソースオーナーが指定できるようにすることで、前述の問題がなくなります。

認可フロー

認可フローとは、要するにクライアントにアクセストークンを与えるまでのフローのことです。RFC では 4 つの認可フローが定義されています。

認可コード(Authorization Code)

認可コードはいわゆる OAuth のフローです。ざっくりと流れをまとめます。

  1. クライアントがリソースオーナーを認可サーバにリダイレクトする
  2. 認可サーバはリソースオーナーを認証する
  3. 認可サーバはリソースオーナーに、クライアントによるリソースへのアクセスを許可してよいか尋ねる
  4. リソースオーナーがアクセスを許可したら、認可サーバは認可グラントとともにリソースオーナーをクライアントの指定された場所にリダイレクトする
  5. クライアントは 4 で受け取った認可コードと引き換えに、認可サーバに対してアクセストークンを要求する
  6. 認可サーバはクライアント ID とクライアントシークレットを用いてクライアントを認証し、認可コードを検証する
  7. 6 で問題がなければ認可サーバはアクセストークンをクライアントに送る
  8. クライアントはアクセストークンを使ってリソースサーバにリソースオーナーのリソースをリクエストする

テスト

では doorkeeper で認可サーバを作成し、この流れを試してみましょう。doorkeepr のインストールは README に従ってやってみてください。あと適当に User モデルも作っておいてください。devise とか使っても良いですが、今回はユーザ認証が主題ではないので、認証できなくても良いです。

initializers/doorkeeper.rb に以下の設定を書きます。

initializers/doorkeeper.rb
Doorkeeper.configure do
 ...
  resource_owner_authenticator do
    User.first
  end
  ...
end

先ほど行ったとおり、認証が主題ではないので、ユーザの認証は行わずに最初のユーザを返すようにしています。

次に doorkeeper をインストールしたアプリを起動し、oauth/applications/new にアクセスします。Name には適当なアプリケーションの名前を、Redirect uri には http://lvh.me と入れておきます。この後 Application id と Secret が表示されたら認可サーバの準備は OK です。

クライアントは作成せずに、pry(irb)上でアクセストークンが取得できるところまでテストします。oauth2 gemを使うのでgem install oauth2 でインストールしておいてください。

require 'oauth2'

# CLIENT_ID と CLIENT_SECRET には認可サーバでアプリを登録した際に表示された Application id と Secret を
# site には認可サーバを起動している URL を渡す
client = OAuth2::Client.new(CLIENT_ID, CLIENT_SECRET, site: 'http://lvh.me:3000')

# redirect_uri には認可サーバにアプリを登録した際に入力した Redirect uri を渡す
client.auth_code.authorize_url(redirect_uri: 'http://lvh.me')

# 以下のような URL が返ってくるのでブラウザで開く
# "http://lvh.me:3000/oauth/authorize?client_id=xxx&redirect_uri=http%3A%2F%2Flvh.me&response_type=code"

# Authorize required という画面が表示されるので Authorize ボタンを押す

# redirect_uri に以下のようにリダイレクトされるので、code の値をコピーする
# http://lvh.me/?code=c5122213a32e460ef1c393e6aa69a0dac20521aac367bc43bcfb7992da4594af

# CODE には先ほどコピーした code の値を渡す
# redirect_uri は先程と一緒でとりあえず OK
token = client.auth_code.get_token(CODE, :redirect_uri => 'http://lvh.me', :headers => {'Authorization' => 'Basic some_password'})

# token が返ってきたら成功!

インプリシットグラント(Implicit Grant)

インプリシットグラントはちょっとあまりよくわかっていないです。

インプリシットグラントタイプは, アクセストークンを取得するために用いられ (リフレッシュトークンの発行はサポートされない), 特定のリダイレクトURIを利用することが既知であるパブリッククライアントに最適化されている. これらのクライアントは, 通常JavaScriptなどのスクリプト言語を使用してブラウザ上に実装される.

とある通り、リソースオーナーが操作しているブラウザと認可サーバ間でやり取りをしてアクセストークンを取得するようです。認可コードフローの場合は認可サーバとクライアント間でやり取りをして、クライアント間の認証とアクセストークンの受け渡しを行います。アクセストークンがリソースオーナーに渡ることはありません。一方でインプリシットグラントフローではリソースオーナー(が操作するブラウザとか)にアクセストークンが渡ります。クライアントが介在しないので、クライアントの認証も行われないです。

  1. クライアントがリソースオーナーの操作するブラウザなどを認可サーバの認可エンドポイントに送る 認可サーバーがアクセス許可取得後にユーザーエージェントを戻すリダイレクトURIをリクエストに含める.
  2. 認可サーバーはリソースオーナーを認証し, リソースオーナーにリソースへのアクセスの許可/拒否を尋ねる
  3. リソースオーナーがアクセスを許可した場合, 認可サーバーは 1 の段階で与えられいたリダイレクト URI にリソースオーナー(の操作するブラウザ)をリダイレクトさせてクライアントに戻す。この時リダイレクトURI にはアクセストークンが含まれる
  4. リソースオーナーの操作するブラウザがリダイレクト先のクライアントのスクリプトを実行することで、クライアントにアクセストークンを渡す

テスト

認可サーバの /oauth/authorize に対して以下のようなリクエストを投げれば良いっぽい。ブラウザで以下を開くと、acces_token を URL に含んだ状態で redirect_uri にリダイレクトされてるっぽいです。client_id と redirect_uri は認可サーバ登録時の値で置き換えてください。

http://lvh.me:3000/oauth/authorize?response_type=token&client_id=CLIENT_ID&redirect_uri=http%3A%2F%2Flvh.me

リソースオーナーパスワードクレデンシャルグラント(Resource Owner Password Credentials Grant)

リソースオーナーパスワードクレデンシャルグラントは、クライアントがリソースオーナーのクレデンシャル(通常はユーザ名とパスワード)を受け取り、それを認可サーバに渡す、というフローです。

それって最初に挙げた問題何も解決できてないじゃん!って言いたくなります。その通りです。RFC にも以下のように書いてあります。

リソースオーナーがクライアントと信頼関係にある場合, 例えばリソースオーナーの所有するデバイスOSや特別許可されたアプリケーションなどに適している. 認可サーバーはこのグラントタイプを有効にする際は特に注意するべきである. また他のフローが利用できない場合にのみ許可するべきである.

このグラントタイプは, リソースオーナーのクレデンシャル (通常は対話型入力フォームにて取得するユーザー名とパスワード) を取得できるクライアントに適している. また, 保存済みのクレデンシャルをアクセストークンへ変換できるため, Basic認証やDigest認証のような直接的な認証スキームを利用している既存のクライアントをOAuthへ移行する際にも利用できる.

というわけで、通常は使わないほうが良いわけですが、上記の通り OAuth への移行段階で使えそうです。

テスト

initializers/doorkeeper.rb に以下の設定を書きます。

initializers/doorkeeper.rb
Doorkeeper.configure do
 ...
  resource_owner_from_credentials do |routes|
    if params[:password] == "password"
      User.find(params[:username])
    end
  end
  ...
end

今度は一応パスワードで認証して、username で指定された id のユーザを返すようにしてみました。

今度もクライアントアプリは作らずに pry 上で試してみます。

require 'oauth2'

# ここまでは一緒
client = OAuth2::Client.new(CLIENT_ID, CLIENT_SECRET, site: 'http://lvh.me:3000')

# 第一引数に username 代わりの id を、第二引数に先ほどのパスワードを入力
token = client.password.get_token('2', 'password')

# アクセストークンを取得できたら成功!

クライアントクレデンシャルグラント(Client Credentials Grant)

クライアントクレデンシャルグラントは、自社サービスがクライアントの場合のように、クライアントのコントロール下にあるリソースを取得したり、認可サーバと調整済みの場合などに、クライアントクレデンシャルのみでアクセストークンを取得する方式です。

テスト

このフローは認可サーバの適切なエンドポイントに POST リクエストを投げるだけです。rest-client gemを使って pry 上でテストしてみます。それぞれインストールしておいて下さい。

require 'rest-client'

# post 先は起動している認可サーバの /oauth/token の URL
RestClient.post 'http://lvh.me:3000/oauth/token', {
  grant_type: 'client_credentials',
  client_id: CLIENT_ID,
  client_secret: CLIENT_SECRET
}

# 以下のようなレスポンスでアクセストークンが返ってきたら成功!
"{\"access_token\":\"12f307a4f13bbc6442b20448e3c4c648af3cb3d5975b9cd06bd6fc237d5cdadc\",\"token_type\":\"bearer\",\"expires_in\":7200}"

doorkeeper 補足

initializers/doorkeeper.rb の以下のところで許可するフローを指定できます。

initializers/doorkeeper.rb
  # Specify what grant flows are enabled in array of Strings. The valid
  # strings and the flows they enable are:
  #
  # "authorization_code" => Authorization Code Grant Flow
  # "implicit"           => Implicit Grant Flow
  # "password"           => Resource Owner Password Credentials Grant Flow
  # "client_credentials" => Client Credentials Grant Flow
  #
  # If not specified, Doorkeeper enables all the four grant flows.
  #
  # grant_flows %w(authorization_code implicit password client_credentials)
tyamagu2
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした