Rails + devise + omniauth + doorkeeperでoauth認証を行う

この記事はLIFULL Advent Calendar その3の24日目の記事です。

# 概要
・認証周りについて動く物を作りながら理解を深めたいなと、RailsでOAuthによるログイン機能のサンプル作りました
・作るに辺り先人の記事が大いに参考になったのですが所々々ハマった箇所もあり、より詳細に手順を残しておければとまとめました

# ゴール
1　認証を行うproviderをlocalhost:3000に構築する
2　providerの認証を得ることでログインできるclientサーバをlocalhost:3001に構築する
3　1で認証を行ったのち、2のコンテンツにアクセスする

# 実行環境
MacOS High Sierra 10.13
Rails version: 5.2.2
Ruby version: 2.4.0 (x86_64-darwin17)

# 構築手順
## Ruby on Railsのインストール
こちらの記事を参考にさせていただきました。
https://qiita.com/yoshiokatsuneo@github/items/d75d4fef9e75afb65126#mac%E3%81%A7%E3%81%AErails%E3%82%A4%E3%83%B3%E3%82%B9%E3%83%88%E3%83%BC%E3%83%AB

尚、Rails起動時に下記のようにRailsがinstallされてないよーと言われた際は

```
% rails
Rails is not currently installed on this system. To get the latest version, simply type:
```

rbenvのPathが通っていないので、下記の記事に習ってPathを通すといけます。

・zshの方は
https://qiita.com/seijikohara/items/79b479c9dd2e3b950301
・bashの方は
https://qiita.com/w7tree/items/0860e2856f2429b20eee

## Providerサーバ構築
これ以降の作業は下記の記事が大変に参考になりました。
provider側は基本的に同じ内容になりますが、client側の細かい部分で追加作業が必要になります。
https://qiita.com/8398a7/items/9b13ac2e7c401dee4a39

尚、以後はホームディレクトリ内にworkspaceというディレクトリを切って作業していきます。
この場所はお好みでOKです。

```
% mkdir ~/workspace && cd ~/workspace
```
provider用に、workspace内でRailsプロジェクトを新規作成します。
(後ほど同じ場所にclientsプロジェクトも作っていくことになります)

```
% rails new provider
```

providerディレクトリが出来ていれば成功です

```
% ls
provider
```

移動しておく

```
% cd proveder
```

私の端末にRailsをインストールしたのは初めてだったので、一度起動してみました

```
% rail s
```
http://localhost:3000
にアクセス

