---
title: "Secure Ruby on Rails RAG Applications with Auth0 FGA"
description: "Learn how to secure your Ruby on Rails RAG application using Auth0 FGA to prevent data leakage and implement fine-grained, identity-aware document retrieval."
authors:
  - name: "Carla Urrea Stabile"
    url: "https://auth0.com/blog/authors/carla-stabile/"
date: "Apr 9, 2026"
category: "Developers"
tags: ["ruby-on-rails", "fga", "rag", "auth0-fga", "fga "]
url: "https://auth0.com/blog/secure-ruby-on-rails-application-auth0-fga/"
---

# Secure Ruby on Rails RAG Applications with Auth0 FGA

<style>
    
  /* Increases spacing between bullet points */   
    li {padding-bottom: .7em; }

  /* Hides Disqus module */
  #disqus_thread {display: none;}

</style>
As the development of AI applications moves forward, there are new attack threats. In RAG applications, vector and embedding vulnerabilities are at the top of security risks. Securing every stage, from generating and storing to retrieving vectors and embeddings, is crucial. In this blog post, we’ll focus on securing a Ruby on Rails RAG application with Auth0 and mitigating the risk of data leakage by ensuring only the right information is retrieved for the right user using fine grained authorization with Auth0 FGA. 

## Understanding the RAG Application

Before we dive into the authentication and authorization, let's take a look at the code sample. The app is a Work Companion chat that uses internal documentation for the employees of a made-up company. Users have access to different documents depending on their permissions, and the app can only use those documents to answer questions they might have. 

Think about a standard company knowledge base. You’ve got sensitive docs from HR, Engineering, and Legal all living in the same vector database.

In a risky RAG setup, if an Engineer asks about "salary bands," the vector search might accidentally grab a snippet from a private HR file and feed it right to the LLM. Not great for privacy.

What we want to achieve is what the diagram below shows, we want to make the retrieval step identity-aware. Before we even touch the vector database, the user must authenticate with Auth0, otherwise the app won’t even load for them. Once they are authenticated, the app asks Auth0 FGA: *"Which documents is this specific user allowed to see?"* We then use those allowed IDs as a filter for our search. 

<picture>
<img src="https://images.ctfassets.net/23aumh6u8s0i/3I6ycFx52NhzXtX7dGXh1y/d94770173a81d32bc9bee29518db004c/WorkCompanionAppFlow.png" alt="Diagram of an identity-aware RAG pipeline using Ruby, Auth0 authentication, and FGA authorization." style="width:100%; margin: 1em auto; border: solid black 0px; border-radius: 0px;">
</picture>
*Diagram explaining the Work Companion app flow using Auth0 and Auth0 FGA for authentication and authorization.* 

### What is RAG in this context?

If you're new to the term, Retrieval-Augmented Generation( RAG) is basically a way to give an LLM "specialized knowledge" without retraining it by adding more context. In our Work Companion:

* **Retrieval:** When a user asks a question, the app searches on a vector database to find the most relevant "chunks" of text from our internal docs.  
* **Augmentation:** We take those text chunks and "augment" the user's original question by adding them as extra context.  
* **Generation**: We send that combined package to the LLM, which generates a precise answer based only on the provided context.

## The Anatomy of the Work Companion Chat App

For this code sample you’ll need the following tools: 

### Pre-requisites 

* Ruby 4.0.1  
* PostgreSQL 17 with pgvector extension  
* An OpenAI API key

The app is built to keep data ingestion, retrieval, and authorization separate and clean. Here’s the breakdown:

