Doorkeeper

OAuth認証ライブラリRubyRailsセキュリティAPI認証アクセストークン

Doorkeeper

概要

DoorkeeperはRuby on Rails/Grape向けのOAuth 2.0プロバイダーライブラリです。標準準拠のOAuth 2.0認証サーバーを構築でき、アクセストークン管理、スコープ制御、リフレッシュトークンなどの機能を提供します。大規模なAPIエコシステムや複数のクライアントアプリケーションに認証サービスを提供する際に威力を発揮し、Facebook、Twitterのようなソーシャルメディアプラットフォームでも採用される信頼性の高いソリューションです。

詳細

Doorkeeperは本格的なOAuth 2.0認証基盤の構築を可能にします:

  • OAuth 2.0準拠: RFC 6749に完全準拠したOAuth 2.0実装
  • 柔軟な認証フロー: 複数のグラントタイプをサポート(Authorization Code、Client Credentials、Resource Owner Password等)
  • 高度なトークン管理: アクセストークン、リフレッシュトークンの完全制御
  • スコープシステム: 細かなアクセス権限制御
  • カスタマイズ性: アプリケーション登録、承認画面の完全カスタマイズ
  • セキュリティ機能: PKCE、Token Introspection、セッション保護
  • API統合: Rails APIとシームレスな統合

メリット・デメリット

メリット

  • OAuth 2.0標準への完全準拠による相互運用性
  • 豊富なカスタマイズオプションと拡張性
  • 大規模システムでの実績と安定性
  • 詳細なドキュメントと活発なコミュニティ
  • マルチテナント対応と高いスケーラビリティ
  • セキュリティベストプラクティスの実装

デメリット

  • OAuth 2.0の複雑性による学習コストの高さ
  • 初期設定とカスタマイズに時間を要する
  • Rails/Ruby以外の環境では使用不可
  • 小規模プロジェクトには過剰な機能セット
  • データベース設計への影響

参考ページ

書き方の例

基本セットアップ

# Gemfile
gem 'doorkeeper'

# インストール
bundle install

# マイグレーション生成
rails generate doorkeeper:install
rails generate doorkeeper:migration
rails db:migrate

アプリケーション登録

# config/initializers/doorkeeper.rb
Doorkeeper.configure do
  # リソースオーナー認証
  resource_owner_authenticator do
    current_user || warden.authenticate!(scope: :user)
  end

  # アプリケーション作成
  admin_authenticator do
    current_user&.admin? || redirect_to(new_user_session_url)
  end

  # 利用可能なスコープ
  default_scopes :public
  optional_scopes :write, :update, :admin

  # トークン有効期限
  access_token_expires_in 2.hours
  refresh_token_enabled true
end

API保護の実装

class Api::V1::BaseController < ApplicationController
  before_action :doorkeeper_authorize!

  private

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

  def current_user
    @current_user ||= current_resource_owner
  end
end

class Api::V1::UsersController < Api::V1::BaseController
  before_action :doorkeeper_authorize!, scopes: [:read]

  def show
    render json: current_user
  end

  def update
    doorkeeper_authorize! :write
    # 更新処理
  end
end

クライアント認証フロー

# Authorization Code Grant
# 1. 認証URL生成
application = Doorkeeper::Application.find_by(uid: 'your-app-uid')
auth_url = "#{request.base_url}/oauth/authorize?client_id=#{application.uid}&redirect_uri=#{CGI.escape(redirect_uri)}&response_type=code&scope=read write"

# 2. コールバック処理
class OauthCallbacksController < ApplicationController
  def create
    response = HTTParty.post("#{request.base_url}/oauth/token", {
      body: {
        grant_type: 'authorization_code',
        client_id: params[:client_id],
        client_secret: application.secret,
        code: params[:code],
        redirect_uri: redirect_uri
      }
    })
    
    token = JSON.parse(response.body)['access_token']
    # トークンを保存して使用
  end
end

スコープベースのアクセス制御

# コントローラーでスコープ制御
class Api::V1::PostsController < Api::V1::BaseController
  before_action -> { doorkeeper_authorize! :read }, only: [:index, :show]
  before_action -> { doorkeeper_authorize! :write }, only: [:create, :update]
  before_action -> { doorkeeper_authorize! :admin }, only: [:destroy]

  def index
    @posts = current_user.posts.accessible_by_scope(doorkeeper_token.scopes)
    render json: @posts
  end
end

# モデルでスコープ処理
class Post < ApplicationRecord
  scope :accessible_by_scope, ->(scopes) {
    return all if scopes.include?('admin')
    return where(published: true) if scopes.include?('read')
    none
  }
end

Token Introspection実装

# config/initializers/doorkeeper.rb
Doorkeeper.configure do
  # Token Introspection有効化
  allow_blank_redirect_uri true
  
  # カスタムレスポンス
  custom_access_token_expires_in do |context|
    context.client.additional_settings[:token_lifetime] || 2.hours
  end
end

# Introspectionコントローラー
class Api::V1::TokenIntrospectionController < ApplicationController
  def introspect
    token = Doorkeeper::AccessToken.by_token(params[:token])
    
    if token&.acceptable?(doorkeeper_token.scopes)
      render json: {
        active: !token.expired?,
        scope: token.scopes.to_s,
        client_id: token.application.uid,
        username: token.resource_owner_id,
        exp: token.expires_in_seconds
      }
    else
      render json: { active: false }
    end
  end
end

エラーハンドリングとセキュリティ

class ApplicationController < ActionController::Base
  rescue_from Doorkeeper::Errors::DoorkeeperError, with: :handle_doorkeeper_error
  rescue_from Doorkeeper::Errors::TokenExpired, with: :handle_token_expired
  rescue_from Doorkeeper::Errors::TokenRevoked, with: :handle_token_revoked

  private

  def handle_doorkeeper_error(exception)
    render json: {
      error: exception.type,
      error_description: exception.description
    }, status: :unauthorized
  end

  def handle_token_expired(exception)
    render json: {
      error: 'token_expired',
      error_description: 'The access token expired'
    }, status: :unauthorized
  end

  # セキュリティヘッダー設定
  before_action :set_security_headers

  def set_security_headers
    response.headers['X-Content-Type-Options'] = 'nosniff'
    response.headers['X-Frame-Options'] = 'DENY'
    response.headers['X-XSS-Protection'] = '1; mode=block'
  end

  # レート制限(Redis使用)
  before_action :rate_limit_check

  def rate_limit_check
    key = "rate_limit:#{doorkeeper_token&.resource_owner_id || request.ip}"
    current_requests = Rails.cache.increment(key, 1, expires_in: 1.hour) || 1
    
    if current_requests > 1000
      render json: { error: 'rate_limit_exceeded' }, status: :too_many_requests
    end
  end
end