あかんわ

ブログで日誌でも書くようにすれば多少はやる気が出るかと思ったんです

GrapeとDoorkeeperでRailsアプリに認証付きのREST-like Web APIを実装する 後編

Railsチュートリアルの第2版を参考にして作ったマイクロブログに、GrapeDoorkeeperを使用して、OAuth認可を利用するWeb APIを実装しようと試みました。

後編では、前編で実装したAPIに、Doorkeeperを使ったOAuth認可の機能を実装します。

実際の所、ユーザ認証とかOAuth認可とかを理解しているとは言い難いので、セキュリティを保つための必要な措置が万全とは言えませんが、とりあえず、それっぽく動くものはできました。

目次

補足:実装したいAPI v1の仕様

  • マイクロブログにチャットボットからマイクロポストをPOSTしたい
  • APIを利用するアプリの作成や認可はサインインしているユーザのみ可能にしたい
  • つまるところTwitter APIの機能を縮小したようなAPIを作りたい

APIの実装を試みるマイクロブログは、Railsチュートリアルの第2版を参考にして作ってますので、UserモデルとMicropostモデルが関連して成り立っています。

f:id:b0npu:20160709222053p:plain

認証システムも、認証システムを提供するgemを使用せずにRailssessionメソッドを使用して構築していますので、sessionメソッドを使用する認証メソッドをSessionsHelperに定義して、色々な場所で認証メソッドを使えるようになっています。

Doorkeeperの準備

Doorkeeperのgemは、前編Gemfileに追加してbundle installまでしております。

gem 'grape', '0.16.2'
gem 'doorkeeper', '4.0.0'
doorkeeper:installでインストール

doorkeeper:installconfig/initializers/doorkeeper.rbが作られ、ルーティングにuse_doorkeeperが追加されます。

$rails generate doorkeeper:install
Running via Spring preloader in process 14348
      create  config/initializers/doorkeeper.rb
      create  config/locales/doorkeeper.en.yml
       route  use_doorkeeper
===============================================================================

There is a setup that you need to do before you can use doorkeeper.

Step 1.
Go to config/initializers/doorkeeper.rb and configure
resource_owner_authenticator block.

Step 2.
Choose the ORM:

If you want to use ActiveRecord run:

  rails generate doorkeeper:migration

And run

  rake db:migrate

Step 3.
That's it, that's all. Enjoy!

===============================================================================
doorkeeper:migrationからのdb:migrate

DoorkeeperはOAuth認可でデータベースを使うため、rails generate doorkeeper:migrationマイグレーションファイルを生成します。

$rails generate doorkeeper:migration         
      create  db/migrate/20160*********_create_doorkeeper_tables.rb

rake db:migrateマイグレーションファイルの内容をデータベースに反映させ、OAuth認可で使用するテーブルをデータベースに作成します。

$rake db:migrate
== 20160********* CreateDoorkeeperTables: migrating ===========================
・
・
・
== 20160********* CreateDoorkeeperTables: migrated (0.5095s) ==================

どんなテーブルが作られたのか、ちょいとデータベースを確認してみます。

$psql railsapp_development            
psql (9.5.1)
Type "help" for help.

railsapp_development=# \d
                    List of relations
 Schema |            Name            |   Type   | Owner  
--------+----------------------------+----------+----------
 public | users                        | table    | railsapp
・
・
・
 public | oauth_access_grants        | table    | railsapp
 public | oauth_access_grants_id_seq | sequence | railsapp
 public | oauth_access_tokens        | table    | railsapp
 public | oauth_access_tokens_id_seq | sequence | railsapp
 public | oauth_applications         | table    | railsapp
 public | oauth_applications_id_seq  | sequence | railsapp
 public | schema_migrations          | table    | railsapp
(11 rows)

railsapp_development=# 

OAuth認可で使いそうなテーブルが、3つ作成されてました。

DoorkeeperとGrapeの設定

Doorkeeper::Grape::Helpersで、Grapeを使って実装したAPIのメソッドへアクセスする際に、Doorkeeperの認可が必要となるように設定します。

config/initializers/doorkeeper.rbに初期設定を記述する

