Doorkeeper

OAuthAuthentication LibraryRubyRailsSecurityAPI AuthenticationAccess Token

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