ITエンジニアによるITエンジニアのためのブログ

【Rails】OAuth 2.0の認可サーバー・リソースサーバー・クライアントを構築する

本記事では、OAuth 2.0の認可サーバーリソースサーバークライアントを、それぞれRuby on Railsで構築する方法を解説します。

普段OAuthクライアントのみを実装する方も、認可サーバーやリソースサーバーを実際に構築して動かすことで、OAuthの仕組みをより深く理解できます。

なお、OAuth 2.0には複数のグラントタイプ(認可フロー)がありますが、本記事では最もセキュアで広く推奨されている「認可コードグラント(Authorization Code Grant)」を扱います。

目次

  1. 認可コードグラントの仕組み
  2. 認可サーバーの設定
  3. リソースサーバーの設定
  4. クライアントの設定
  5. 全体の動作フロー
  6. まとめ

1. 認可コードグラントの仕組み

認可コードグラントは、Webアプリケーションで安全に外部サービスのデータにアクセスするための、最も標準的でセキュアな認可フローです。

登場人物

このフローには、5つの役割が登場します。

  1. リソースオーナー: エンドユーザー本人です。
  2. ユーザーエージェント: エンドユーザーが操作するブラウザです。
  3. クライアント: リソースオーナーが利用するアプリケーションです。認可サーバーから発行されたアクセストークンを使い、リソースサーバー上のユーザー情報にアクセスします。
  4. 認可サーバー: 以下の役割を担うサーバーです。
    • リソースオーナーを認証する。
    • クライアントに認可コードを発行する。
    • クライアントにアクセストークンを発行する。
  5. リソースサーバー: リソースオーナーの情報を保護・管理するサーバーです。有効なアクセストークンを持つクライアントに対して、リソースへのアクセスを許可します。

本記事では、認可サーバーとリソースサーバーを一つのRailsアプリケーションで、クライアントを別のRailsアプリケーションで構築します。

認可コードグラントの流れ

5つの登場人物が、以下の流れで連携します。

  1. [認可リクエスト] ユーザーがクライアントアプリ上で「外部サービスでログイン」ボタンなどをクリックします。クライアントは、ユーザーを認可サーバーへリダイレクトさせます。
  2. [ユーザー認証・同意] 認可サーバーは、ユーザーにIDとパスワードの入力を求め認証します。認証後、「クライアントアプリにデータへのアクセスを許可しますか?」という同意画面を表示します。
  3. [認可コード発行] ユーザーが同意すると、認可サーバーは一時的な認可コードを発行し、ユーザーをクライアントアプリへリダイレクト(コールバック)させます。
  4. [アクセストークン交換] クライアントは、受け取った認可コードを使い、バックグラウンドで認可サーバーにアクセストークンを要求します。
  5. [アクセストークン発行] 認可サーバーは認可コードを検証し、問題がなければアクセストークンを発行してクライアントに渡します。
  6. [リソースアクセス] クライアントは、取得したアクセストークンを使って、リソースサーバーに保護された情報(例: ユーザープロフィール)を要求します。
  7. [リソース提供] リソースサーバーはアクセストークンを検証し、有効であればクライアントに要求された情報を返します。

この仕組みの最大の利点は、ユーザーのIDやパスワードがクライアントに渡ることがないため、安全にサービス連携が実現できる点です。

認可サーバーの設定

まず、認可と認証の仕組みを持つ認可サーバーを構築し、クライアントアプリケーションを登録します。

認可の仕組み (doorkeeper)

OAuth 2.0の認可サーバー機能は、doorkeeper gemを利用して構築します。

1. Gemfileにdoorkeeperを追加し、インストールします。

# Gemfile
gem 'doorkeeper'
bundle install

2.以下のコマンドで、doorkeeperの設定ファイルと関連マイグレーションを生成し、データベースを更新します。

rails generate doorkeeper:install
rails generate doorkeeper:migration
rails db:migrate

これにより、クライアント情報 (oauth_applications)、認可コード (oauth_access_grants)、アクセストークン (oauth_access_tokens) を管理するテーブルが作成されます。

3.config/initializers/doorkeeper.rb を編集し、リソースオーナーの認証方法を定義します。

