OAuth2.0の認可サーバーとクライアントをRailsで実装
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
で以下の画面が出るところまで進めます。
認可サーバ用のリポジトリに実装していきます。
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 の設定をします。
172.30.0.3
の部分は docker network inspect shared-network
を実行した時の client_app
の IPv4Address
を入力してください。
Rails.application.config.middleware.insert_before 0, Rack::Cors do
allow do
origins 'http://172.30.0.3:3004' # 許可するオリジンを指定
resource '*', headers: :any, methods: [:get, :post, :put, :patch, :delete, :options, :head]
end
end
認可サーバー側のアプリケーションログイン機能を実装します。
bin/rails g devise:install
config/environments/development.rb に追加
config.action_mailer.default_url_options = { host: 'localhost', port: 3003 }
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 の設定を追加します
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 パラメーターを追加しておきます
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 に追加しておきます
<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" %>
ログインしていることがわかるようにルートページにログイン情報が表示されるようにしておきます
root to: "home#index"
class HomeController < ApplicationController
before_action :authenticate_user!, only: :index
def index
end
end
<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.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
# 以下省略
ルーティングを追加します
namespace :api do
namespace :v1 do
get '/me' => 'users#me'
end
end
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 としてください。
gem を追加してインストールします
gem 'oauth2', '~> 2.0', '>= 2.0.9'
先ほど作成した認可サーバー側のアプリケーションのリダイレクト URI に対応する API を作成します。
get "oauth/callback" => "oauth#callback"
認可サーバーの IPv4Address は docker network inspect shared-network
で確認します。
class OauthController < ApplicationController
def callback
host = '#{認可サーバーのIPv4Address}'
client = OAuth2::Client.new(
'#{先ほど作成したoauth_applicationのUID}',
'#{先ほど作成したoauth_applicationのSecret}',
site: "http://#{host}:3003",
authorize_url: "http://#{host}:3003/oauth/authorize",
token_url: "http://#{host}:3003/oauth/token",
)
# 以下は認証URLが取得できる
# client.auth_code.authorize_url(redirect_uri: 'http://localhost:3004/oauth/callback')
access = client.auth_code.get_token(
params[:code],
redirect_uri: 'http://localhost:3004/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"
}