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" })
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
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
- The
set_content_security_policy
method checks for a matchingAccount
based on anapi_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 setsframe-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
# 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
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>
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)