OmniAuth (Ruby)

認証ライブラリOmniAuthRubyRailsOAuthOpenID多認証Rack

認証ライブラリ

OmniAuth (Ruby)

概要

OmniAuthは、Rubyアプリケーション向けの柔軟な認証システムです。Rackミドルウェアを活用し、OAuth、OpenID、その他多数の認証プロバイダーとの統合を簡単に実現できます。戦略パターンを使用して様々な認証サービス(Google、Facebook、Twitter、GitHub等)をプラグイン方式で追加でき、RailsをはじめとするRack対応フレームワークでの認証実装を大幅に簡素化します。

詳細

OmniAuthは、Ruby Webアプリケーション開発における事実上の標準認証ライブラリとして長年にわたって信頼されています。主な特徴として、戦略(Strategy)ベースのアーキテクチャによる拡張性、複数認証プロバイダーの同時サポート、統一されたAPIインターフェース、CSRF保護機能があります。

OmniAuth 2.0以降では、セキュリティ強化のためCSRF保護がデフォルトで有効になり、Rails環境ではomniauth-rails_csrf_protection gemとの組み合わせが推奨されています。また、開発環境向けの:developer戦略や、本格的なプロダクションでの外部プロバイダー戦略など、用途に応じた柔軟な設定が可能です。

認証フローは標準化されており、すべての戦略が共通の/auth/:provider/callbackエンドポイントを使用し、認証情報はrequest.env['omniauth.auth']として一貫した形式で提供されます。

メリット・デメリット

メリット

  • 戦略パターン: プラグイン形式で多数の認証プロバイダーをサポート
  • 統一API: プロバイダーが異なっても一貫したインターフェース
  • Rack統合: RailsやSinatraなど主要Rubyフレームワークでの使用が容易
  • 豊富なエコシステム: 100を超える認証戦略が利用可能
  • CSRF保護: セキュリティベストプラクティスに対応
  • 設定の柔軟性: 開発環境から本番環境まで適応可能

デメリット

  • Ruby限定: Ruby以外の言語では使用不可
  • セットアップの複雑さ: 初期設定と戦略選択が複雑になる場合がある
  • CSRF設定: Rails環境でのCSRF保護設定が必要
  • セッション管理: アプリケーション側でのセッション管理実装が必要

参考ページ

書き方の例

Gemfileの設定

# Gemfile
gem 'omniauth'
gem 'omniauth-rails_csrf_protection' # Rails用CSRF保護

# 必要な戦略を追加
gem 'omniauth-google-oauth2'
gem 'omniauth-facebook'
gem 'omniauth-twitter'
gem 'omniauth-github'

Rails初期化設定

# config/initializers/omniauth.rb
Rails.application.config.middleware.use OmniAuth::Builder do
  # 開発環境用の簡易戦略
  provider :developer unless Rails.env.production?
  
  # Google OAuth2
  provider :google_oauth2, 
           ENV['GOOGLE_CLIENT_ID'], 
           ENV['GOOGLE_CLIENT_SECRET']
  
  # Facebook
  provider :facebook, 
           ENV['FACEBOOK_APP_ID'], 
           ENV['FACEBOOK_APP_SECRET']
  
  # GitHub
  provider :github, 
           ENV['GITHUB_CLIENT_ID'], 
           ENV['GITHUB_CLIENT_SECRET']
end

# CSRF保護の設定(rack_csrf gemを使用する場合)
OmniAuth::AuthenticityTokenProtection.default_options(
  key: "csrf.token", 
  authenticity_param: "_csrf"
)

ルート設定

# config/routes.rb
Rails.application.routes.draw do
  # OmniAuthコールバック
  get '/auth/:provider/callback', to: 'sessions#create'
  
  # ログインページ
  get '/login', to: 'sessions#new'
  
  # ログアウト
  delete '/logout', to: 'sessions#destroy'
  
  root 'home#index'
end

セッションコントローラー

# app/controllers/sessions_controller.rb
class SessionsController < ApplicationController
  def new
    # ログインページの表示
  end

  def create
    # OmniAuth認証後のコールバック処理
    auth_data = request.env['omniauth.auth']
    
    user = User.find_or_create_by(email: auth_data.info.email) do |u|
      u.name = auth_data.info.name
      u.provider = auth_data.provider
      u.uid = auth_data.uid
      u.avatar_url = auth_data.info.image
    end

    if user.persisted?
      session[:user_id] = user.id
      redirect_to root_path, notice: 'ログインしました'
    else
      redirect_to login_path, alert: 'ログインに失敗しました'
    end
  end

  def destroy
    session[:user_id] = nil
    redirect_to root_path, notice: 'ログアウトしました'
  end
end

Userモデル

# app/models/user.rb
class User < ApplicationRecord
  validates :email, presence: true, uniqueness: true
  validates :name, presence: true
  validates :provider, presence: true
  validates :uid, presence: true

  def self.from_omniauth(auth_data)
    where(provider: auth_data.provider, uid: auth_data.uid).first_or_create do |user|
      user.email = auth_data.info.email
      user.name = auth_data.info.name
      user.avatar_url = auth_data.info.image
    end
  end

  def display_name
    name.presence || email.split('@').first
  end
end