作成されたdoorkeeper設定ファイルに以下の設定を記載します。

# config/initializers/doorkeeper.rb
Doorkeeper.configure do
  orm :active_record

  # リソースオーナー(ユーザー)が認証済みかチェックする
  resource_owner_authenticator do
    # sessionにuser_idがなければログインページへリダイレクト
    # 認証後は元の認可フローに戻れるよう、リクエストURLをsessionに保存しておく
    User.find_by(id: session[:user_id]) || begin
      session['user_return_to'] = request.url
      redirect_to(api_v1_auth_path)
    end
  end
end

4.config/routes.rbuse_doorkeeper を追記し、doorkeeperが提供する認可関連のエンドポイントを有効化します。

# config/routes.rb
Rails.application.routes.draw do
  use_doorkeeper
  # ... 他のルーティング
end

use_doorkeeper は、認可リクエスト (/oauth/authorize) やトークン発行 (/oauth/token) など、必要なエンドポイントを自動で設定します。

認証の仕組み (has_secure_password)

doorkeeperは認可の仕組みを提供しますが、ユーザー認証(ログイン機能)は別途実装が必要です。ここではRails標準の has_secure_password を用います。

1.ログイン処理用のルーティングを追加します。

# config/routes.rb
Rails.application.routes.draw do
  use_doorkeeper

  namespace :api do
    namespace :v1 do
      # ログインフォーム
      get 'auth', to: 'sessions#new'
      # ログイン処理
      post 'auth', to: 'sessions#create'
      # ...
    end
  end
end

2.SessionsController を作成し、ログイン・ログアウト処理を実装します。

# app/controllers/api/v1/sessions_controller.rb
module Api
  module V1
    class SessionsController < ApplicationController
      def new; end

      def create
        user = User.find_by(email: params[:email])&.authenticate(params[:password])

        if user
          session[:user_id] = user.id
          # doorkeeperの処理で保存したURLにリダイレクトして認可フローに戻る
          redirect_to session.delete('user_return_to') || root_path, notice: 'ログインしました'
        else
          flash.now[:alert] = 'メールアドレスまたはパスワードが正しくありません'
          render 'new'
        end
      end
    end
  end
end

3.User モデルで has_secure_password を有効にします。

class User < ApplicationRecord
  has_secure_password
end

4.ログインフォームを作成します。

# app/views/api/v1/sessions/new.html.erb
<%= form_with url: api_v1_auth_path do |f| %>
  <%= f.label 'Email', for: :email %>
  <%= f.email_field :email %>
  <%= f.label 'パスワード', for: :password %>
  <%= f.password_field :password %>
  <%= f.submit :send %>
<% end %>

クライアント登録

認可サーバーを起動し、doorkeeperが提供する管理画面 (/oauth/applications/new) にアクセスして、クライアントアプリを登録します。

*本番環境では、この管理画面を管理者のみがアクセスできるよう制限してください。

フォームを送信すると、クライアントアプリの識別に必要な Application UIDSecret が発行されます。これらは後ほどクライアント側の設定で使用します。

3. リソースサーバーの設定

認可サーバーと同じRailsアプリケーション内に、リソースサーバーの機能も構築します。

1.ユーザー情報を返すAPIエンドポイント (/api/v1/profiles/me) を routes.rb に追加します。

# config/routes.rb
namespace :api do
  namespace :v1 do
    # ... ログイン関連
    get 'profiles/me', to: 'profiles#me'
  end
end

2.ProfilesController を作成し、APIを保護します。

before_action :doorkeeper_authorize! を設定することで、このコントローラのアクションは有効なアクセストークンがなければアクセスできなくなります。

# app/controllers/api/v1/profiles_controller.rb
module Api
  module V1
    class ProfilesController < ActionController::API
      before_action :doorkeeper_authorize!

      def me
        render json: current_resource_owner
      end

      private

      # doorkeeper_tokenから現在のリソースオーナー(ユーザー)を取得する
      def current_resource_owner
        User.find(doorkeeper_token.resource_owner_id) if doorkeeper_token
      end
    end
  end
end

