@znz blog

ZnZ の memo のようなもの

doorkeeper gem の API のクライアント

| Comments

doorkeeper gem で API を作る方は doorkeeper-provider-app というサンプルの app/controllers/api/ 以下などをみればすぐにわかったのですが、 API を呼び出す方は OmniAuth の中でユーザーの情報を取り出す だけならすぐに出来たのですが、 コントローラーの中など呼び出す方法は doorkeeper-devise-client を見てもよくわからなかったので、まとめてみました。

動作確認バージョン

  • provider 側
    • rails 3.2.14
    • doorkeeper 0.7.3
  • client 側
    • rails 4.0.0
    • devise 3.1.1
    • omniauth 1.1.4
    • omniauth-oauth2 1.1.1
    • oauth2 0.8.1

準備

まず Create a OmniAuth strategy for your provider を参考にして、 OmniAuth の中で access_token.get('/api/v1/me.json').parsed は出来るところまでは準備しておきます。

目的としては、 この access_token が認証より後で呼ばれる他のコントローラーの中で取得できれば良いということになります。

余談ですが、 doorkeeper の wiki は Supported Ruby & Rails versions のように情報が古いまま放置されているページもあるようなので、 README などのソースコード側のドキュメントも参照した方が良さそうです。

必要なもの

access_tokenOAuth2::AccessToken クラスのオブジェクトです。

生成するには

  • OAuth2::Client のオブジェクト
  • 認証で取得した token

が必要になります。

OAuth2::Client の生成には

  • client_id
  • client_secret
  • URL

が必要になります。

token の保存

まず OAuth2 の認証で取得した token を保存しておく必要があります。

Users::OmniauthCallbacksController#doorkeepersession[:doorkeeper_token] = request.env["omniauth.auth"]["credentials"]["token"] のようにしてセッションなどの後で使える場所に保存しておきます。

後で調べてわかったのですが、 doorkeeper-devise-client の Users::OmniauthCallbacksController では request.env["omniauth.auth"].credentials.tokenuser.doorkeeper_access_token でデータベースに保存していました。

OAuth2::Client の作成

doorkeeper-devise-client の ApplicationController では必要な情報は定数経由で受け取るようになっていました。

今回は deviseomniauth-oauth2 を使っているので、 その情報を使って生成するようにしました。 要点だけまとめると以下のコードになります。

1
2
3
config = Devise.omniauth_configs[:doorkeeper]
strategy = config.strategy_class.new(*config.args)
client = strategy.client

OAuth2::AccessToken の生成

ここまで準備ができれば後は OAuth2::AccessToken.new するだけです。

まとめると以下のコードになります。

1
2
3
4
5
6
7
  def access_token
    return @access_token if defined?(@access_token)
    config = Devise.omniauth_configs[:doorkeeper]
    strategy = config.strategy_class.new(*config.args)
    token = session[:doorkeeper_token]
    @access_token = OAuth2::AccessToken.new(strategy.client, token)
  end

API 呼び出し

access_token が出来たら後は呼び出しに使うだけです。

単純な情報取得は get して JSON なら parsed を呼び出すだけです。

1
2
  access_token.get("/api/v1/me.json").parsed
  access_token.get("/api/v1/posts.json").parsed

今回は連携して書き込みたいというのが目的だったため、 post も使いました。

モデルの例としては rails g scaffold post title body:text で API の提供側では以下の実装とします。

app/controllers/api/v1/posts_controller.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
module Api::V1
  class PostsController < ApiController
    doorkeeper_for :index
    doorkeeper_for :create
    respond_to     :json

    def index
      respond_with Post.all
    end

    def create
      post = Post.new(params[:post])
      post.user = current_resource_owner
      post.save!
      respond_with post
    end

呼び出し側は以下のようになります。

1
access_token.post("/api/v1/posts", params: { post: { title: title, body: body } })

params による指定は OAuth2::AccessToken のソースをみて推測しました。

scope 付き API 提供

書き込みも許可すると scope を分けたくなります。

doorkeeper 側では Using Scopes を参考にして

  • initializers に scopes 追加
  • 翻訳追加
  • API に scopes 追加

