DEV3 Technologies home

Embed loader with domain allowlist

Serve a widget script only to pages on approved domains, with wildcard subdomain matching and a frame-ancestors header to match.

LaravelMiddlewareembedcorsallowlistcsp

Code

<?php

namespace App\Embed;

use Illuminate\Http\Request;

final class OriginAllowlist
{
    /** The page that embeds us: Origin header, else the Referer's origin. */
    public static function requestOrigin(Request $request): ?string
    {
        $raw = $request->headers->get('Origin');
        if (! $raw || $raw === 'null') {
            $raw = $request->headers->get('Referer');
        }

        return $raw ? self::normalise($raw) : null;
    }

    /** scheme://host[:port] lower-cased, default ports removed, anything else rejected. */
    public static function normalise(string $url): ?string
    {
        $p = parse_url(strtolower(trim($url)));
        if (! isset($p['scheme'], $p['host']) || ! in_array($p['scheme'], ['http', 'https'], true)) {
            return null;
        }
        $port = $p['port'] ?? null;
        if (($p['scheme'] === 'https' && $port === 443) || ($p['scheme'] === 'http' && $port === 80)) {
            $port = null;
        }

        return $p['scheme'].'://'.$p['host'].($port ? ":{$port}" : '');
    }

    /** Entries are exact origins; a leading "*." matches subdomains but not the bare domain. */
    public static function allows(?string $origin, array $allowed): bool
    {
        if ($origin === null) {
            return false; // an empty list therefore allows nothing
        }
        foreach ($allowed as $entry) {
            $entry = strtolower(trim($entry));
            if ($entry === $origin) {
                return true;
            }
            if (str_contains($entry, '://*.')) {
                [$scheme, $suffix] = explode('://*.', $entry, 2);
                $host = parse_url($origin, PHP_URL_HOST);
                if (str_starts_with($origin, $scheme.'://') && str_ends_with($host, '.'.$suffix)) {
                    return true;
                }
            }
        }

        return false;
    }
}

How it works

What it solves

An embeddable widget lives on other people's websites, but you do not want just anyone copying a customer's embed key and running their widget elsewhere. A domain allowlist ties each key to the sites the customer approved, without needing a login on the embedding page.

How it works

requestOrigin() takes the browser's Origin header, falling back to the origin of the Referer, because a plain script tag on the same site sends only a referer. normalise() reduces both forms to scheme://host[:port] in lower case and drops default ports, so https://Shop.Example.com:443/page and https://shop.example.com compare equal.

allows() supports two kinds of entries: an exact origin, or a wildcard such as https://*.example.com. The wildcard matches any subdomain but deliberately not the bare domain, and it keeps the scheme, so an http page cannot use an https entry. An empty list or a missing origin allows nothing.

The controller replies 403 with a harmless comment when the check fails. When it passes, it echoes the origin in Access-Control-Allow-Origin, adds Vary: Origin so caches stay correct, and sends a frame-ancestors policy for the widget frame.

Adapting it

  • Store allowed origins as a JSON array on the site and validate each entry when saved.
  • Allow http://localhost:3000 only when the site is flagged as a test site.

Pitfalls

  • Referrer policies can strip the header; tell customers to keep strict-origin-when-cross-origin or looser.
  • This is access control for convenience, not strong secrecy, since origins can be spoofed outside browsers.

How to use it

  1. Add key (string, unique) and allowed_origins (json) to a sites table and cast the latter to array.
  2. Put OriginAllowlist.php in app/Embed/ and the controller in app/Http/Controllers/.
  3. Register Route::get('/embed/{siteKey}.js', EmbedController::class);.
  4. Put your widget bootstrap script at public/widget/loader.js.
  5. Customers paste <script async src="https://your-app.example.com/embed/KEY.js"></script>.

Licence: DEV3 Snippet Licence. Free to use, change and ship in personal and commercial projects. Attribution is appreciated, not required. Please do not redistribute snippets as a collection.

Related snippets