4. クライアントの設定

次に、全く別のRailsアプリケーションでクライアントを構築します。外部サービス認証を簡単にするため、以下のgemを使用します。

# Gemfile
gem 'devise' # ユーザー認証
gem 'omniauth' # 外部認証フレームワーク
gem 'omniauth-rails_csrf_protection'# OmniAuthのPOSTリクエストに対するCSRF対策
gem 'omniauth-oauth2' # OmniAuthでOAuth 2.0を扱うための基本ストラテジー

OmniAuth ストラテジーの作成

先ほど構築した認可サーバーを、OmniAuthの新しい認証プロバイダーとして登録するための「ストラテジー」を作成します。

# lib/omniauth/strategies/engineerjutsu.rb
require 'omniauth-oauth2'

module OmniAuth
  module Strategies
    class Engineerjutsu < OmniAuth::Strategies::OAuth2
      # ストラテジー名
      option :name, 'engineerjutsu'

      # 認可サーバーのURLを設定
      option :client_options, {
        site: 'https://engineerjutsu.com:3000' # 認可サーバーのベースURL
      }

      # 認証プロバイダーにおけるユーザーの一意なID
      uid { raw_info['id'] }

      # 標準化されたユーザー情報 (例: email)
      info do
        {
          email: raw_info['email']
        }
      end

      # 認証プロバイダーから得た生のユーザー情報
      extra do
        { 'raw_info' => raw_info }
      end

      private

      # アクセストークンを使ってリソースサーバーからユーザー情報を取得
      def raw_info
        @raw_info ||= access_token.get('/api/v1/profiles/me').parsed
      end
    end
  end
end

DeviseとOmniAuthの設定

1.認可サーバーで発行されたクライアントのUIDとSecretを、.envファイルなどで環境変数として設定します。

#.env
ENGINEERJUTSU_CLIENT_ID="iN2zJ56CDrVonpQd4UAg9ZCBb69QW8w-s53M50CmLKY"
ENGINEERJUTSU_CLIENT_SECRET="xot6V6R9rN7St83FFs2duHCCX8aXpKjFrzhb3V3Gepk"

2.config/initializers/devise.rbで、作成したストラテジーをOmniAuthプロバイダーとして登録します。

# config/initializers/devise.rb
# 先ほど作成したストラテジーファイルを読み込む
require Rails.root.join('lib/omniauth/strategies/engineerjutsu')

Devise.setup do |config|
  # ...
  config.omniauth :engineerjutsu, ENV['ENGINEERJUTSU_CLIENT_ID'], ENV['ENGINEERJUTSU_CLIENT_SECRET']
end

この設定により、以下の2つのパスが自動的に利用可能になります。

  • /users/auth/engineerjutsu: リクエストフェーズ。認可サーバーへのリダイレクトを開始します。
  • /users/auth/engineerjutsu/callback: コールバックフェーズ。認可サーバーからリダイレクトされ、認証結果を処理します。

3.Users::OmniauthCallbacksController を作成し、コールバックフェーズの処理を実装します。OmniAuthは認証結果を request.env['omniauth.auth'] に格納してくれます。

# app/controllers/users/omniauth_callbacks_controller.rb
class Users::OmniauthCallbacksController < Devise::OmniauthCallbacksController
  def engineerjutsu
    # OmniAuthが認可サーバーから取得したユーザー情報を格納したハッシュ
    auth_hash = request.env['omniauth.auth']

    # ここで、auth_hashを元にユーザーを検索または新規作成し、
    # サインインさせるロジックを実装するのが一般的です。
    # @user = User.from_omniauth(auth_hash)
    # sign_in_and_redirect @user, event: :authentication

    # 本記事ではデモのため、取得した情報を画面に表示するのみとします。
    render plain: auth_hash.to_yaml
  end

  def failure
    redirect_to root_path, alert: "認証に失敗しました。"
  end
end

4.ログインリンクをビューに設置します。

<%= link_to 'Engineerjutsuでログイン', user_engineerjutsu_omniauth_authorize_path, method: :post %>

5. 全体の動作フロー

すべての設定が完了したので、実際の動作を確認します。認可&リソースサーバーをポート3000で、クライアントをポート3001で起動します。

