FileGate
File Gate restricts access to private files on your Drupal site, ensuring only authenticated or authorized users can download them. It works by generating short-lived, signed URLs that grant temporary access after passing a configured "gate," such as a login, token, or form submission, making it suitable for headless and decoupled sites. This module prevents anonymous users from accessing private files even if they have the direct link, with per-field configuration for easy setup and robust security.
File Gate gates the delivery of private files behind pluggable "gate methods" and streams them through its own signed endpoint. It is built for decoupled and traditional Drupal alike: a trusted back end mints a short-lived signed URL after running its own gate (a lead form, a login, a purchase, a step-up authentication) and any front end redeems it — the bytes are delivered only through File Gate, never through /system/files.
The problem it solves
Core's private file system grants an anonymous visitor download access to a private file whenever any published entity that references it is viewable. That is often too permissive for gated content, and it ties delivery to your Drupal site's own access model — awkward when the front end lives elsewhere. File Gate replaces that with an explicit, deny-by-default decision:
- Deny by default. A
hook_file_download()implementation returns a hard veto for any gated private file requested at/system/files, overriding core's permissive behaviour (unless the account holds an explicit bypass permission). - Self-hosted signed delivery. The file is streamed from
GET /api/file-gate/downloadonly after a gate method approves the request. The private path is never disclosed, and delivery does not depend on anonymous file access. - Front-end-agnostic. Mint returns a root-relative, host-agnostic path; your front end prepends its own public origin. The signing secret never reaches the browser.
Gate methods
A gate method answers one question: has this request passed the gate for this file? Five ship in the core module:
signed_url— a short-lived, HMAC-signed URL. Per-field TTL, an absolute availability window, and usage limits (a one-time link ismax_uses = 1), all bound into the signature. Outstanding grants can be listed and revoked individually or in bulk (GET /api/file-gate/grants,POST /api/file-gate/grants/revoke-bulk, or byjti).authenticated— deliver to a logged-in Drupal user, with an optional role allowlist.token— a revocable per-grant token (its hash is stored and bound into the signature; revoke by deleting the hash, no secret rotation) and/or a pre-shared campaign-token allowlist for static links, with optional per-token TTL and max uses. Revoke is scoped to the mint credential's field allowlist.referrer_lock— a signed URL that is additionally only redeemable from an allowed origin/referrer. Defence in depth, not authorization — the header is spoofable.otp— a single-use one-time passcode e-mailed to a self-identified address. TTL-limited, attempt-locked, and hashed with the same authenticated secret material used for mint (named secrets and dual-key rotation included). PreferPOST /api/file-gate/otp/sessionso redeem uses an HttpOnlyFG_OTPcookie instead of putting secrets in the query string.
Three more ship in optional submodules, so the core module stays dependency-light:
- File Gate Form — a coupled
formmethod: Drupal renders a lightweight email / lead-capture form and grants the download on submission. Spam-guarded (honeypot + per-IP rate limit); aLeadCapturedEventlets the site persist leads into Contact, Webform, or a CRM without File Gate storing PII. - File Gate Commerce — a
commercemethod: deliver only to a buyer or licensee, re-checked live on every download so expiry and revocation take effect immediately. The bundled checker grants on a completed Drupal Commerce order matching a configured SKU via a SKU-scoped query (not a full order-history load); swap the entitlement-checker service for licences or an external API. Access gating, not DRM. - File Gate Assurance — an
assurancemethod: gate delivery on a hardware-backed, phishing-resistant OIDC assurance level (PIV/CAC — HSPD-12 / FIPS 201 — or FIDO2/WebAuthn) proven at any standards-compliant IdP. Provider-agnostic (OIDC discovery + JWKS), with opt-in DPoP (RFC 9449), edge-mTLS for PIV/CAC behind a trusted proxy, and optional RFC 7662 introspection. For browser downloads, a short-lived session bridge cookie lets the same plain signed URL work after step-up (no second hop). On same-origin Drupal SSO, the bridge can use the site'sopenid_connectsession access token when present. Field-configured authorize URLs can append IdPacr_valuesfor step-up without open redirects. Native WebAuthn RP mode and a user enrollment UI are available when you do not use an IdP as the sole relying party. This is federation (NIST SP 800-63C, an asserted level), not File Gate acting as a full-site AAL3 verifier. Requiresfirebase/php-jwt; native WebAuthn additionally requiresweb-auth/webauthn-lib.
How it works
A back end calls POST /api/file-gate/mint (authenticated with a shared secret, constant-time, rate-limited) and receives a signed path. The browser redeems it at GET /api/file-gate/download. Minted token grants and signed-url jtis can be listed or invalidated at GET /api/file-gate/grants, POST /api/file-gate/revoke, and bulk revoke; one-time passcodes are issued at POST /api/file-gate/otp (prefer /api/file-gate/otp/session for cookie-based redeem). Every endpoint fails closed when no signing secret is configured.
Mint can require an acting account (global or per-field), pin a multi-field file to one storage key, verify an OIDC token at mint time (assurance A2), and re-check host-entity view rights for identity-aware grants. Named mint credentials are scoped to field allowlists so one front end cannot mint the whole corpus. Gating is a third-party setting on the file field storage — a toggle plus a gate-method picker that forces and locks the private file system.
Security model
- Deny by default; every gate method must fail closed. The signing core is target-agnostic HMAC-SHA256 with constant-time comparison and an injective claim encoding.
- Secrets are injected from the environment, not committed to config. Named secrets (
$settings['file_gate.secrets']) can be limited to field scopes; minted URLs carryk=<id>, and scope is re-checked at redemption. Dual-key rotation keeps outstanding grants valid during cutover (previous_secrets/previous_download_secrets). - Step-up login for assurance uses only the field's configured absolute
step_up_login_url— query overrides are ignored (open-redirect defence). - Download denial flood limits bound abuse on the redeem endpoint.
- Optional soft integration with
audit_chainfor a durable, hash-chained security trail (mint, download, deny, revoke, OTP). - The admin settings page surfaces mint, delivery, and denial counts from the
file_gatelog channel when Database Logging is enabled. - File Gate is access gating, not DRM: it controls who receives the bytes, not what they do with them afterwards.
- The Assurance module is federation (an asserted level), not an AAL3 verifier; a plain signed URL is a bearer capability unless DPoP-bound or bridge-bound. Trade-offs are documented in the submodule README and design note.
Requirements
Drupal ^11.4 || ^12. Core File only — Media is optional (gating works for any private file field, whether or not the file is wrapped in a media entity). The Assurance submodule additionally requires firebase/php-jwt (^6.10 || ^7.0); native WebAuthn mode also needs web-auth/webauthn-lib (^5). Both are suggested dependencies.
Documentation
The repository ships a full README.md, API reference (docs/API.md), architecture decisions (docs/ADR.md), assurance redeem / bridge notes (docs/assurance-redeem.md), secret rotation runbook, optional audit trail, E2E checklist, program plan, and the design note for PIV/CAC and WebAuthn. Please use the issue queue for bugs and feature requests.
Maintainers
Maintained by Jeremy Michael Cerda, sponsored by Wilkes & Liberty, LLC.