config/initializers/doorkeeper.rbで、Doorkeeperが認可できる権限のリソースオーナー*1を指定します。
リソースオーナーは、マイクロブログのユーザに限定したいと考えていますので、認証メソッドのcurrent_user*2でサインインの状態を確認して、サインインしていなければサインインページへリダイレクトします。
Scopesとやらで認可できる権限の制限もできるようなので、念のため、コメントアウトを外して設定しておきます。

  resource_owner_authenticator do
    # fail "Please configure doorkeeper resource_owner_authenticator block located in #{__FILE__}"
    # Put your resource owner authentication logic here.
    # Example implementation:
    #   User.find_by_id(session[:user_id]) || redirect_to(new_user_session_url)
    current_user || redirect_to(signin_path)
  end
  ・
  ・
  ・
  default_scopes  :public
  optional_scopes :write, :update
APIのモジュールにDoorkeeperのヘルパーを追加する

app/api/v1/v1_api.rbDoorkeeper::Grape::Helpersを追加して、API v1のメソッドにアクセスする際にはdoorkeeper_authorize!で確認するようにします。
マイクロポストのPOSTに必要なuser_idには、doorkeeper_token[:resource_owner_id]からリソースオーナーのidを取得して受け渡しますので、paramsブロックからuser_idの記述を削除しておきます。

require 'doorkeeper/grape/helpers'

module V1
  class V1API < Grape::API
    helpers Doorkeeper::Grape::Helpers

    before do
      doorkeeper_authorize!
    end

    resource :statuses do
   ・
   ・
   ・
    resource :statuses do
      ・
      ・
      ・
      desc 'Post new micropost'
      params do
        requires :content, type: String
      end
      post do
        Micropost.create!({
          user_id: doorkeeper_token[:resource_owner_id],
          content: params[:content]
        })
        status 201
      end

    end
  end
end
Doorkeeperのルーティングを設定する

doorkeeper:installconfig/routes.rbに追加されるuse_doorkeeperの記述があれば、Doorkeeperに認可させるアプリケーションの作成やアクセストークンの発行はできるようですが、アプリケーションの作成や認可の際には、SessionsHelperに定義している認証メソッドを使用し、ユーザのサインインの状態を確認したいと考えているので、use_doorkeeperをブロックにして、DoorkeeperのAuthorizationsControllerApplicationsControllerをそれぞれ継承した、カスタムコントローラを使用できるようにします。

Rails.application.routes.draw do
  use_doorkeeper do
    controllers authorizations: 'custom_authorizations'
    controllers applications: 'custom_applications'
  end
  mount API::Base => '/'
・
・
・
controller/custom_authorization_controller.rbを作成する

認証メソッドのcurrent_userを、config/initializers/doorkeeper.rbで使えるようにするため、AuthorizationsControllerを継承したCustomAuthorizationsControllerを作成し、認証メソッドを定義しているSessionsHelperをインクルードします。

class CustomAuthorizationsController < Doorkeeper::AuthorizationsController
  include SessionsHelper
end
controller/custom_applicatinos_controller.rbを作成する

Doorkeeperに認可させるアプリケーションのURIである/oauth/applicationsへのアクセスも、認証メソッドのsined_in_userを使用してユーザのサインインが必要となるようにしたいので、DoorkeeperのApplicationsControllerを継承したCustomApplicationsControllerを作成し、認証メソッドを定義しているSessionHelperをインクルードします。

class CustomApplicationsController < Doorkeeper::ApplicationsController
  include SessionsHelper
  before_action :signed_in_user
end

Doorkeeperからアクセストークンを取得

DoorkeeperのOAuth認可の動作確認をするために、rails serverを起動し、アクセストークンの取得を試みます。

$rails server
=> Booting WEBrick
=> Rails 4.2.6 application starting in development on http://localhost:3000
=> Run `rails server -h` for more startup options
=> Ctrl-C to shutdown server
[2016-06-18 21:57:52] INFO  WEBrick 1.3.1
[2016-06-18 21:57:52] INFO  ruby 2.3.0 (2015-12-25) [x86_64-darwin15]
[2016-06-18 21:57:52] INFO  WEBrick::HTTPServer#start: pid=62210 port=3000
Doorkeeperに認可させるアプリケーションを作成する

マイクロブログにサインインしてhttp://localhost:3000/oauth/applicationsにアクセスし、New Applicationから作成します。
Authorization codeの送付先になるRedirect URIを用意していなかったので、Doorkeeperに用意されているテスト用のビューのURIを使用します。