# マイグレーション例
# rails generate migration CreateUsers email:string name:string provider:string uid:string avatar_url:string

ログインビュー

<!-- app/views/sessions/new.html.erb -->
<div class="login-page">
  <h1>ログイン</h1>
  
  <div class="login-options">
    <!-- 開発環境用 -->
    <% unless Rails.env.production? %>
      <%= form_tag('/auth/developer', method: 'post', data: {turbo: false}) do %>
        <button type='submit' class="btn btn-developer">
          開発者ログイン
        </button>
      <% end %>
    <% end %>
    
    <!-- Google -->
    <%= link_to '/auth/google_oauth2', method: :post, 
                data: {turbo: false}, 
                class: 'btn btn-google' do %>
      <i class="fab fa-google"></i> Googleでログイン
    <% end %>
    
    <!-- Facebook -->
    <%= link_to '/auth/facebook', method: :post, 
                data: {turbo: false}, 
                class: 'btn btn-facebook' do %>
      <i class="fab fa-facebook"></i> Facebookでログイン
    <% end %>
    
    <!-- GitHub -->
    <%= link_to '/auth/github', method: :post, 
                data: {turbo: false}, 
                class: 'btn btn-github' do %>
      <i class="fab fa-github"></i> GitHubでログイン
    <% end %>
  </div>
</div>

ApplicationController認証ヘルパー

# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
  before_action :authenticate_user!, except: [:index, :show]

  private

  def current_user
    @current_user ||= User.find(session[:user_id]) if session[:user_id]
  end
  helper_method :current_user

  def user_signed_in?
    current_user.present?
  end
  helper_method :user_signed_in?

  def authenticate_user!
    unless user_signed_in?
      redirect_to login_path, alert: 'ログインが必要です'
    end
  end

  def require_admin
    unless current_user&.admin?
      redirect_to root_path, alert: '管理者権限が必要です'
    end
  end
end

複数プロバイダーサポート

# ユーザーが複数の認証プロバイダーを使用できる場合
class UserAuth < ApplicationRecord
  belongs_to :user
  validates :provider, presence: true
  validates :uid, presence: true
  validates :user_id, uniqueness: { scope: [:provider, :uid] }
end

class User < ApplicationRecord
  has_many :user_auths, dependent: :destroy
  
  def self.from_omniauth(auth_data)
    # 既存の認証があるかチェック
    auth = UserAuth.find_by(provider: auth_data.provider, uid: auth_data.uid)
    
    if auth
      auth.user
    else
      # メールアドレスで既存ユーザーを検索
      user = User.find_by(email: auth_data.info.email)
      
      if user
        # 既存ユーザーに新しい認証を追加
        user.user_auths.create!(
          provider: auth_data.provider,
          uid: auth_data.uid
        )
      else
        # 新しいユーザーを作成
        user = User.create!(
          email: auth_data.info.email,
          name: auth_data.info.name,
          avatar_url: auth_data.info.image
        )
        
        user.user_auths.create!(
          provider: auth_data.provider,
          uid: auth_data.uid
        )
      end
      
      user
    end
  end
end

カスタム戦略の作成

# lib/omniauth/strategies/custom_provider.rb
module OmniAuth
  module Strategies
    class CustomProvider < OmniAuth::Strategies::OAuth2
      option :name, 'custom_provider'
      
      option :client_options, {
        site: 'https://api.customprovider.com',
        authorize_url: '/oauth/authorize',
        token_url: '/oauth/token'
      }

      uid { raw_info['id'] }

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

      extra do
        {
          raw_info: raw_info
        }
      end

      private

      def raw_info
        @raw_info ||= access_token.get('/me').parsed
      end
    end
  end
end

# config/initializers/omniauth.rb で使用
Rails.application.config.middleware.use OmniAuth::Builder do
  provider :custom_provider, ENV['CUSTOM_CLIENT_ID'], ENV['CUSTOM_CLIENT_SECRET']
end

エラーハンドリング

# config/initializers/omniauth.rb
OmniAuth.config.on_failure = Proc.new do |env|
  # 失敗時のカスタム処理
  SessionsController.action(:failure).call(env)
end

# app/controllers/sessions_controller.rb
class SessionsController < ApplicationController
  def failure
    error = params[:message] || '認証に失敗しました'
    redirect_to login_path, alert: "ログインエラー: #{error}"
  end
end

# config/routes.rb
get '/auth/failure', to: 'sessions#failure'

Sinatra での使用例

# app.rb
require 'sinatra'
require 'omniauth'
require 'omniauth-google-oauth2'

class MyApp < Sinatra::Base
  use Rack::Session::Cookie, secret: 'your-secret-key'
  
  use OmniAuth::Builder do
    provider :google_oauth2, ENV['GOOGLE_CLIENT_ID'], ENV['GOOGLE_CLIENT_SECRET']
  end

  get '/' do
    if session[:user]
      "Hello, #{session[:user]['name']}!"
    else
      '<a href="/auth/google_oauth2">Sign in with Google</a>'
    end
  end

  get '/auth/:provider/callback' do
    auth = request.env['omniauth.auth']
    session[:user] = {
      'name' => auth.info.name,
      'email' => auth.info.email
    }
    redirect '/'
  end

  get '/logout' do
    session[:user] = nil
    redirect '/'
  end
end