OAuth2.0の認可サーバーとクライアントをRailsで実装し、アクセストークンでユーザーのリソースを取得する仕組みを開発環境で再現
OAuth2.0の規約を用いて、クライアントアプリケーションが認可サーバー側のアプリケーションののリソースを取得する以下の流れを開発環境でRailsで実装しました。
認可サーバー側のgemにはdoorkeeperを使います。
実装した流れ
1. クライアントアプリケーションから認可サーバーの認証URLにアクセスして認証を行う
2. リソースオーナーが、クライアントアプリケーションが認可サーバー側のリソースにアクセスすることを許可する
3. 認可サーバーが、クライアントアプリケーションのURLに認可コードを付与してリダイレクトする
4. クライアントアプリケーションが認可コードを利用して、認可サーバーからアクセストークンを取得する
5. クライアントアプリケーションが4で取得したアクセストークンを用いて、認可サーバー側のリソースを取得する
準備
以下のリポジトリを使用します。
認可サーバ用のリポジトリ
クライアントアプリケーション用のリポジトリ
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の設定をします。
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 というホスト名でアクセスされるので以下の設定を追加します。
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の設定を追加します
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の設定を追加します。
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 としてください。
クライアントアプリケーション側の実装(client_appディレクトリ)
gemを追加してインストールします
gem 'oauth2', '~> 2.0', '>= 2.0.9'
先ほど作成した認可サーバー側のアプリケーションのリダイレクトURIに対応するAPIを作成します。
get "oauth/callback" => "oauth#callback"
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