項目 登録内容
Name b0npubot 適当にアプリに名前を付ける
Redirect URI urn:ietf:wg:oauth:2.0:oob Authorization codeの送付先になるURIなので
テスト用に用意されているビューのURIを使った
Scopes 必須項目ではなかったので空白にしました

必要な項目を登録するとアプリケーションのページが作成され、Application IdSecretが記載されています。

作成したアプリケーションをAuthorizeする

アプリケーションのページにあるAuthorize*3からRedirect URIAuthorization code*4が送付され、Redirect URIに記述したURIに移動します。
Redirect URIに、Doorkeeperに用意されているテスト用のビューのURIを使用している場合は、Authorization codeが記載されたページが表示されます。

cURLコマンドでアクセストークンを取得する

Access Token*5を取得するためには、Application IdSecretAuthorization codehttp://localhost:3000/oauth/tokenに送信するのですが、Access Tokenを取得するためのフォームやビューを用意してませんので、curlコマンドを使用して取得します。

curl -F grant_type=authorization_code \
-F client_id=9b36d8c0db59eff5038aea7a417d73e69aea75b41aac771816d2ef1b3109cc2f \
-F client_secret=d6ea27703957b69939b8104ed4524595e210cd2e79af587744a7eb6e58f5b3d2 \
-F code=fd0847dbb559752d932dd3c1ac34ff98d27b11fe2fea5a864f44740cd7919ad0 \
-F redirect_uri=urn:ietf:wg:oauth:2.0:oob \
-X POST http://localhost:3000/oauth/token
{"access_token":"1d69ea5e8011579d35d16aacec463862855701ed4805f31014b72c2862c18e9d","token_type":"bearer","expires_in":7200,"scope":"public","created_at":1437932641}%     
  • curl … 指定したURLへデータの送受信を行うコマンド
  • -F "name=content" … フォームからのPOSTリクエストのふるまいを模倣するオプション
  • grant_type … `Authorization code`を送るので"authorization_code"を入力する
  • client_it … `Application Id`を入力する
  • client_secret … `Secret`を入力する
  • code … `Authorization code`を入力する
  • redirect_uri … アプリケーションの作成時に登録した`Redirect URI`を入力する
  • -X POST … 指定したURLへの通信に用いるリクエストメソッドにPOSTを指定するオプション
  • \ … コマンドやオプションの途中で使うと改行できる

認証付きAPI v1の動作確認

取得したAccess Tokencurlコマンドで、認証付きAPI v1の動作を確認します。

アクセストークン無しではアクセス出来ない事を確認する

前編で確認した動作を試してみると、"The access token is invalid"が表示されます。

$curl http://localhost:3000/api/v1/statuses/     
{"error":"The access token is invalid"}%                                                                                       
$curl http://localhost:3000/api/v1/statuses/index
{"error":"The access token is invalid"}%                                                                                       
$curl -d "user_id=2&content=api test" http://localhost:3000/api/v1/statuses
{"error":"The access token is invalid"}%
アクセストークンを送ってGet the root url

curlコマンドの-Hオプションで、AuthorizationリクエストヘッダにBearerなトークンとしてAccess Tokenを指定し、root URLにアクセスしてみます。

$curl -H "Authorization: Bearer 1d69ea5e8011579d35d16aacec463862855701ed4805f31014b72c2862c18e9d" http://localhost:3000/api/v1/statuses
200%       

無事に、ステータスコード200が表示されました。
rails serverのログでは、Access Tokenが検証されている様子が確認できます。

Started GET "/api/v1/statuses" for ::1 at 2016-06-18 22:13:24 +0900
  Doorkeeper::AccessToken Load (0.4ms)  SELECT  "oauth_access_tokens".* FROM "oauth_access_tokens" WHERE "oauth_access_tokens"."token" = $1 LIMIT 1  [["token", "1d69ea5e8011579d35d16aacec463862855701ed4805f31014b72c2862c18e9d"]]
  Doorkeeper::AccessToken Load (0.3ms)  SELECT  "oauth_access_tokens".* FROM "oauth_access_tokens" WHERE "oauth_access_tokens"."refresh_token" = $1 LIMIT 1  [["refresh_token", ""]]
   (0.2ms)  BEGIN
   (0.2ms)  COMMIT
