OAuth2.0の認可サーバーとクライアントをRailsで実装し、アクセストークンでユーザーのリソースを取得する仕組みを開発環境で再現

2024/01/10に公開

OAuth2.0の規約を用いて、クライアントアプリケーションが認可サーバー側のアプリケーションののリソースを取得する以下の流れを開発環境でRailsで実装しました。

認可サーバー側のgemにはdoorkeeperを使います。

実装した流れ

1. クライアントアプリケーションから認可サーバーの認証URLにアクセスして認証を行う

2. リソースオーナーが、クライアントアプリケーションが認可サーバー側のリソースにアクセスすることを許可する

3. 認可サーバーが、クライアントアプリケーションのURLに認可コードを付与してリダイレクトする

4. クライアントアプリケーションが認可コードを利用して、認可サーバーからアクセストークンを取得する

5. クライアントアプリケーションが4で取得したアクセストークンを用いて、認可サーバー側のリソースを取得する

準備

以下のリポジトリを使用します。

認可サーバ用のリポジトリ
https://github.com/ryotaro-tenya0727/authorization_end_point_app

クライアントアプリケーション用のリポジトリ
https://github.com/ryotaro-tenya0727/client_app

docker-compose間で通信するためにホスト側で以下を実行してnetworkを作成します。

docker network create --driver bridge shared-network

リポジトリをクローンして以下のディレクトリ構成を作成します。

├ authorization_end_point_app(ディレクトリ)
└ client_app(ディレクトリ)

クローンしたらそれぞれのアプリケーションに対して以下を実行して、Railsの最初のスタート画面が出るところまで進めます。

authorization_end_point_app の場合のコマンドです。client_appも同じコマンドを実行します。

docker-compose build

docker-compose run --rm authorization_end_point_app rails db:create

docker-compose up -d

authorization_end_point_app はlocalhost:3005 client_app は localhost:3006 で以下の画面が出るところまで進めます。

認可サーバー側の実装(authorization_end_point_appディレクトリ)

認可サーバ用のリポジトリに実装していきます。

Gemfileにgemを追加してインストールします。

gem 'doorkeeper', '~> 5.6', '>= 5.6.8'
gem 'devise', '~> 4.9', '>= 4.9.3'
gem 'rack-cors', '~> 2.0', '>= 2.0.1'

doorkeeperに必要なファイルを生成するコマンドを実行します。

bundle exec rails generate doorkeeper:install

# 以下のログが出ます
create  config/initializers/doorkeeper.rb
create  config/locales/doorkeeper.en.yml
route  use_doorkeeper

doorkeeperの使用に必要なmigrationファイルを生成するコマンドを実行後、migrateしてテーブルを作成します

bundle exec rails generate doorkeeper:migration

bundle exec rails db:migrate

CORSの設定

認可サーバー側のcorsの設定をします。

config/initializers/cors.rb
Rails.application.config.middleware.insert_before 0, Rack::Cors do
  allow do
    origins 'client_app:3006' # 許可するオリジンを指定
    resource '*', headers: :any, methods: [:get, :post, :put, :patch, :delete, :options, :head]
  end
end

許可するホスト名の追加

認可サーバーはauthorization_end_point_app というホスト名でアクセスされるので以下の設定を追加します。

config/environments/development.rb
Rails.application.configure do
  config.hosts << "authorization_end_point_app"
# 以下略
.
.
.

ログイン機能

認可サーバー側のアプリケーションログイン機能を実装します。

bin/rails g devise:install

config/environments/development.rb に追加

config.action_mailer.default_url_options = { host: 'localhost', port: 3005 }

deviseのviewとモデルを作成します

bin/rails g devise:views

bin/rails g devise User

上記で作成されたusersテーブルに関するmigrationファイルにnameカラムとimage_urlカラムを追加しておきます。(ファイル作成時にコメントアウトされていた部分は削除してます。)

class DeviseCreateUsers < ActiveRecord::Migration[7.1]
  def change
    create_table :users do |t|
      ## Database authenticatable
      t.string :email, null: false, default: ""
      t.string :name, null: false, default: ""
      t.string :image_url
      t.string :encrypted_password, null: false, default: ""

      ## Recoverable
      t.string   :reset_password_token
      t.datetime :reset_password_sent_at

      ## Rememberable
      t.datetime :remember_created_at
      t.timestamps null: false
    end

    add_index :users, :email,                unique: true
    add_index :users, :reset_password_token, unique: true
  end
end

migrateしてテーブル作成

rails db:migrate

Userモデルにdoorkeeperで作成したテーブルへの関連付けと、deviseの設定を追加します

app/models/user.rb
class User < ApplicationRecord
  extend Devise::Models
  # Include default devise modules. Others available are:
  # :confirmable, :lockable, :timeoutable, :trackable and :omniauthable
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :validatable

  has_many :access_grants,
          class_name: 'Doorkeeper::AccessGrant',
          foreign_key: :resource_owner_id,
          dependent: :destroy # or :destroy if you need callbacks

  has_many :access_tokens,
           class_name: 'Doorkeeper::AccessToken',
           foreign_key: :resource_owner_id,
           dependent: :destroy # or :destroy if you need callbacks
end

deviseのサインアップ時のnameパラメーターを追加しておきます

app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
  before_action :configure_permitted_parameters, if: :devise_controller?

  protected
  def configure_permitted_parameters
    devise_parameter_sanitizer.permit(:sign_up, keys: [:name])
  end
end

nameフィールドをviewに追加しておきます

