GrapeとDoorkeeperでRailsアプリに認証付きのREST-like Web APIを実装する 後編
Railsチュートリアルの第2版を参考にして作ったマイクロブログに、GrapeとDoorkeeperを使用して、OAuth認可を利用するWeb APIを実装しようと試みました。
後編では、前編で実装したAPIに、Doorkeeperを使ったOAuth認可の機能を実装します。
実際の所、ユーザ認証とかOAuth認可とかを理解しているとは言い難いので、セキュリティを保つための必要な措置が万全とは言えませんが、とりあえず、それっぽく動くものはできました。
目次
- 目次
- 補足:実装したいAPI v1の仕様
- Doorkeeperの準備
- DoorkeeperとGrapeの設定
- Doorkeeperからアクセストークンを取得
- 認証付きAPI v1の動作確認
- 蛇足:Strong Parametersを使ってMass Assignment脆弱性対策を試みる
- 参考記事
- 開発環境
補足:実装したいAPI v1の仕様
- マイクロブログにチャットボットからマイクロポストをPOSTしたい
- APIを利用するアプリの作成や認可はサインインしているユーザのみ可能にしたい
- つまるところTwitter APIの機能を縮小したようなAPIを作りたい
APIの実装を試みるマイクロブログは、Railsチュートリアルの第2版を参考にして作ってますので、User
モデルとMicropost
モデルが関連して成り立っています。
認証システムも、認証システムを提供するgem
を使用せずにRailsのsession
メソッドを使用して構築していますので、session
メソッドを使用する認証メソッドをSessionsHelperに定義して、色々な場所で認証メソッドを使えるようになっています。
Doorkeeperの準備
Doorkeeperのgemは、前編でGemfile
に追加してbundle install
までしております。
gem 'grape', '0.16.2' gem 'doorkeeper', '4.0.0'
doorkeeper:installでインストール
doorkeeper:install
でconfig/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.rb
にDoorkeeper::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:install
でconfig/routes.rb
に追加されるuse_doorkeeper
の記述があれば、Doorkeeperに認可させるアプリケーションの作成やアクセストークンの発行はできるようですが、アプリケーションの作成や認可の際には、SessionsHelperに定義している認証メソッドを使用し、ユーザのサインインの状態を確認したいと考えているので、use_doorkeeper
をブロックにして、DoorkeeperのAuthorizationsController
とApplicationsController
をそれぞれ継承した、カスタムコントローラを使用できるようにします。
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 Id
とSecret
が記載されています。
作成したアプリケーションをAuthorizeする
アプリケーションのページにあるAuthorize
*3からRedirect URI
にAuthorization code
*4が送付され、Redirect URI
に記述したURIに移動します。
Redirect URI
に、Doorkeeperに用意されているテスト用のビューのURIを使用している場合は、Authorization code
が記載されたページが表示されます。
cURLコマンドでアクセストークンを取得する
Access Token
*5を取得するためには、Application Id
とSecret
とAuthorization code
をhttp://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 Token
とcurl
コマンドで、認証付き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
を取得できるため、content
のtest 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 v1
のPOST
メソッドで、Micropost
モデルにparams
から直接パラメータを渡しているのを見ていて、なにやら不安になってきたので、もうちょっと良い書き方が無いものかと調べた所、Grape
からでも、RailsのStrong Parameters
を使えるらしいと知ったので、ヘルパーメソッドとしてhelpers
のブロックに定義して使ってみました。
ついでに、リソースオーナーのid
からUser
オブジェクトを取得して、User
とMicropost
の関連付けを使用してマイクロポストを作成するメソッドを使うようにしました。
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に関しては、こちらの記事を参考にさせていただきました。
- GitHub - ruby-grape/grape: An opinionated framework for creating REST-like APIs in Ruby.
- Rails x Grapeで簡単API開発 - プログラミングノート
- RailsとGrapeをはじめてつかってみた | Webuilder240
- Grape での mass assignment 対策 - Qiita
Doorkeeperに関しては、こちらの記事を参考にさせていただきました。
- GitHub - doorkeeper-gem/doorkeeper: Doorkeeper is an OAuth 2 provider for Rails
- Rails 4.1 で Doorkeeper を使った OAuth2 Provider のサンプルを実装した - @znz blog
- Rails で API サーバーの認証の仕組みを作る - nirasan's tech blog
- よくわかる認証と認可 | Developers.IO
cURLに関しては、こちらの記事を参考にさせていただきました。
開発環境
- OSX 10.11.5 El Capitan
- テキストエディタ: MacVim
- ターミナルエミュレータ: Macターミナル
- シェル: zsh
- パッケージマネージャ: Homebrew
- ブラウザ: Firefox - Ruby 2.3.0
- バージョンマネージャ: rbenv
- Webフレームワーク: Ruby on Rails 4.2.6 - データベース
- ORDBMS: PostgreSQL 9.5.1