アクセストークンを送ってPost new micropost

同様にして、Access Tokenを用いたマイクロポストのPOST機能を試してみます。 前編で確認した際にはuser_idも送信していましたが、Access Tokenからリソースオーナーのidを取得できるため、contenttest api postのみPOSTします。

$curl -H "Authorization: Bearer 1d69ea5e8011579d35d16aacec463862855701ed4805f31014b72c2862c18e9d" -d "content=test api post" http://localhost:3000/api/v1/statuses
201%    
  • curl … 指定したURLへデータの送受信を行うコマンド
  • -H "header" … 追加のヘッダを送信できるオプション
  • -d "name=value" … データをPOSTリクエストとして送信できるオプションで'&'を使って複数項目をまとめて送れる

無事に、ステータスコード201が表示されました。
rails serverのログでも、Access Tokenの検証とSQLのINSERTが確認できます。

Started POST "/api/v1/statuses" for ::1 at 2016-06-18 22:15:55 +0900
  Doorkeeper::AccessToken Load (0.6ms)  SELECT  "oauth_access_tokens".* FROM "oauth_access_tokens" WHERE "oauth_access_tokens"."token" = $1 LIMIT 1  [["token", "1d69ea5e8011579d35d16aacec463862855701ed4805f31014b72c2862c18e9d"]]
  Doorkeeper::AccessToken Load (0.6ms)  SELECT  "oauth_access_tokens".* FROM "oauth_access_tokens" WHERE "oauth_access_tokens"."refresh_token" = $1 LIMIT 1  [["refresh_token", ""]]
   (0.5ms)  BEGIN
   (0.3ms)  COMMIT
   (0.3ms)  BEGIN
  SQL (47.3ms)  INSERT INTO "microposts" ("user_id", "content", "created_at", "updated_at") VALUES ($1, $2, $3, $4) RETURNING "id"  [["user_id", 1], ["content", "test api post"], ["created_at", "2016-06-18 13:15:55.972622"], ["updated_at", "2016-06-18 13:15:55.972622"]]
   (647.8ms)  COMMIT

念のため、データベースも確認してみます。

railsapp_development=# SELECT * FROM microposts ORDER BY id DESC LIMIT 1;
 id  |    content    | user_id |         created_at         |         updated_at         
-----+---------------+---------+----------------------------+----------------------------
 334 | test api post |       1 | 2016-06-18 13:15:55.972622 | 2016-06-18 13:15:55.972622
(1 row)

よっしゃー保存されてたー∠( ゚д゚)/

蛇足:Strong Parametersを使ってMass Assignment脆弱性対策を試みる

API v1POSTメソッドで、Micropostモデルにparamsから直接パラメータを渡しているのを見ていて、なにやら不安になってきたので、もうちょっと良い書き方が無いものかと調べた所、Grapeからでも、RailsStrong Parametersを使えるらしいと知ったので、ヘルパーメソッドとしてhelpersのブロックに定義して使ってみました。
ついでに、リソースオーナーのidからUserオブジェクトを取得して、UserMicropostの関連付けを使用してマイクロポストを作成するメソッドを使うようにしました。

module V1
  class V1API < Grape::API

    helpers do
      include Doorkeeper::Grape::Helpers

      def owner_user
        User.find_by_id(doorkeeper_token[:resource_owner_id])
      end

      def content_params
        ActionController::Parameters.new(params).permit(:content)
      end
    end

    before do
      doorkeeper_authorize!
    end

    resource :statuses do
      ・
   ・
   ・
      post do
        owner_user.microposts.create!(content_params)
        status 201
      end

    end

  end
end

参考記事

Grapeに関しては、こちらの記事を参考にさせていただきました。

Doorkeeperに関しては、こちらの記事を参考にさせていただきました。

cURLに関しては、こちらの記事を参考にさせていただきました。

開発環境

*1:アクセストークンを発行できるユーザ

*2:SessionsHelperに定義している認証メソッド

*3:作成したアプリケーションの初回のAuthorizeの際はAuthorizeかDenyか再確認されます

*4:Authorization codeの有効期限の初期設定値は10分だそうです

*5:Access Tokenの有効期限の初期設定値は2時間だそうです