Skip to main content

Overview

Rails GraphQL provides field-level authorization through the authorize method and the AuthorizedField module. Authorization checks run during request execution and can prevent field resolution based on custom logic.
Authorization is separate from authentication. It determines what authenticated users can access.

Basic Authorization

Using authorize

Add authorization to any field:
field(:email, :string).authorize do
  current_user == current_value
end
field(:secret_data).authorize do
  current_user.admin?
end

Method Reference

field(:email, :string).authorize(:check_ownership)

def check_ownership
  current_user.id == current_value.user_id
end

AuthorizedField Module

The authorization system is implemented in the AuthorizedField module:
# From lib/rails/graphql/field/authorized_field.rb
module Field::AuthorizedField
  # Add settings for authorization or a block to execute
  def authorize(*args, **xargs, &block)
    @authorizer = [args, xargs, block]
    self
  end
  
  # Return the settings for the authorize process
  def authorizer
    @authorizer if authorizable?
  end
  
  # Checks if the field should go through authorization
  def authorizable?
    defined?(@authorizer)
  end
end

Checking Authorization

if field.authorizable?
  # Field has authorization logic
end

Authorization Patterns

Role-Based

field(:admin_panel).authorize do
  current_user.role == :admin
end

Ownership-Based

field(:edit_post).authorize do
  current_value.user_id == current_user.id
end

Permission-Based

field(:publish_post).authorize do
  current_user.can?(:publish, Post)
end

Integration with Pundit

class PostPolicy < ApplicationPolicy
  def show?
    user.admin? || record.published?
  end
  
  def update?
    user.id == record.user_id
  end
end

Integration with CanCanCan

class Ability
  include CanCan::Ability
  
  def initialize(user)
    user ||= User.new
    
    if user.admin?
      can :manage, :all
    else
      can :read, Post, published: true
      can :update, Post, user_id: user.id
    end
  end
end

Error Handling

Unauthorized Errors

field(:secret).authorize do
  return false unless current_user.admin?
end

Schema-Level Handler

class AppSchema < GraphQL::Schema
  rescue_from(UnauthorizedError) do |error|
    request.report_error(
      error.message,
      extensions: {
        code: "FORBIDDEN",
        required_role: error.required_role
      }
    )
  end
end

field(:admin_action).authorize do
  raise UnauthorizedError.new("Admin required", required_role: :admin) unless current_user.admin?
end

Proxy Field Authorization

Proxy fields inherit authorization:
# From lib/rails/graphql/field/authorized_field.rb
module Proxied
  def authorizer
    super || field.authorizer
  end
  
  def authorizable?
    super || field.authorizable?
  end
end
If a proxied field doesn’t have its own authorization, it uses the original field’s authorization.

Best Practices

Fail Securely

Default to denying access:
field(:sensitive).authorize do
  return false unless current_user.present?
  current_user.has_permission?(:view_sensitive)
end

Clear Error Messages

Provide helpful error messages:
unless authorized?
  request.report_error(
    "You need #{required_role} role to access this",
    extensions: { code: "FORBIDDEN" }
  )
  return false
end

Avoid N+1 Queries

Preload authorization data:
field(:posts).before_resolve do
  # Preload user abilities
  @abilities = current_user.abilities.index_by(&:subject_type)
end.authorize do
  @abilities['Post']&.can_read?
end

Use Policy Objects

Centralize authorization logic:
field(:post).authorize do |id:|
  PostPolicy.new(current_user, Post.find(id)).show?
end

Testing Authorization

RSpec.describe "Post authorization" do
  let(:admin) { create(:user, :admin) }
  let(:user) { create(:user) }
  
  it "allows admins to view all posts" do
    result = execute(<<~GQL, user: admin)
      { posts { id } }
    GQL
    
    expect(result[:errors]).to be_nil
  end
  
  it "denies regular users" do
    result = execute(<<~GQL, user: user)
      { adminPosts { id } }
    GQL
    
    expect(result[:errors]).to be_present
    expect(result[:errors][0][:extensions][:code]).to eq("FORBIDDEN")
  end
end

Performance Considerations

Batch Authorization

Check authorization in bulk:
field(:posts).before_resolve do
  @post_permissions = current_user
    .permissions
    .where(subject_type: 'Post')
    .index_by(&:subject_id)
end

field(:post).authorize do |id:|
  @post_permissions[id]&.can_read?
end

Cache Permissions

Cache expensive permission checks:
def can?(action, subject)
  cache_key = "permission:#{current_user.id}:#{action}:#{subject}"
  Rails.cache.fetch(cache_key, expires_in: 5.minutes) do
    current_ability.can?(action, subject)
  end
end

Error Handling

Handling authorization errors

Events

Authorization in event callbacks

Build docs developers (and LLMs) love