をしておきます。

config/initializers/doorkeeper.rb
1
2
  default_scopes  :public
  optional_scopes :admin, :write

2013-12-20 追記: doorkeeper gem を 0.7.3 から 0.7.4 に上げたところ、 シンボルだとうまく動かなくなってしまったので、 文字列に変更しました。

(doorkeeer gem 0.7.3 以前)

app/controllers/api/v1/posts_controller.rb
1
2
doorkeeper_for :index,  :show,   scopes: [:public]
doorkeeper_for :create, :update, scopes: [:admin, :write]

(doorkeeer gem 0.7.4 以降)

app/controllers/api/v1/posts_controller.rb
1
2
doorkeeper_for :index,  :show,   scopes: %w"public"
doorkeeper_for :create, :update, scopes: %w"admin write"

参考のため、この API の rspec も載せておきます。 複数の scopes を設定する時に , 区切りだとうまくいかないところがあったので、 スペース区切りにしています。

spec/controllers/api/v1/posts_controller_spec.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
require 'spec_helper'

describe Api::V1::PostsController do
  describe "GET 'index'" do
    let!(:application) { Doorkeeper::Application.create!(name: "MyApp", redirect_uri: "http://app.com") }
    let!(:user) { FactoryGirl.create(:normal_user) }
    let!(:token) { Doorkeeper::AccessToken.create! application_id: application.id, resource_owner_id: user.id, scopes: "public" }
    subject { response }

    context "valid token" do
      before do
        get 'index', format: :json, access_token: token.token
      end
      it { should be_success }
      its(:status) { should eq(200) }
      its(:body) { should == Post.all.to_a.to_json }
    end

    context "invalid token" do
      before do
        get 'index', format: :json, access_token: token.token.succ
      end
      it { should_not be_success }
      its(:status) { should eq(401) }
    end
  end

  describe "GET 'index' without scopes" do
    let!(:application) { Doorkeeper::Application.create!(name: "MyApp", redirect_uri: "http://app.com") }
    let!(:user) { FactoryGirl.create(:normal_user) }
    let!(:token) { Doorkeeper::AccessToken.create! application_id: application.id, resource_owner_id: user.id, scopes: "write" }
    subject { response }

    context "valid token" do
      before do
        get 'index', format: :json, access_token: token.token
      end
      it { should_not be_success }
      its(:status) { should eq(401) }
      its(:body) { should == " " }
    end
  end

  describe "POST 'create'" do
    let!(:application) { Doorkeeper::Application.create!(name: "MyApp", redirect_uri: "http://app.com") }
    let!(:user) { FactoryGirl.create(:normal_user) }
    let!(:token) { Doorkeeper::AccessToken.create! application_id: application.id, resource_owner_id: user.id, scopes: "public write" }
    subject { response }

    context "valid token" do
      before do
        post 'create', format: :json, access_token: token.token, post: { title: "title", body: "some content" }
      end
      it { should be_success }
      its(:status) { should eq(201) } # 201 Created
      its(:body) { should == Post.last.to_json }
    end

    context "invalid token" do
      before do
        post 'create', format: :json, access_token: token.token.succ
      end
      it { should_not be_success }
      its(:status) { should eq(401) }
    end
  end
end

scope 付き呼び出し

devise の設定で omniauth の設定に scope を追加するだけです。

config/initializers/devise.rb
1
  config.omniauth :doorkeeper, ENV['DOORKEEPER_APP_ID'], ENV['DOORKEEPER_APP_SECRET'], { scope: 'public write' }

rspec のところでもちょっと書きましたが、 区切りが , だとうまくいかないことがあったので、 スペース区切りにしています。

原因は lib/doorkeeper/oauth/scopes.rbstring.split のように無引数の String#split を使っているからではないかと推測していますが、確認はしていません。

まとめ

client_idclient_secret と provider の URL はあらかじめ用意しておいて、 client 側の rails アプリに設定しておきます。

token は OAuth2 で取得したものを session やデータベースなどに保存しておいて使います。

必要なら scopes も設定できます。

Comments