![スクリーンショット 2018-12-24 9.24.17.png](https://qiita-image-store.s3.amazonaws.com/0/280433/a7bb8e2b-0602-b038-2423-866cd51b2d37.png)

Yay出来ていたら成功です。うれしい

正常に起動が出来ていることが確認できたら、「Ctrl-C」で、サーバを一度停止します

必要なGemをインストールしていきます
Gemfileの末尾に下記を追記します

```Ruby:~/workspace/provider/Gemfile
gem 'devise'
gem 'doorkeeper'
```

deviseのインストールとUserテーブルの作成を行います

```
% rails g devise:install
% rails g devise User
% rake db:migrate
```

先の記事に習いUserテーブルにカラムを追加

```
% rails g migration AddOmniauthToUser uid name admin:boolean
% rake db:migrate
```
modelの作成を行います。
modeldevise用のオプションを指定することができます。
オプションの種類については下記にまとめてくださっています。
https://qiita.com/cigalecigales/items/73d7bd7ec59a001ccd74#2-devise%E3%83%A2%E3%82%B8%E3%83%A5%E3%83%BC%E3%83%AB%E6%A6%82%E8%A6%81

下記のように修正

```Ruby:~/workspace/provider/app/models/user.rb
class User < ActiveRecord
  # Include default devise modules. Others available are:
  # :confirmable, :lockable, :timeoutable and :omniauthable
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :trackable, :validatable
  after_create :update_uid!

  def update_uid!
    update(uid: id)
  end
end
```

次はdoorkeeperのインストールとDB構築を行います。

```
% rails g doorkeeper:install
% rails g doorkeeper:migration
% rake db:migrate
```

doorkeeperの設定を行います。
Doorkeeper.configureブロック内、resource_owner_authenticatorとadmin_authenticatorを追記します

```Ruby:~/workspace/provider/config/initializers/doorkeeper.rb
Doorkeeper.configure do
  # Change the ORM that doorkeeper will use (needs plugins)
  orm :active_record
  
  resource_owner_authenticator do
    session[:return_to] = request.fullpath
    current_user || redirect_to(new_user_session_url)
  end 

  admin_authenticator do
    current_user.try(:admin) || redirect_to(new_user_session_url)
  end
end
```
ちなみにこのままだとrootへのURLが引けなくてdoorkeeperの管理画面でエラーを吐くので、routes.rbにrootの設定をしてあげます(ここでは仮のcontrollerにしておきます)

```~/workspace/provider/config/routes.rb
 Rails.application.routes.draw do
   use_doorkeeper
   devise_for :users
   # For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.html
   root to: 'home#show'
 end
```
認証用のUserを作成します
railsのコンソールから直接データを投入することが出来ます
ここで投入するemail、passwordを後ほど認証に利用します

```
% rails c 
> User.create(email: 'hoge@bar.com', password: 'hogehoge', name: 'fuga', admin: true)
```

これでdoorkeeperで必要な設定が出来ました。
「Ctrl-D」でコンソールを抜けた後に、Railsサーバを起動します

```
% rails s
```

起動出来たら下記にアクセスします。

http://localhost:3000/oauth/applications

まずdeviseにより、下記のような認証を求める画面に遷移します。
先ほどUserテーブルに投入したemail、paswordを入力してログインできます

![スクリーンショット 2018-12-24 9.41.06.png](https://qiita-image-store.s3.amazonaws.com/0/280433/01662782-9d21-e7f5-d5de-65b014308533.png)

http://localhost:3000
に遷移してエラーが出るかと思いますが、認証には成功しています
(本筋には影響ないのでスルー)

改めて、下記にアクセス、doorkeeperの認証キーを作成するページに移動します。
http://localhost:3000/oauth/applications
![スクリーンショット 2018-12-24 10.21.08.png](https://qiita-image-store.s3.amazonaws.com/0/280433/9075afa5-7af0-b72a-a623-1d6102eed20b.png)

「New Application」から認証キーを新規発行します。
Name: Mock
Redirect URL: ~~http://localhost:3001/users/auth/doorkeeper/callback~~
のように入力します
→(訂正)　URL: http://localhost:3001/users/auth/mock/callback
![スクリーンショット 2018-12-24 12.58.43.png](https://qiita-image-store.s3.amazonaws.com/0/280433/34a80d12-be8f-e29a-de6a-a86c4f4d11fe.png)

設定したらSubmitすると、Application UID、とSecretが発行されます。
この認証キーが漏れると第三者からのアクセスが可能となってしまうので、秘匿情報として扱うのが良いかと思います(localのみで実行する分には大丈夫ですが、念のため)。


最後にユーザ情報を取得するAPIを追加します

```
% rails g controller api/v1/api
```

```Ruby:~/workspace/provider/app/controllers/api/v1/api_controller.rb
class Api::V1::ApiController < ApplicationController
  private
  def current_resource_owner
    User.find(doorkeeper_token.resource_owner_id) if doorkeeper_token
  end 
end
```

```
% rails g controller api/v1/users
```

```Ruby:~/workspace/provider/app/controllers/api/v1/users_controller.rb
class Api::V1::UsersController < Api::V1::ApiController
  before_action :doorkeeper_authorize!
  respond_to :json

  def me
    respond_with current_resource_owner
  end 
end
```

ログイン後のリダイレクト処理を記述

```
% rails g controller users/sessions
```

```Ruby:~/workspace/provider/app/controllers/users/sessions_controller.rb
class Users::SessionsController < Devise::SessionsController
  def create
    self.resource = warden.authenticate!(auth_options)
    set_flash_message(:notice, :signed_in) if is_flashing_format?
    sign_in(resource_name, resource)
    yield resource if block_given?
    if session[:return_to]
      redirect_to session[:return_to]
      session[:return_to] = nil 
    else
      respond_with resource, location: after_sign_in_path_for(resource)
    end 
  end 
end
```
最後にroutesを設定して完了です

```Ruby:~/workspace/provider/config/routes.rb
Rails.application.routes.draw do
  use_doorkeeper
  devise_for :users, controllers: {
    sessions: 'users/sessions'
  }
  # For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.html
  namespace :api do
    namespace :v1 do
      get '/me' => 'users#me'
    end
  end
  root to: 'home#show'
end
```

## Client側サーバ構築
次にCLientsサーバを構築していきます

Railsプロジェクトを新規作成

```
%cd ~/.workspace/ && rails new client && cd client
```

deviseとomuniauthのインストール

Gemfileに必要なモジュールを記述
Railsプロジェクトを新規作成

```Ruby:~/workspace/client/Gemfile
gem 'devise'
gem 'omniauth'
gem 'omniauth-oauth2'
```

deviseインストールとUserテーブルの作成

```
% rails g devise:install
% rails g devise User
% rake db:migrate
```
omuniauthでの認証結果を格納するカラムを追加

```
% rails g migration AddOmniauthToUser uid provider name token raw:text
% rake db:migrate
```
starategiesの作成

```Ruby:workspace/client/lib/omniauth/strategies/mock.rb
module OmniAuth
  module Strategies
    class Mock < OmniAuth::Strategies::OAuth2
      RAW_INFO_URL = 'api/v1/me'

      uid { raw_info['uid'] }

      info do
        {
          email: raw_info['email'],
          name: raw_info['name']
        }
      end

      extra do
        { raw_info: raw_info }
      end

      def raw_info
        @raw_info ||= JSON.parse(access_token.get(RAW_INFO_URL).response.body)
      end
    end
  end
end
```
deviseの設定
provider側で先ほど発行したAPI_IDとAPP_SECRETを設定します

```Ruby:config/initializers/devise.rb
require File.expand_path('../../../lib/omniauth/strategies/mock', __FILE__)
Devise.setup do |config|
  config.omniauth :mock, 'APP_ID', 'APP_SECRET', client_options: { site: 'http://localhost:3000' }
end
```

providerで認証の後にコールバックされるURLの受け先を作成します

```
% rails g controller users/omniauth_callbacks
```
コントローラに下記を追記

```Ruby:~/workspace/client/app/controllers/users/omniauth_callbacks_controller.rb
class Users::OmniauthCallbacksController < Devise::OmniauthCallbacksController
  def mock
    @user = User.find_for_mock(request.env['omniauth.auth'])

    if @user.persisted?
      flash[:notice] = I18n.t 'devise.omniauth_callbacks.success', kind: 'mock'
      sign_in_and_redirect @user, event: :authentication
    else
      session['devise.mock_data'] = request.env['omniauth.auth']
      redirect_to new_user_session_url, alert: @user.errors
    end 
  end 
end
```

下記Userモデルの作成
ここでdeviseのオプションに「:omuniauthable」を設定しておく必要があります

```Ruby:~/.workspace/client/app/models/user.rb
class User < ApplicationRecord
 devise :database_authenticatable, :registerable,
        :recoverable, :rememberable, :validatable, :omniauthable

 def self.find_for_mock(auth)
    parameters = { 
      name:     auth.info.name,
      email:    auth.info.email,
      provider: auth.provider,
      uid:      auth.uid,
      token:    auth.credentials.token,
      password: Devise.friendly_token[0, 20],
      raw: auth.extra.to_json
    }   
    user = User.find_by(uid: auth.uid)
    return update_mock!(user, parameters) if user

    User.create(parameters)
  end 

  def self.update_mock!(user, parameters)
    user.update(parameters)
    user
  end 
end
```

routingの設定をすれば完了です

```Ruby:~/workspace/client/config/route.rb
Rails.application.routes.draw do
  devise_for :users, :controllers => {
    :omniauth_callbacks => "users/omniauth_callbacks"
  }
end
```

3001番ポートでClient側サーバを起動します

```
% rails s -p 3001
```

http://localhost:3001/users/sign_in
にアクセスするとリンクに「Sign in with Mock」というリンクが表示されています
このリンクをクリックすると、Provider側の認証ページに遷移します
![スクリーンショット 2018-12-24 14.07.36.png](https://qiita-image-store.s3.amazonaws.com/0/280433/dffc4212-c86e-1eae-d6a8-e2f8f553f344.png)

認証先のページでAuthorizeをクリックすると、Providerサーバでの認証後にClientサーバへのコールバックが実行され、
localhost:3001
にリダイレクトされると思います

流れとしては、
http://localhost:3001/user/sign_in
↓ 「Sign in with Mock」をクリックすると下記リンクに遷移
http://localhost:3001/users/auth/mock
↓ 認証用URLにリダイレクト
http://localhost:3000/oauth/authorize
↓ ログインしていければ認証情報を入力、認証後、コールバック関数を実行
http://localhost:3001/users/auth/mock/callback?code=xxxxx

Client側でのログインを行わずに、Providerサーバでの認証を行えていることが分かります
(ChromeのNetWorkタブを利用すると分かりやすいです)
![スクリーンショット 2018-12-24 14.32.32.png](https://qiita-image-store.s3.amazonaws.com/0/280433/0cf30c8f-20ee-5b4c-d9e7-b8c2b7e57d3c.png)

# おまけ
このままだと認可が与えられているか分かりにくいので、下記処理を追加しました。
今回は「localhost:3001/home」をログインしないと参照できないページと見立てています。
ログイン後に表示させるページ用のcontrollerを作成

```
bundle exec rails generate controller home index
```

Clientサーバ側のrouteに下記を追記してあげます。
「as: "user_root」をつけるとリダイレクト先のページを指定することができます。

```Ruby:~/workspace/client/config/route.rb
get "home", to: "home#index", as: "user_root
```

Controllerに認証済みのuserかを確認するように修正

```Ruby:~/workspace/app/controllers/home_controller.rb
class HomeController < ApplicationController
  before_action :authenticate_user!
  def index
  end
end
```
これで、「localhost:3001/home」にアクセスした際に認証が済んでいないと、ログイン画面に戻されるようになるかと思います。

# 参考
Rails導入周り
https://qiita.com/yoshiokatsuneo@github/items/d75d4fef9e75afb65126#mac%E3%81%A7%E3%81%AErails%E3%82%A4%E3%83%B3%E3%82%B9%E3%83%88%E3%83%BC%E3%83%AB


Devise, Doorkeeper, omuniauth周り
https://tech.mof-mof.co.jp/blog/rails5-devise-doorkeeper.html
https://qiita.com/8398a7/items/9b13ac2e7c401dee4a39
https://qiita.com/kami_zh/items/94aec2d94a2b4e9a1d0b
https://qiita.com/kyonsuke19101/items/407f3cdfec38d1108e9d
https://easyramble.com/create-views-on-rails-devise.html
https://teratail.com/questions/143329

ログイン前後の処理周りは
https://qiita.com/ytkt/items/d78841f7dea5e29f38ee

## おわりに
・勉強のために作リ始めたはずが結構時間がかかってしまった・・まだ理解が浅いのでもう少し深掘りしたい
・諸所でうまく行かない箇所を潰しながら進めたので、同じ手順で動かないなどあればご連絡いただけるとありがたいです
・参考にさせていただいた記事の執筆者の方々、大変ありがとうございます