rails s -p 3000 (認可/リソースサーバー)

rails s -p 3001 (クライアント)

ステップ1: クライアントから認可サーバーへ

ユーザーがクライアントアプリ(https://engineerjutsu.com:3001)で「Engineerjutsuでログイン」リンクをクリックします。omniauth gemがPOSTリクエスト(/auth/engineerjutsu)を受け、ユーザーを認可サーバー(https://engineerjutsu.com:3000)の認可エンドポイントへリダイレクトします。

リダイレクト先URLの例:

https://engineerjutsu.com:3000/oauth/authorize?client_id=...&redirect_uri=...&response_type=code&state=...

パラメータ説明
client_idクライアントを識別するID。認可サーバーでクライアント登録時に発行されたUIDと同一。
redirect_uri認可後に戻ってくるクライアントのコールバックURL。
response_typecode を指定し、認可コードグラントを要求。
stateCSRF攻撃を防ぐためのランダムな文字列。

ステップ 2: 認可サーバーでの認証と同意

ユーザーは、認可サーバーで認可エンドポイントからログインパス(https://engineerjutsu.com:3000/api/v1/auth)にリダイレクトされ、ログインを求められます。

ログイン成功後、認可エンドポイントにリダイレクトバックし、「(クライアント名)があなたのアカウント情報にアクセスすることを許可しますか?」という同意画面が表示されます。

ステップ 3: 認可コード発行とクライアントへのリダイレクト

ユーザーが「Authorize」ボタンをクリックすると、認可コード作成リクエスト(POST /oauth/authorize)が認可サーバーに送信されます。認可サーバーは一時的な認可コードを発行し、ユーザーをクライアントのコールバックURLへリダイレクトさせます。

リダイレクト先URLの例: https://engineerjutsu.com:3001/users/auth/engineerjutsu/callback?code=...&state=...

URLパラメーターの意味は以下の通りです。

  • code: アクセストークン発行に必要な認可コード
  • state: クライアントから認可サーバーへの元々のリダイレクトに含まれていた、CSRF防止の値と同一の値。

ステップ 4: OmniAuthによるバックグラウンド処理

クライアントのコールバックURLが呼び出されると、OmniAuthストラテジーが水面下で以下の処理を自動的に行います

  1. 受け取った認可コードクライアントシークレットを認可サーバーのトークンエンドポイント (/oauth/token) に送信します。POSTパラメーターは以下の通りです。
    • client_id: 認可サーバーでクライアント登録時に発行されたUID。
    • client_secret: 認可サーバーでクライアント登録時に発行されたシークレット。
    • code: 認可サーバーより受け取った認可コード。
    • grant_type: 固定値で”authorization_code”.
    • redirect_uri: 最初のクライアントから認可サーバーへのリダイレクトURLに含まれていたredirect_uriパラメーターと同一。この例ではredirect_uri=https%3A%2F%2Fengineerjutsu.com%3A3001%2Fauth%2Fengineerjutsu%2Fcallback
  2. 認可サーバーはこれらを検証し、問題なければアクセストークンを返却します。
  3. ストラテジーは、受け取ったアクセストークンを使い、リソースサーバーのAPI (/api/v1/profiles/me) にアクセスしてユーザー情報を取得します。

ステップ 5: コールバック処理の実行

OmniAuthストラテジーは、ステップ4で取得したすべての情報(UID, ユーザー情報, トークンなど)を request.env['omniauth.auth'] というハッシュにまとめて、Users::OmniauthCallbacksControllerengineerjutsu アクションを呼び出します。

開発者はこの omniauth.auth ハッシュを使って、ユーザーのサインインや新規登録といったアプリケーション固有の処理を実装します。

6. まとめ

本記事では、Railsとdoorkeeper, omniauth gemを利用して、OAuth 2.0の認可サーバー、リソースサーバー、クライアントの一連の仕組みを構築しました。

実際に全体を構築することで、各コンポーネントの役割や、認可コード、アクセストークンがどのように連携して安全な認証・認可を実現しているかの理解が深まったのではないでしょうか。

参考リンク