Skip to content

Pages Plugins

Put a staff area behind Cloudflare Access and check a signup form with Turnstile on your Pages site. Each plugin returns a Pages Function, which an F# module exports under a handler name such as onRequest.

Staff Area

Compiled to functions/staff/_middleware.js, this module is the middleware for /staff and every path below it. It validates the Access token in each request's Cf-Access-Jwt-Assertion header. When validation fails, it redirects the browser to your team's Access login page with a 302 status. aud is your Access application's audience tag, and domain is your team domain.

module Access = FSharp.CloudEdge.Runtime.PagesPluginCloudflareAccess

let onRequest =
    Access.Exports.pagesPluginCloudflareAccess(
        Access.PluginArgs.Create(
            aud = "your-application-aud-tag",
            domain = "https://your-team.cloudflareaccess.com"
        )
    )

Needs a Cloudflare Access application that covers /staff. Use its audience tag as aud.

Emitted JavaScript
import pages_plugin_cloudflare_access from "@cloudflare/pages-plugin-cloudflare-access";

export const onRequest = pages_plugin_cloudflare_access({
    aud: "your-application-aud-tag",
    domain: "https://your-team.cloudflareaccess.com",
});

Staff Profile

After the middleware, context.data.cloudflareAccess.JWT holds the token's payload and a getIdentity function. getIdentity requests the user's identity from your team domain with the token. It resolves to Some identity when the reply has a success status, and to None for any other status. The identity includes the user's name and email, and its groups array lists the user's groups.

open Fable.Core

module Workers = FSharp.CloudEdge.Runtime.Workers
module Access = FSharp.CloudEdge.Runtime.PagesPluginCloudflareAccess

let onRequestGet (context: Workers.EventContext<obj, string, Access.PluginData>) =
    async {
        let! identity = context.data.cloudflareAccess.JWT.getIdentity () |> Async.AwaitPromise
        match identity with
        | Some person ->
            let profile = {| name = person.name; email = person.email; groups = person.groups |}
            return Workers.Exports.Response.json profile
        | None ->
            let notFound = Workers.ResponseInit.Create(status = 404.)
            return Workers.Exports.Response.Create("No identity for this sign-in", notFound)
    }
    |> Async.StartAsPromise

Needs the Staff Area middleware in the same directory or a parent directory.

generateLogoutURL builds the Access logout address for your team domain. The function responds with a 302 redirect to that address.

module Workers = FSharp.CloudEdge.Runtime.Workers
module Access = FSharp.CloudEdge.Runtime.PagesPluginCloudflareAccess

let onRequestGet (_: Workers.EventContext<obj, string, obj>) =
    let logout =
        Access.Api.Exports.generateLogoutURL(
            Access.Api.GenerateLogoutURL0.Create(domain = "https://your-team.cloudflareaccess.com"))
    Workers.Exports.Response.redirect(logout, 302.)
Emitted JavaScript
import { generateLogoutURL } from "@cloudflare/pages-plugin-cloudflare-access/api";

export function onRequestGet(_arg) {
    const logout = generateLogoutURL({
        domain: "https://your-team.cloudflareaccess.com",
    });
    return globalThis.Response.redirect(logout, 302);
}

Signup Form

Pages calls the functions of an exported array in order. The Turnstile plugin validates the token in the form's cf-turnstile-response field. When the token is valid, register handles the request next. For an invalid token, the plugin returns a 400 response with Turnstile's error descriptions.

open Fable.Core

module Workers = FSharp.CloudEdge.Runtime.Workers
module Turnstile = FSharp.CloudEdge.Runtime.PagesPluginTurnstile

let verifyHuman =
    Turnstile.Exports.pagesPluginTurnstile(
        Turnstile.PluginArgs.Create(secret = "1x0000000000000000000000000000000AA"))

let register (context: Workers.EventContext<obj, string, Turnstile.PluginData>) =
    async {
        let! form = context.request.formData () |> Async.AwaitPromise
        match form.get "email" with
        | Some (U2.Case1 email) -> return Workers.Exports.Response.Create($"Thanks for signing up, {email}.")
        | _ ->
            let badRequest = Workers.ResponseInit.Create(status = 400.)
            return Workers.Exports.Response.Create("An email address is required.", badRequest)
    }
    |> Async.StartAsPromise

let onRequestPost: obj[] = [| verifyHuman; register |]

The type of a form value is U2<string, File>, and U2.Case1 matches the text of the email field.

Needs a Turnstile widget on the signup form. The secret in the code is Turnstile's test secret key, which passes every validation. A live site uses the widget's own secret key.

Library Table

Runtime.PagesPluginStaticForms binds the plugin that handles submissions from HTML forms with a data-static-form-name attribute. From .NET, Management.Security creates Access applications with AccessApplicationsAddAnApplication and Turnstile widgets with AccountsTurnstileWidgetCreate.

Library npm package What it covers
Runtime.PagesPluginCloudflareAccess @cloudflare/pages-plugin-cloudflare-access 1.0.5 Access middleware, identity
Runtime.PagesPluginTurnstile @cloudflare/pages-plugin-turnstile 1.0.2 Turnstile validation
Runtime.PagesPluginStaticForms @cloudflare/pages-plugin-static-forms 1.0.3 Static form replies
Runtime.Workers @cloudflare/workers-types 5.20260906.1 Pages Functions context

NuGet packages

Management.Security 0.1.0, Runtime.PagesPluginCloudflareAccess 0.1.0, Runtime.PagesPluginStaticForms 0.1.0, Runtime.PagesPluginTurnstile 0.1.0, Runtime.Workers 0.1.0.

See installation and release availability.

Edit this page