Doorkeeper
Doorkeeper
Overview
Doorkeeper is an OAuth 2.0 provider library for Ruby on Rails/Grape. It enables building standards-compliant OAuth 2.0 authentication servers and provides features such as access token management, scope control, and refresh tokens. It demonstrates its power when providing authentication services to large API ecosystems and multiple client applications, and is a reliable solution trusted by social media platforms like Facebook and Twitter.
Details
Doorkeeper enables building full-scale OAuth 2.0 authentication infrastructure:
- OAuth 2.0 Compliance: Complete OAuth 2.0 implementation fully compliant with RFC 6749
- Flexible Authentication Flows: Support for multiple grant types (Authorization Code, Client Credentials, Resource Owner Password, etc.)
- Advanced Token Management: Complete control over access tokens and refresh tokens
- Scope System: Fine-grained access permission control
- Customizability: Complete customization of application registration and authorization screens
- Security Features: PKCE, Token Introspection, session protection
- API Integration: Seamless integration with Rails API
Pros and Cons
Pros
- Interoperability through complete compliance with OAuth 2.0 standards
- Rich customization options and extensibility
- Proven track record and stability in large-scale systems
- Detailed documentation and active community
- Multi-tenant support and high scalability
- Implementation of security best practices
Cons
- High learning cost due to OAuth 2.0 complexity
- Time-consuming initial setup and customization
- Cannot be used outside Rails/Ruby environments
- Excessive feature set for small projects
- Impact on database design
Reference Pages
Code Examples
Basic Setup
# Gemfile
gem 'doorkeeper'
# Installation
bundle install
# Generate migration
rails generate doorkeeper:install
rails generate doorkeeper:migration
rails db:migrate
Application Registration
# config/initializers/doorkeeper.rb
Doorkeeper.configure do
# Resource owner authentication
resource_owner_authenticator do
current_user || warden.authenticate!(scope: :user)
end
# Application creation
admin_authenticator do
current_user&.admin? || redirect_to(new_user_session_url)
end
# Available scopes
default_scopes :public
optional_scopes :write, :update, :admin
# Token expiration
access_token_expires_in 2.hours
refresh_token_enabled true
end
API Protection Implementation
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
# Update processing
end
end
Client Authentication Flow
# Authorization Code Grant
# 1. Generate authentication 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. Callback processing
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']
# Save and use token
end
end
Scope-based Access Control
# Scope control in controller
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
# Scope processing in model
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 Implementation
# config/initializers/doorkeeper.rb
Doorkeeper.configure do
# Enable Token Introspection
allow_blank_redirect_uri true
# Custom response
custom_access_token_expires_in do |context|
context.client.additional_settings[:token_lifetime] || 2.hours
end
end
# Introspection controller
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
Error Handling and Security
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
# Set security headers
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
# Rate limiting (using 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
Advanced Configuration and Monitoring
# config/initializers/doorkeeper.rb
Doorkeeper.configure do
# Custom token generator
access_token_generator '::Doorkeeper::JWT'
# JWT configuration
use_refresh_token
# Grant flows configuration
grant_flows %w[authorization_code client_credentials]
# PKCE (Proof Key for Code Exchange)
force_ssl_in_redirect_uri false
# Custom authentication methods
resource_owner_from_credentials do |routes|
user = User.find_for_database_authentication(email: params[:username])
if user && user.valid_for_authentication? { user.valid_password?(params[:password]) }
user
end
end
# Cleanup configuration
access_token_expires_in 2.hours
authorization_code_expires_in 10.minutes
# Custom scopes with descriptions
default_scopes :public
optional_scopes :read => 'Read access to your data',
:write => 'Write access to your data',
:admin => 'Admin access to manage your account'
end
# Monitoring and analytics
class Api::V1::AnalyticsController < ApplicationController
before_action :doorkeeper_authorize!, scopes: [:admin]
def token_usage
tokens = Doorkeeper::AccessToken.includes(:application)
.where(created_at: 1.month.ago..Time.current)
usage_stats = tokens.group_by_day(:created_at).count
app_usage = tokens.joins(:application).group('oauth_applications.name').count
render json: {
daily_usage: usage_stats,
application_usage: app_usage,
total_active_tokens: Doorkeeper::AccessToken.where(revoked_at: nil).count
}
end
def revoke_tokens
application = Doorkeeper::Application.find(params[:application_id])
revoked_count = application.access_tokens.where(revoked_at: nil).update_all(revoked_at: Time.current)
render json: { revoked_tokens: revoked_count }
end
end
# Custom token response
module CustomTokenResponse
def body
additional_data = {
user_id: token.resource_owner_id,
permissions: token.scopes.map(&:to_s),
issued_at: token.created_at.to_i
}
super.merge(additional_data)
end
end
Doorkeeper::OAuth::TokenResponse.prepend(CustomTokenResponse)
Testing and Development Tools
# spec/support/doorkeeper_helpers.rb
module DoorkeeperHelpers
def create_application(name: 'Test App', redirect_uri: 'urn:ietf:wg:oauth:2.0:oob')
Doorkeeper::Application.create!(
name: name,
redirect_uri: redirect_uri,
scopes: 'public read write'
)
end
def create_access_token(application: nil, resource_owner: nil, scopes: 'public')
application ||= create_application
Doorkeeper::AccessToken.create!(
application: application,
resource_owner_id: resource_owner&.id,
scopes: scopes
)
end
def auth_header(token)
{ 'Authorization' => "Bearer #{token.token}" }
end
end
# spec/requests/api/v1/posts_spec.rb
RSpec.describe 'Posts API', type: :request do
include DoorkeeperHelpers
let(:user) { create(:user) }
let(:application) { create_application }
let(:token) { create_access_token(application: application, resource_owner: user) }
describe 'GET /api/v1/posts' do
context 'with valid token' do
it 'returns posts' do
get '/api/v1/posts', headers: auth_header(token)
expect(response).to have_http_status(:ok)
expect(JSON.parse(response.body)).to be_an(Array)
end
end
context 'without token' do
it 'returns unauthorized' do
get '/api/v1/posts'
expect(response).to have_http_status(:unauthorized)
end
end
context 'with insufficient scope' do
let(:limited_token) { create_access_token(scopes: 'public') }
it 'returns forbidden' do
get '/api/v1/posts', headers: auth_header(limited_token)
expect(response).to have_http_status(:forbidden)
end
end
end
end