app/views/devise/registrations/new.html.erb
<h2>Sign up</h2>

<%= form_for(resource, as: resource_name, url: registration_path(resource_name)) do |f| %>
  <%= render "devise/shared/error_messages", resource: resource %>

  <div class="field">
    <%= f.label :email %><br />
    <%= f.email_field :email, autofocus: true, autocomplete: "email" %>
  </div>

  <div class="name">
    <%= f.label :name %><br />
    <%= f.text_field :name  %>
  </div>

  <div class="field">
    <%= f.label :password %>
    <% if @minimum_password_length %>
    <em>(<%= @minimum_password_length %> characters minimum)</em>
    <% end %><br />
    <%= f.password_field :password, autocomplete: "new-password" %>
  </div>

  <div class="field">
    <%= f.label :password_confirmation %><br />
    <%= f.password_field :password_confirmation, autocomplete: "new-password" %>
  </div>

  <div class="actions">
    <%= f.submit "Sign up" %>
  </div>
<% end %>

<%= render "devise/shared/links" %>

ログインしていることがわかるようにルートページにログイン情報が表示されるようにしておきます

config/routes.rb
root to: "home#index"
app/controllers/home_controller.rb
class HomeController < ApplicationController
  before_action :authenticate_user!, only: :index

  def index
  end
end
app/views/home/index.html.erb
<h1>Home#index</h1>
<p>Find me in app/views/home/index.html.erb</p>

<%= "メールアドレス: #{current_user.email}" %>
<br>
<%= "名前: #{current_user.name}" %>
<%= button_to "ログアウト", destroy_user_session_path, method: :delete %>

doorkeeperの設定ファイルの記述

doorkeeperの設定を追加します。

config/initializers/doorkeeper.rb
Doorkeeper.configure do

  # リソースオーナーにの認証に関するロジックを記述します。
  # 認可サーバー側のログインしているユーザー(リソースオーナー)を返したいので以下のように記述します。
  resource_owner_authenticator do
    current_user || warden.authenticate!(scope: :user)
  end

  # /oauth/applications のOAuthのアプリケーションリストの表示に関するロジックを記述します。
  # 今回はいつでも見れるようにしたいため空白にしてます。
  # 例 redirect_to root unless current_user.admin? 
  admin_authenticator do |_routes|

  end

  # scopeに関する設定です。今回は記述しなくても大丈夫です。
  # 参考 https://github.com/doorkeeper-gem/doorkeeper/wiki/Using-Scopes
  default_scopes :public

  optional_scopes :write
  
  # 以下省略

アクセストークンを使ってユーザーのプロフィール情報を取得するためのエンドポイント

ルーティングを追加します

config/routes.rb
namespace :api do
  namespace :v1 do
    get '/me' => 'users#me'
  end
end
app/controllers/api/v1/users_controller.rb
class Api::V1::UsersController < ApplicationController
  before_action :doorkeeper_authorize!
  respond_to :json

  def me
    respond_with current_resource_owner
  end

  private

  def current_resource_owner
    User.find(doorkeeper_token.resource_owner_id) if doorkeeper_token
  end
end

/users/sign_up にアクセスして、ユーザーを作成しログイン状態にします。

/oauth/applications にアクセスして、oauth_applications を作成します。

作成すると、以下のようにclient uidとシークレットが発行されます。

scopesをpublic
Callback urlsを http://localhost:3006/oauth/callback としてください。

クライアントアプリケーション側の実装(client_appディレクトリ)

gemを追加してインストールします

gem 'oauth2', '~> 2.0', '>= 2.0.9'

先ほど作成した認可サーバー側のアプリケーションのリダイレクトURIに対応するAPIを作成します。

ruby
get "oauth/callback"  => "oauth#callback"
app/controllers/oauth_controller.rb
class OauthController < ApplicationController
  def callback
    host = 'authorization_end_point_app'
    client = OAuth2::Client.new(
                                '#{先ほど作成したoauth_applicationのUID}',
                                '#{先ほど作成したoauth_applicationのSecret}',
                                site: "http://#{host}:3005",
                                authorize_url: "http://#{host}:3005/oauth/authorize",
                                token_url: "http://#{host}:3005/oauth/token",
                              )
    # 以下は認証URLが取得できる
    # client.auth_code.authorize_url(redirect_uri: 'http://localhost:3006/oauth/callback')
    access = client.auth_code.get_token(
                                        params[:code],
                                        redirect_uri: 'http://localhost:3006/oauth/callback'
                                      )
    token = access.token
    response = access.get('/api/v1/me', headers: {"Authorization" => "Bearer #{token}"})
    body = JSON.parse(response.body)
    binding.pry

  end
end

実装が完了したので、うまく行けば上記の binding.pry でのデバッグに成功しbodyには認可サーバー側でログインしたユーザーの情報が入っているはずです。

対象のアプリのCallbackURLsのAuthorizeから認証します。
本来のアプリケーションではこのAuthorizeボタンのURLが、クライアントアプリケーション内に設置してあります。

client_app が authorization_end_point_app にアクセスを許可するかどうか確認する画面が出るので、authorizeを押します。

以下のように認可サーバー側でログインしたユーザーの情報を取得できれば成功です!

[1] pry(#<OauthController>)> body
=> {"id"=>6, "email"=>"ryotaro123@test.com", "name"=>"test_ryotaro", "image_url"=>nil, "created_at"=>"2024-01-06T22:20:04.345Z", "updated_at"=>"2024-01-06T22:20:04.345Z"}

Discussion