* **The Database:** We have a simple schema with `users`, `documents`, and `document_chunks`. Users are stored in the database to showcase how to handle this in a Ruby on Rails application. The `document_chunks` table is the star of the show, it stores the `vector[1536]` embeddings and uses an [HNSW index](https://en.wikipedia.org/wiki/Hierarchical_navigable_small_world) to make sure our searches are fast.  
* **The Services:** We use a `RagQueryService` to coordinate the RAG flow and an `FgaService` to talk to the Auth0 FGA API.  
* **The Gems:**  
  * [`neighbor`](https://github.com/ankane/neighbor): Our go-to for handling `pgvector` inside Rails’ ActiveRecord.  
  * [`ruby-openai`](https://github.com/alexrudall/ruby-openai): To connect with OpenAI and turn user questions into embeddings.  
  * [`openfga`](https://github.com/carlastabile/openfga-ruby-sdk): The Ruby SDK for OpenFGA that connects to Auth0 FGA via client credentials exchange.

Now that you know how the application works, let’s download and install it. 

### Running the code sample

The first thing you need to do is clone and install dependencies

```shell
git clone https://github.com/auth0-blog/ruby-rag-fga
cd ruby-rag-fga
bundle install
```

Then you’ll need to set up PostgreSQL with `pgvector` and create the database and seed it with data. I recommend you follow the steps from the repo’s [README](https://github.com/auth0-blog/ruby-rag-fga?tab=readme-ov-file#installation). You can run the server with `rails s` and navigate to [http://localhost:3000](http://localhost:3000) to see the chat view. 

<picture>
<img src="https://images.ctfassets.net/23aumh6u8s0i/6vXRQvk4vBrvzMEh1UuYmp/eeeddd84c853578a1cac135c55a100c3/WorkCompanionAppChatView.png" alt="Chat view for the Work Companion app." style="width:85%; margin: 1em auto; border: solid black 0px; border-radius: 0px;">
</picture>
*Chat view for the Work Companion app.* 

At this point the app doesn’t know who the user is so let’s proceed to add authentication with Auth0. 

## Add Authentication with Auth0 

Before we can ask Auth0 FGA what a user is allowed to see, we first need to know exactly who the user is. In our Rails application, we use Auth0 to handle identity management.

*Read the [Rails Authentication by Example Guide](https://developer.auth0.com/resources/guides/web-app/rails/basic-authentication#set-up-the-ruby-on-rails-auth-0-sdk) to get an in depth explanation of all the details for implementing Auth0 in a Ruby on Rails application*

The first step is to Create an Auth0 Application: In your [Auth0 dashboard](https://manage.auth0.com/), create a new "Regular Web Application" and take note of the Domain and Client ID and Client Secret.  

### Install and set up the Auth0 SDK

Now you need to install the Auth0 SDK that lives in the [`omniauth-auth0`](https://github.com/auth0/omniauth-auth0) gem. Add the following to your Gemfile: 

```ruby
source "https://rubygems.org"

# ....

# New code 👇
# Auth0 Authentication
gem "omniauth-auth0", "~> 3.1"
gem "omniauth-rails_csrf_protection", "~> 1.0"

#...
```

Make sure to install the new gems by running `bundle install` in your terminal. Next, let’s add the initializer file for Auth0. Create a new file `config/initializer/auth0.rb` with the following content:

```ruby
Rails.application.config.middleware.use OmniAuth::Builder do
  provider(
    :auth0,
    ENV.fetch("AUTH0_CLIENT_ID"),
    ENV.fetch("AUTH0_CLIENT_SECRET"),
    ENV.fetch("AUTH0_DOMAIN"),
    callback_path: "/auth/auth0/callback",
    authorize_params: {
      scope: "openid profile email"
    }
  )
end

OmniAuth.config.on_failure = Proc.new do |env|
  OmniAuth::FailureEndpoint.new(env).redirect_to_failure
end
```

Next you’ll need to handle the authentication flow with a rails controller. When a user successfully logs in through Auth0, they are redirected back to our application's callback action. Let’s create a new controller `app/controllers/auth0_controller.rb` which will handle the callback with the following content: 

```ruby
class Auth0Controller < ActionController::Base
  protect_from_forgery with: :exception
  skip_forgery_protection only: [:callback]

  def callback
    auth_info = request.env["omniauth.auth"]
    user = User.create_or_update_from_auth0(auth_info)
    session[:user_id] = user.id
    redirect_to "/chat"
  end

  def failure
    Rails.logger.error "Auth0 login failed: #{params[:message]}"
    render plain: "Authentication failed. Please try again.", status: :unauthorized
  end

  def logout
    session.delete(:user_id)
    reset_session

    logout_url = URI::HTTPS.build(
      host: ENV.fetch("AUTH0_DOMAIN"),
      path: "/v2/logout",
      query: {
        returnTo: root_url,
        client_id: ENV.fetch("AUTH0_CLIENT_ID")
      }.to_query
    ).to_s

    redirect_to logout_url, allow_other_host: true
  end
end
```

Note the method `create_or_update_from_auth0` is defined in the `User` model and it’s not a given method from the gem. 

### Securing our controllers

To make sure only logged-in users can access the Work Companion, let’s implement a Rails concern `Secured`. This is a simple module we include in any controller that needs protection.

```ruby
module Secured
  extend ActiveSupport::Concern

  included do
    before_action :logged_in_using_omniauth?
    helper_method :current_user
  end

  def current_user
    @current_user ||= User.find_by(id: session[:user_id]) if session[:user_id]
  end

  private

  def logged_in_using_omniauth?
    return if current_user

    redirect_post('/auth/auth0', params: { prompt: 'login' },
                                 options: { method: :post, authenticity_token: 'auto' })
  end
end
```

The logic is as follows: a `before_action` checks for the existence of a user ID in the session. If the session is empty, we need to send the user to the login page. Here, we are using the `repost` gem to allow us to perform a clean redirect-to-POST, ensuring the user hits the Auth0 login flow securely. 

Next, add the new Auth0 routes in your ``config/routes.rb`` file:

```ruby
# .... 
scope :auth do
  get 'failure' => 'auth0#failure'
  get 'auth0/callback' => 'auth0#callback'
  get 'logout' => 'auth0#logout', as: :logout
end
```

Now we need to use the `Secured` concern where we need it: in the chat view and the chat API. Go to `app/controllers/chat_controller.rb` and add the following:

```ruby
class ChatController < ActionController::Base
  # New code 👇
  include Secured 

  def index
    # current_user is available here
  end
end
```

And the `app/controllers/api/chat_controller.rb`:

```ruby
class Api::ChatController < ActionController::Base
  # New code 👇
  include Secured

  # Override Secured's redirect behavior for API — return 401 JSON instead
  def logged_in_using_omniauth?
    return if current_user

    render json: { error: "Unauthorized" }, status: :unauthorized
  end
#...
```

We are keeping the HTML controller separate from our API controller to maintain their single responsibilities and to make it clearer for this code sample. I’m also skipping the changes in the view. You should adjust it to your own implementation. 

If you now navigate to [http://localhost:3000](http://localhost:3000), you’ll be redirected to login with Auth0, and once you do, you should see your name and email in the chat view as the app knows who you are. 
  
<picture>
<img src="https://images.ctfassets.net/23aumh6u8s0i/7ACzDxmeBNRvbGoVaJ2naG/64d515488f22e994c7958cac3a8b2b31/WorkCompanionAppChatViewUserAuthenticated.png" alt="Ruby on Rails application showing successful user login via Auth0." style="width:85%; margin: 1em auto; border: solid black 0px; border-radius: 0px;">
</picture>
*The chat view from the Work Companion with the user authenticated.* 

Now let’s proceed to add authorization with Auth0 FGA. 

### Defining permissions with Auth0 FGA

With user identity established through Auth0, the focus shifts from *who* the user is to *what* they are permitted to see. This is where Auth0 FGA (Fine-Grained Authorization) becomes essential for out Ruby RAG architecture. It provides a centralized, externalized authorization layer that prevents your Rails controllers from becoming cluttered with complex, hard-to-maintain permission logic.

### Why use Auth0 FGA?

Traditional Role-Based Access Control (RBAC) is often the first tool developers reach for, but it quickly breaks down in document-heavy RAG scenarios. While you could technically create roles like `rails_guide_reader` or `postgres_guide_reader`, this "role explosion" becomes unmanageable as your document library grows from dozens to thousands.

Auth0 FGA leverages Relationship-Based Access Control (ReBAC), which is a far more natural fit for RAG applications. Instead of checking broad roles, you define direct relationships between users and specific objects, for example, "Carla is a **viewer** of **document:rails_guide**." This approach is significantly more granular, scalable, and intuitive for managing document-level security.

*To learn more about Auth0 FGA and how to use it in your applications, read the [Auth0 FGA documentation](https://docs.fga.dev/).*

### The authorization model

You can write your authorization model using the [rake task](https://github.com/auth0-blog/ruby-rag-fga/blob/main/lib/tasks/fga.rake) provided in the repo or by going to your Auth0 FGA Dashboard. The authorization model looks like this:

```shell
model
  schema 1.1

type user

type document
  relations
    define owner: [user]
    define viewer: [user] or owner
```

This model represents the following:

* Users can be owners or viewers of documents  
* Anyone who is an owner is automatically also a viewer   
* You can also grant viewer access directly without ownership

### Creating relationship tuples 

Relationship tuples are the facts of your authorization model. They define what user has a relationship with what document. Because we already created five documents earlier and we know their IDs in our database, we’ll use that to identify them on Auth0 FGA, similarly we’ll use the user’s Auth0 ID to identify them. Let’s create the following tuples: 

```shell
user:google-oauth2|100...5110    viewer    document    3
user:google-oauth2|100...5110    viewer    document    2
```

Now that we have a model and tuples, we’ll need to create an authorized client on Auth0 FGA so our application can securely talk to the API. 

Go to the Auth0 FGA Dashboard and then to Store *Settings*. Scroll down to *Authorized Clients* and click on *Create Client*. Select the client permissions we need, which in our case are all except for Search Logs, and click on *Create*. Take note of all the ENV variables and add them to your .env file. 

## Implementing FGA in the Ruby RAG Pipeline

We are almost done with our application. We’ve added authentication but now we need to add authorization. The end to end flow of our application should end up as follows:

1. User visits /chat → redirected to Auth0 Universal Login  
2. After authentication, user is created/updated in the database  
3. User sends a message in the chat interface  
4. FGA list_objects returns the document IDs the user can view  
5. Query is embedded via OpenAI  
6. pgvector nearest-neighbor search runs filtering authorized documents only  
7. Top-k chunks are assembled into context and sent to the LLM  
8. Response is returned with sources and processing time

We already took care of steps 1-3 but now we need to filter the documents the user has access to before we create the embeddings and send the question to OpenAI.

### Install and set up the OpenFGA Ruby SDK 

For this example we’ll use a community built ruby SDK: [openfga](https://github.com/carlastabile/openfga-ruby-sdk). Add it to your Gemfile and then run bundle install to install it locally:

```ruby
source "https://rubygems.org"

# ....

# New code 👇
gem "openfga", "~> 0.1.5"

#...
```

### Retrieve the user’s ID 

After the user signs in, we store it and use it as the current user in the session. Let’s pass the user’s ID to the RAG Pipeline by adding the following to the Api::ChatController:

```ruby
class Api::ChatController < ActionController::Base
  include Secured

  #  ...

  def create
    message = params[:message]
    
    if message.blank?
      return render json: { error: "Message is required" }, status: :bad_request
    end

    begin
      # New code 👇 (pass user: current_user)

      response = RagQueryService.call(message, user: current_user)
      
      render json: {
        message: message,
        response: response[:answer],
        sources: response[:sources],
        processing_time: response[:processing_time]
      }
    rescue StandardError => e
      Rails.logger.error "Chat error: #{e.message}"
      render json: { 
        error: "Sorry, I encountered an error processing your request." 
      }, status: :internal_server_error
    end
  end
end
```

Now that we have the user’s information we can build the authorization service.

### Build the authorization service

Next, let’s create an FgaService. Create a new file app/services/fga_service.rb and add the following code:

```ruby
require "openfga"

class FgaService
  class << self
    def client
      @client ||= OpenFga::SdkClient.new(
        api_url: ENV.fetch("FGA_API_URL"),
        store_id: ENV.fetch("FGA_STORE_ID"),
        authorization_model_id: ENV.fetch("FGA_MODEL_ID", nil),
        credentials: {
          method: :client_credentials,
          api_token_issuer: ENV.fetch("FGA_API_TOKEN_ISSUER"),
          api_audience: ENV.fetch("FGA_API_AUDIENCE"),
          client_id: ENV.fetch("FGA_CLIENT_ID"),
          client_secret: ENV.fetch("FGA_CLIENT_SECRET")
        }
      )
    end
  end
end
```

We are creating a new instance of the SdkClient and connect it to our Auth0 FGA instance using [Client Credentials Exchange.](https://auth0.com/docs/get-started/authentication-and-authorization-flow/client-credentials-flow) 

Next, let’s add a method to see the list of accessible documents by the current user. In your FgaService add the following:

```ruby
def list_accessible_documents(user)
  response = client.list_objects(
    user: "user:#{user.auth0_sub}",
    relation: "viewer",
    type: "document"
  )

  response.objects.map { |obj| obj.delete_prefix("document:").to_i }
rescue => e
  Rails.logger.error "FGA ListObjects error: #{e.message}"
  raise e
end
```

In the list_accessible_documents method, we are using the [list_objects](https://openfga.dev/docs/concepts#what-is-a-list-objects-request) method of the SDK to list all the objects that the user is related to via the viewer relation. This is our main document filtering mechanism for our RAG pipeline. 

### Filter the documents

Time has come to finally filter documents. Let’s go to the RagQueryService class and add the following code in the call method:

```ruby
 def call
    start_time = Time.current
    
    # New code 👇
    # Step 1: Get authorized document IDs from FGA
    authorized_doc_ids = FgaService.list_accessible_documents(user)
    
    if authorized_doc_ids.empty?
      return {
        answer: "You don't have access to any documents. Please contact your administrator.",
        sources: [],
        context_chunks_count: 0,
        processing_time: (Time.current - start_time).round(2)
      }
    end
    
    # Step 2: Generate embedding for the query
    query_embedding = OpenaiService.generate_embedding(query)
    
    # New code👇 (user authorized_doc_ids) 
    # Step 3: Find similar document chunks (filtered by authorized documents)
    similar_chunks = find_similar_chunks(query_embedding, authorized_doc_ids)
    
    # Step 4: Build context from chunks
    context = build_context(similar_chunks)
    
    # Step 5: Generate response using OpenAI
    answer = OpenaiService.generate_completion(query, context)
    
    processing_time = Time.current - start_time
    
    {
      answer: answer,
      sources: format_sources(similar_chunks),
      context_chunks_count: similar_chunks.length,
      processing_time: processing_time.round(2)
    }
  end
```

### Test the work companion 

With the tuples we created in Auth0 FGA, the user has access to two documents only: a Auth0 Guide and a Postgres Guide, so let’s try to ask questions NOT about those documents to see if the filtering is working: 

<picture>
<img src="https://images.ctfassets.net/23aumh6u8s0i/50zR2w2GGjdYYB0jRJQUcJ/6e5cee8fba0d9c38d49ffeb73c821f6a/WorkCompanionAppChatViewWhatAreViewsInRails.gif" alt="User asking questions about topics it does not have access to" style="width:85%; margin: 1em auto; border: solid black 0px; border-radius: 0px;">
</picture>

If we now try to ask about a topic we know we have access, we’ll get the following response: 

<picture>
<img src="https://images.ctfassets.net/23aumh6u8s0i/3ZeVQdMWFVyzV1o8B5blc9/60673acd8dd2c46b736c945f7ceb862b/WorkCompanionAppChatViewWhatisUniversalLogin.gif" alt="User asking questions about topics it does have access to" style="width:85%; margin: 1em auto; border: solid black 0px; border-radius: 0px;">
</picture>

## Securing Ruby on Rails RAG Apps

Building a RAG application is a major step toward making AI more useful for your organization, but it shouldn't come at the cost of data security. As we’ve seen, traditional RBAC often hits a wall when you need to manage permissions at the individual document level across complex hierarchies.

By integrating Auth0 FGA with your Ruby on Rails application, you’ve moved from a coarse approach to a precise, relationship-based authorization model. You’ve successfully:

* **Identified the User:** Leveraged Auth0 and OmniAuth to establish a secure identity.  
* **Centralized Permissions:** Used a ReBAC model with Auth0 FGA to define exactly who can see what.  
* **Secured the Pipeline:** Implemented an "identity-aware" retrieval step that filters sensitive context before it ever reaches the LLM.

This architecture doesn’t just stop at document ownership. Because Auth0 FGA is built on top of OpenFGA, it allows you to handle millions of objects and complex sharing rules, like department-wide access or temporary viewer permissions, without ever having to rewrite your core search logic.

If you’re ready to start securing your own RAG pipelines, check out the [Authorization for RAG Quickstart](https://auth0.com/ai/docs/get-started/authorization-for-rag) and if you’re using [Ruby check out the OpenFGA SDK](https://github.com/carlastabile/openfga-ruby-sdk)

<FAQs>
  <FAQ>
    <FAQQuestion>What problem does FGA solve for Rails RAG applications?</FAQQuestion>
    <FAQAnswer>FGA ensures RAG systems respect document permissions. When your Rails app
      retrieves documents for AI context, FGA checks whether the current user
      can access each document before including it. This prevents AI responses
      from leaking information from documents the requesting user shouldn't
      access, maintaining confidentiality and compliance.</FAQAnswer>
  </FAQ>
  <FAQ>
    <FAQQuestion>Why use FGA instead of RBAC for RAG authorization?</FAQQuestion>
    <FAQAnswer>RBAC assigns permissions by role (all "editors" can see all documents),
      but document access often depends on relationships—ownership, sharing,
      team membership. FGA handles these: "user can access documents they own,"
      "documents shared with their team," "documents they were granted access to
      by owner." These rules can't be expressed in RBAC without creating a role
      per relationship combination—role explosion.</FAQAnswer>
  </FAQ>
  <FAQ>
    <FAQQuestion>What tools are required for FGA with Rails?</FAQQuestion>
    <FAQAnswer>You need the OpenFGA Ruby SDK, an Auth0 FGA store with your
      authorization model defined, and integration points in your Rails
      application where you check permissions before retrieving documents. For
      RAG specifically, you'll also need your document storage system, embedding
      service (OpenAI, Hugging Face, etc.), and vector database configured
      alongside FGA checks.</FAQAnswer>
  </FAQ>
</FAQs>