nirasan's tech blog

趣味や仕事の覚え書きです。Linux, Perl, PHP, Ruby, Javascript, Android, Cocos2d-x, Unity などに興味があります。

Rails で API サーバーの認証の仕組みを作る

はじめに

  • Rails でモバイルアプリのバックエンドに使う API サーバーのユーザー作成と認証の仕組みを作ったメモです
  • Doorkeeper + Sorcery を使った認証の構築とテストまで記述します

導入

新しいプロジェクトを作成
$ rails new -T -B doorkeeper-test

gem のインストール

デフォルトの Gemfile に以下を追記
gem 'doorkeeper'
gem 'sorcery'
group :development, :test do
  gem 'rspec-rails'
  gem 'factory_girl_rails'
  gem 'database_rewinder'
end
インストール
$ bundle install

Sorcery のインストール

# Sorcery の設定ファイルと Sorcery に対応した User モデルの生成
$ rails generate sorcery:install

Doorkeeper のインストール

# 設定ファイルの作成とルートの追加
$ rails generate doorkeeper:install
# マイグレーションファイルの作成
$ rails generate doorkeeper:migration

マイグレーションの実行

$ rake db:migarate

Doorkeeper の設定

Doorkeeper.configure do
  # Change the ORM that doorkeeper will use (needs plugins)
  orm :active_record

  resource_owner_authenticator do
    User.find_by_id(session[:current_user_id]) || redirect_to(login_url)
  end

  resource_owner_from_credentials do
    User.authenticate(params[:username], params[:password])
  end
end
Doorkeeper.configuration.token_grant_types << "password"

ユーザーコントローラの作成

  • ユーザーの作成とユーザー情報表示ページを作成する
  • ユーザー作成は認証なしで、ユーザー情報表示は認証必須のページにする
generate
$ rails g controller users create show
routes.rb
  resources :users, only: [:create, :show]
app/controllers/users_controller.rb
class UsersController < ApplicationController
  before_action :doorkeeper_authorize!, only: [:show] # show のみ認証が必要

  def create
    @user = User.new(user_params)
    if @user.save
      render json: @user
    else
      head :bad_request
    end
  end

  def show
    render json: User.find(params[:id])
  end

  private

  def user_params
    params.require(:user).permit(:email, :password)
  end
end
app/controllers/application_controller.rb
  • CSRF 対策を無効にする
-  protect_from_forgery with: :exception
+  protect_from_forgery with: :null_session

Doorkeeper の認証用キーの作成

動作確認

  • curl コマンドで動作確認を行う
ユーザーの作成
$ curl -F "user[email]=user1@example.com" -F "user[password]=password" http://127.0.0.1:3000/users
アクセストークンの取得
  • client_id には「Doorkeeper の認証用キーの作成」で作成した Application Id を、client_secret には同じく Secret を指定する
$ curl -F "grant_type=password" \
       -F "client_id=07461c1f68870ea090ed20af49d0680782950119213e2a1af9e1f653c688dca3" \
       -F "client_secret=20dea83f022c9e2a0e6fbec1da0e9c0e20c40e1bf09d2e8db029b461653a88bd" \
       -F "username=user1@example.com" -F "password=password" \
       http://127.0.0.1:3000/oauth/token.json
ユーザーの取得
  • Bearer 以降には「アクセストークンの取得」で取得したトークンの文字列を指定する
$ curl -H "Authorization: Bearer 18afa04c80fbe3528b5d495c24e8badcbaeee12b2866d22d8f220c44543ae01c" http://127.0.0.1:3000/users/1

テスト

rspec の準備
$ rails g rspec:install
rails_helper.rb に追記
  config.before :suite do
    DatabaseRewinder.clean_all
  end

  config.after :each do
    DatabaseRewinder.clean
  end

  config.before :all do
    FactoryGirl.reload
  end
specs_helper.rb に追記
  require 'factory_girl_rails'
  config.include FactoryGirl::Syntax::Methods
factories/users.rb の作成
FactoryGirl.define do
  factory :user do
    sequence(:email) {|n| "user#{n}@example.com" }
    password "password"
  end
end
factories/doorkeeper.rb の作成
FactoryGirl.define do
  factory :access_token, class: Doorkeeper::AccessToken do
    sequence(:resource_owner_id) { |n| n }
    application
    expires_in 1.hours
  end

  factory :application, class: Doorkeeper::Application do
    sequence(:name){ |n| "Application #{n}" }
    redirect_uri 'https://example.com/callback'
  end
end
spec/controllers/user_controller_spec.rb
require 'rails_helper'

RSpec.describe UsersController, type: :controller do
  describe "GET #show" do
    let!(:application) { create :application }
    let!(:user)        { create :user }
    let!(:token)       { create :access_token, :application => application, :resource_owner_id => user.id }
    it "returns http success" do
      get :show, {format: :json, access_token: token.token, id: user.id}
      expect(response).to have_http_status(:success)
    end
  end
end

さいごに

  • クライアントからは Doorkeeper のトークン情報を見れる必要はないから、中継用のアクションとかを用意したほうがいいかな?