Doorkeeper
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