DEV Community

David Paluy
David Paluy

Posted on

Implement a Secure, Dynamic Domain Approval System for Embeddable Widgets in Ruby on Rails

In the previous post I explained how to implement Embed JS Widgets with Ruby on Rails.

But you have to define the approved domain explicitly:

Rails.application.config.action_dispatch.default_headers.merge!({ 'Content-Security-Policy' => "frame-ancestors 'self' https://trusted-domain.com" }) 
Enter fullscreen mode Exit fullscreen mode

A common, naive approach is to set frame-ancestors to allow embedding on any domain using a wildcard (*). While this enables maximum flexibility, it also opens the widget to misuse and unauthorized access.

# app/controllers/widgets_controller.rb class WidgetsController < ApplicationController def show response.headers['Content-Type'] = 'application/javascript' response.headers['Content-Security-Policy'] = "frame-ancestors *" render layout: false end end 
Enter fullscreen mode Exit fullscreen mode

While this approach is convenient, it lacks domain restriction, meaning any website can use the widget, which could lead to potential misuse. For a secure, client-specific widget, let’s set up a dynamic domain approval system.

Secure Approach: Dynamic Domain Approval

To ensure that only authorized clients can embed the widget, we’ll implement a system where each Account in the application has approved domains. When the widget is served, it dynamically checks the account’s approved domains and sets a restrictive frame-ancestors header accordingly.

Step 1: Set Up the Account Model with Approved Domains

Update your Account model to include a domain attribute representing the domain where the client’s widget can be embedded.

Example: Account.create!(name: "Example Client 1", domain: "example1.com")

Step 2: Set Up a Secure Widget Endpoint

To serve the widget securely, we’ll modify the WidgetsController to check the Account model for the requesting client’s approved domain and set the frame-ancestors directive based on that domain.

Define the Widget Controller Action

In the WidgetsController, add logic to look up the client account based on a unique identifier, such as an API key. This API key can be passed securely as part of the script request to identify the client.

# app/controllers/widgets_controller.rb class WidgetsController < ApplicationController before_action :set_content_security_policy def show response.headers['Content-Type'] = 'application/javascript' render layout: false end private def set_content_security_policy # Look up the account based on a unique identifier, such as an API key account = Account.find_by(api_key: params[:api_key]) if account && account.domain.present? # Restrict embedding to the approved domain response.headers["Content-Security-Policy"] = "frame-ancestors #{account.domain}" else # Deny embedding if no approved domain is found response.headers["Content-Security-Policy"] = "frame-ancestors 'none'" head :forbidden # Block access if the account or domain is not valid end end end 
Enter fullscreen mode Exit fullscreen mode
  • The set_content_security_policy method checks for a matching Account based on an api_key parameter.
  • If the account has a valid approved domain, it sets the frame-ancestors directive to allow embedding only on that domain. If no approved domain is found, it sets frame-ancestors to 'none', denying access and returning a 403 Forbidden status.

Generate API Keys for Accounts

Each Account should have a unique API key to secure access. Generate these keys and store them in the Account model.

# db/migrate/xxxxxx_add_api_key_to_accounts.rb class AddApiKeyToAccounts < ActiveRecord::Migration[7.0] def change add_column :accounts, :api_key, :string add_index :accounts, :api_key, unique: true end end 
Enter fullscreen mode Exit fullscreen mode
# app/models/account.rb class Account < ApplicationRecord before_create :generate_api_key private def generate_api_key self.api_key = SecureRandom.hex(20) # Generates a 40-character API key end end 
Enter fullscreen mode Exit fullscreen mode

Use this API key in the widget request URL to identify the account.

Step 3: Update the Embed Code for Client Sites

Provide each client with an embed code that includes their unique API key. This ensures only authorized clients can load the widget on their approved domain.

Client-Specific Embed Code

The API key is embedded in the script URL to securely identify the client account:

<!-- Embed Code for Client Site --> <script type="text/javascript"> (function() { const script = document.createElement('script'); script.src = "https://yourapp.com/widget.js?api_key=CLIENT_API_KEY"; script.async = true; document.head.appendChild(script); })(); </script> 
Enter fullscreen mode Exit fullscreen mode

Replace CLIENT_API_KEY with the actual API key for each client. This key allows Rails to dynamically set the frame-ancestors header according to the client’s approved domain.

Step 4: Test and Monitor Access

Test the widget on both approved and unapproved domains to ensure this solution works as expected.

 1. Approved Domain Test: Embed the widget code on a page hosted on the approved domain (e.g., example1.com). Confirm the widget loads and displays properly.

 2. Unapproved Domain Test: Try embedding the widget on an unapproved domain. Confirm that the widget does not load and the network request returns a 403 Forbidden status.

Happy Hacking!

Top comments (0)