Native Passkeys in Adobe ColdFusion 2025 - Part 1: Registering Your First Passkey

Because Passwords Have Suffered Enough

I have been building authentication systems since HTML introduced forms in HTML 2. Passwords. Password reset links. Security questions. One-time codes. Multi-factor authentication. Account lockouts. Password complexity rules carefully designed to ensure that every user eventually chooses Winter2026!.

We have spent decades trying to make passwords safer. Mostly by making them more irritating.

Passkeys offer a better model. The user authenticates using a credential held by their device or password manager. The private key never reaches our application. There is no password for us to hash, leak, reset, accidentally log or discover sitting in a spreadsheet named passwords-final-FINAL.xlsx.

A passkey is a cryptographic credential that allows a device to prove that the person using it controls an enrolled account.

When a passkey is created, the device generates a public and private key pair. The application stores the public key. The private key remains with the device or the user’s trusted password manager.

During sign-in, the application sends a unique challenge. The device asks the user to approve the request using facial recognition, fingerprint recognition, Windows Hello, a personal identification number or another local verification method. It then uses the private key to sign the challenge. The application verifies that signature using the public key it already has.

The biometric information is never sent to the application. We don't receive a fingerprint, a facial scan or even the device’s personal identification number. The device simply tells us, cryptographically, that the person holding the credential satisfied its local security requirements.

On supported devices, private keys and signing operations may be protected by hardware such as Apple’s Secure Enclave, a Trusted Platform Module or a similar secure environment. Synced passkeys can also be protected and transferred through an end-to-end encrypted password-manager ecosystem.

The application is not being told, “This fingerprint belongs to David.” It is being told, “The person using this device successfully unlocked the credential registered for this website, and here is mathematical proof.”

That is both more private and considerably harder to fake than most user-generated passwords.

ColdFusion 2025 now includes native passkey functions:

passkeyRegister()
passkeyAuthenticate()
passkeyGetResult()

That means we can add passkeys without implementing the Web Authentication protocol ourselves. I enjoy learning how things work, but I have no desire to personally parse authenticator data, validate client-data hashes and build a Concise Binary Object Representation decoder while muttering, “I’m sure this is fine.”

ColdFusion makes hard things easy.

In this four-part series, I'll build a complete passkey implementation. I'll start with registration, add authentication, move the ceremony to a central authentication origin and eventually make the whole thing survive reverse proxies and multiple ColdFusion nodes.

Trust me... That last part is where hope goes to die.

For now, we're going to keep things simple:

  • One ColdFusion application
  • One hostname
  • One ColdFusion instance
  • One authenticated user
  • One passkey registration flow

By the end of this article, an existing user will be able to add a passkey to their account. Period.

What ColdFusion Is Doing for Us

A Web Authentication registration ceremony has several moving parts. The server creates a challenge. The browser passes that challenge to the authenticator. The authenticator creates a credential and signs the response. The server validates the response and stores the credential.

Conceptually, our flow looks like this:

Authenticated user
        |
        v
/account/add-passkey.cfm
        |
        v
PasskeyRegister()
        |
        v
CFPasskey.startRegistration()
        |
        v
Browser Web Authentication interface
        |
        v
DatabasePasskey.cfc
        |
        v
/auth/passkey-callback.cfm
        |
        v
PasskeyGetResult()

ColdFusion handles the protocol-specific work:

  • Creating the Web Authentication challenge
  • Calling the browser credential interfaces
  • Validating the authenticator response
  • Storing the credential
  • Returning a short-lived result token to our callback

Our application is still responsible for the application-specific decisions:

  • Which user is registering the passkey
  • Which relying party identifier we trust
  • Where the callback is allowed to go
  • Whether the registration result belongs to the expected user
  • What happens after registration succeeds or fails

This is an important distinction. ColdFusion proves that the passkey ceremony succeeded, but it doesn't decide whether the person initiating that ceremony should be allowed to attach a credential to a particular account.

That part is still our problem. Authentication systems are generous that way.

Before We Write Code

You need a fully patched ColdFusion 2025 installation that exposes the three native passkey functions.

Don't permanently tie your application to a specific update number. ColdFusion updates continue to move, and security patches have a distressing habit of becoming important immediately after you postpone installing them.

We can check the functions directly:

<cfscript>
    functions = getFunctionList();

    passkeys_supported =
        structKeyExists( functions, "PasskeyRegister" )
        && structKeyExists( functions, "PasskeyAuthenticate" )
        && structKeyExists( functions, "PasskeyGetResult" );

    writeDump( passkeys_supported );
</cfscript>

You will also need:

  • Session management enabled
  • A configured datasource
  • A stable hostname using HTTPS
  • Access to the ColdFusion Administrator
  • An existing authenticated user

Web Authentication depends on a secure browser context. In production, that means HTTPS. Local development can sometimes use browser-supported localhost exceptions, but a real hostname with an encrypted connection will produce fewer opportunities to spend an evening blaming Safari for something you configured incorrectly.

Ask me how I know.

Actually, don’t.

Configuring the DatabasePasskey Backend

ColdFusion provides a DatabasePasskey backend that stores credentials in a ColdFusion datasource. The passkey configuration belongs to ColdFusion. It isn't an Application.cfc setting.

The following one-time script configures it:

<cfscript>
    administrator = createObject( "component", "CFIDE.adminapi.administrator" );
    administrator.login( "YOUR_CF_ADMIN_PASSWORD" );

    security = createObject( "component", "CFIDE.adminapi.security" );
    security.setPasskeyConfig( { datasource: "passkey_demo", challengeStore: "memory", challengeTtl: 60 } );

    writeOutput( "Passkey configuration saved." );
</cfscript>

Now raise your right hand and repeat after me.

“I will not put my ColdFusion Administrator password in source control. I will not leave this script publicly accessible. I will not name it configure-passkeys-public-do-not-run.cfm and hope attackers respect the filename. I will only run it through a protected administrative process, confirm the configuration and remove it from the webroot.”

For this first article, we're using the memory challenge store because we have one ColdFusion instance. A challenge created on that instance will be completed on the same instance.

This will become insufficient when we add multiple nodes. We'll deal with shared caches and load-balancer stickiness in part four, after we've developed enough emotional resilience to handle the scarring it will cause.

ColdFusion’s backend creates and manages its own credential table in the configured datasource. Treat that table as engine-owned. Our application will use the native functions rather than trying to insert credential rows itself.

Keeping the Passkey Service Inside the Application

ColdFusion’s browser-side passkey code makes an HTTP request to DatabasePasskey.cfc during the ceremony. That request must execute inside the same application and session that initiated the registration.

This matters because ColdFusion’s passkey implementation includes session-bound protection against cross-site request forgery. If the service request lands in another ColdFusion application or another CommandBox engine, it receives a different session. The endpoint exists, the browser reaches it, and the ceremony still fails with CSRF_INVALID.

Remember... we're building an authentication system here. Security is paramount.

These are my favourite failures: everything is technically working except the part anyone cares about.

Adobe installs the service here:

/CFIDE/passkey/DatabasePasskey.cfc

Adobe’s documentation also tells the browser to call it through that path.

This presents a small philosophical disagreement between two otherwise sensible pieces of advice:

  1. Adobe’s passkey service must be reachable by the browser.
  2. The ColdFusion internal service directory should not be reachable from the internet.

Both are true, which is always convenient.

My workaround is to keep /CFIDE blocked while exposing an application-local alias:

/__cf_passkey/DatabasePasskey.cfc

On Linux, that alias can be created by linking the application webroot to the passkey directory belonging to the same ColdFusion engine:

ln -s \
    /absolute/path/to/your/coldfusion/CFIDE/passkey \
    /var/www/passkey-demo/__cf_passkey

A conventional Adobe installation and a CommandBox-managed installation won't necessarily place those files in the same location. A server running multiple CommandBox instances may also have several copies. The symbolic link must point to the CFIDE/passkey directory belonging to the engine that serves this application.

The web server should then route this exact address to ColdFusion:

/__cf_passkey/DatabasePasskey.cfc

Every other address beneath /__cf_passkey should be denied. The symbolic link targets a directory because DatabasePasskey.cfc inherits code from neighbouring ColdFusion components, but those neighbouring files don't need to be accessible over Hypertext Transfer Protocol.

To be completely clear, this is a workaround, not a best practice I would enthusiastically reproduce elsewhere. It makes an Adobe-supplied remote ColdFusion component publicly reachable because Adobe’s browser software requires one. The component validates Adobe’s session-bound protection against cross-site request forgery and the Web Authentication ceremony, so it isn't an unguarded database endpoint. That doesn't magically make exposing vendor-managed code from beneath /CFIDE an elegant architecture.

Don't copy the ColdFusion component into your application. A copied version will quietly fall behind ColdFusion security updates, eventually becoming an archaeological exhibit with network access. Link to the engine-managed version, expose only the required address, and verify the arrangement after every ColdFusion update.

The cleaner solution needs to come from Adobe: a supported application-local service endpoint that runs inside the initiating application and session without requiring any part of /CFIDE to be exposed.

Until then, this is the least-bad bridge between Adobe’s implementation and a locked-down server. Reverse-proxy routing deserves more attention than this shortened setup, so we'll give it the full treatment in part four.

Creating a Passkey Service

I prefer to keep the native ColdFusion calls behind a small component.

Could we call passkeyRegister() directly from every controller and template? Yes. We could also scatter relying party identifiers, callback paths and service addresses throughout the application until changing one of them feels like defusing a bomb assembled by our former selves.

I have been that former self often enough in my past not to do it again.

Create models/native_passkey_service.cfc:

component output="false" {

    public nativePasskeyService function init(
        required string rpName,
        required string rpId,
        string servicePath = "/__cf_passkey/DatabasePasskey.cfc",
        string callbackPath = "/auth/passkey_callback.cfm"
    ) {
        variables.rpName = trim( arguments.rpName );
        variables.rpId = lCase( trim( arguments.rpId ) );
        variables.servicePath = arguments.servicePath;
        variables.callbackPath = arguments.callbackPath;

        if ( !len( variables.rpName ) ) {
            throw( type = "passkey.invalidConfiguration", message = "The passkey relying party name is required." );
        }

        if ( !len( variables.rpId ) ) {
            throw( type = "passkey.invalidConfiguration", message = "The passkey relying party identifier is required." );
        }

        return this;
    }

    public boolean function isSupported() {
        var functions = getFunctionList();
        return
            structKeyExists( functions, "PasskeyRegister" )
            && structKeyExists( functions, "PasskeyAuthenticate" )
            && structKeyExists( functions, "PasskeyGetResult" );
    }

    public struct function buildRegistrationUser( required struct user ) {
        var displayName = trim( toString( arguments.user.firstName ?: "" ) & " " & toString( arguments.user.lastName ?: "" ) );
        if ( !len( displayName ) ) { displayName = arguments.user.email; }

        return {
            name: arguments.user.email,
            displayName: displayName,
            id: toString( arguments.user.id ),
            rpName: variables.rpName,
            rpId: variables.rpId,
            userVerification: "preferred",
            authenticatorAttachment: "platform",
            attestation: "none"
        };
    }

    public struct function buildConfig() {
        return { redirectUrl: variables.callbackPath, service: variables.servicePath };
    }

    public struct function interpretResult(
        required string token
    ) {
        var result = {
            success: false,
            action: "",
            userId: "",
            credentialId: "",
            error: "invalid_result"
        };

        if ( !len( trim( arguments.token ) ) ) { return result; }

        try {
            var nativeResult = passkeyGetResult( arguments.token );
            if ( !isStruct( nativeResult ) ) { return result; }

            result.success = nativeResult.success ?: false;
            result.action = lCase( trim( nativeResult.action ?: "" ) );
            result.userId = normalizeUserId( nativeResult.userId ?: "" );
            result.credentialId = toString( nativeResult.credentialId ?: "" );
            result.error = result.success ? "" : toString( nativeResult.message ?: "registration_failed" ); }
        catch ( any error ) {
            result.error = "invalid_result";
        }

        return result;
    }

    public string function normalizeUserId( required string userId ) {
        return reReplace( trim( arguments.userId ), "::\|::[0-9]+$", "", "one" );
    }
}

This component centralizes four decisions:

  • Whether the installed engine supports native passkeys
  • How our application identifies the user
  • Which relying party configuration we send to ColdFusion
  • How we normalize the result returned by ColdFusion

The relying party identifier is deliberately supplied when the component is created. It doesn't come from an address parameter, form field or whichever host header happens to arrive with the request. The server decides which relying party it is. A browser shouldn't be able to post:

rpId = definitely-not-a-phishing-site.example

and convince our application to shrug and go along with it.

Understanding the Registration User Structure

The structure passed to passkeyRegister() contains both user information and Web Authentication options:

{
    name: "ada@example.com",
    displayName: "Ada Lovelace",
    id: "019cc835-28c5-7eec-a8ab-9c4fe4ac4934",
    rpName: "Example Application",
    rpId: "app.example.com",
    userVerification: "preferred",
    authenticatorAttachment: "platform",
    attestation: "none"
}

The abbreviated rpName and rpId properties are required by ColdFusion’s native function. We can spell out the terms when we discuss them, but the structure itself must use the property names ColdFusion expects.

The id value deserves particular attention. Use a stable internal user identifier. Don't use a mutable display name. I also avoid using the email address as the identifier because email addresses change, get corrected, are sometimes shared and occasionally turn out to have been entered by someone whose relationship with spelling is largely theoretical. The example uses a universally unique identifier. The other values control the browser ceremony:

  • userVerification: "preferred" requests user verification when available.
  • authenticatorAttachment: "platform" prefers an authenticator built into the current device.
  • attestation: "none" avoids requesting identifying attestation information from the authenticator.

If you want to allow roaming security keys, you may choose to omit or change authenticatorAttachment. The correct choice depends on the users and security model of your application. For this implementation, we're beginning with platform authenticators: facial recognition, fingerprint recognition, Windows Hello and similar device-backed credentials.

Initializing the Service

Create the service once during application startup. In Application.cfc:

component {
    this.name = "PasskeyDemo";
    this.sessionManagement = true;
    this.sessionTimeout = createTimespan( 0, 2, 0, 0 );
    this.setClientCookies = true;

    public boolean function onApplicationStart() {
        application.passkeys = new models.nativePasskeyService(
            rpName = "Example Application",
            rpId = "app.example.com",
            servicePath = "/__cf_passkey/DatabasePasskey.cfc",
            callbackPath = "/auth/passkey_callback.cfm"
        );

        return true;
    }
}

Modify this to match the information for your application. The relying party identifier must agree with the origin where the browser performs the ceremony. We'll eventually make this more flexible, but flexibility isn't always our friend during the first security-sensitive implementation. Sometimes flexibility is simply ambiguity wearing business-casual clothing.

Starting Registration

The registration endpoint must require an authenticated user. This isn't a signup endpoint. It adds a passkey to an account that has already been authenticated using another trusted method. Create account/add-passkey.cfm:

<cfscript>
    if (
        !structKeyExists( session, "signedIn" )
        || !session.signedIn
        || !structKeyExists( session, "user" )
    ) {
        location( url = "/signin.cfm", addToken = false );
    }

    if ( !application.passkeys.isSupported() ) {
        location( url = "/account/security.cfm?passkey=unsupported", addToken = false );
    }

    user = {
        id: session.user.id,
        email: session.user.email,
        firstName: session.user.firstName ?: "",
        lastName: session.user.lastName ?: ""
    };

    session.pending_passkey_registration = { userId: toString( user.id ), createdAt: now() };

    try { PasskeyRegister( application.passkeys.buildRegistrationUser( user ), application.passkeys.buildConfig() ); }
    catch ( any error ) {
        structDelete( session, "pending_passkey_registration" );

        writeLog(
            type = "error",
            file = "authentication",
            text = "PASSKEY_REGISTRATION_START_FAILED" & " type=#error.type ?: ''#" & " message=#error.message ?: ''#"
        );

        location( url = "/account/security.cfm?passkey=start_failed", addToken = false );
    }

    include "../includes/passkey-ceremony.cfm";
</cfscript>

The pending registration record binds the ceremony to the user who started it. We don't put the credential, challenge or result token in the session. ColdFusion manages the ceremony state. We retain only enough application state to answer one question when the callback arrives:

Is this result for the same user who started registration?

The log entry also avoids dumping the complete exception object. Authentication errors have an unfortunate tendency to contain exactly the information we shouldn't preserve in a log file until the heat death of the universe. Calling passkeyRegister() injects ColdFusion’s passkey configuration and browser-side software into the response.

Starting the Browser Ceremony

Our page still needs to start the registration ceremony. Create includes/passkey-ceremony.cfm:

<div id="passkey-status" role="status">Waiting for your device…</div>
<div id="passkey-error" role="alert" hidden>We could not register this passkey.</div>

<script>
    (function () {
        "use strict";

        var status = document.getElementById( "passkey-status" );
        var error = document.getElementById( "passkey-error" );
        var attempts = 0;
        var started = false;

        function fail(message) {
            if (started === "failed") { return; }
            started = "failed";
            status.hidden = true;
            error.textContent = message;
            error.hidden = false;
            error.setAttribute( "tabindex", "-1" );
            error.focus();
        }

        function startRegistration() {
            if (started) { return; }

            if (!window.PublicKeyCredential) {
                fail( "This browser does not support passkeys." );
                return;
            }

            if (
                !window.CFPasskey
                || !window.CFPasskey._config
                || !window.CFPasskey._config.action
            ) {
                attempts++;

                if (attempts > 60) {
                    fail( "The passkey service is unavailable." );
                    return;
                }

                window.setTimeout( startRegistration, 200 );
                return;
            }

            if ( typeof window.CFPasskey.startRegistration !== "function" ) {
                fail( "Passkey registration is unavailable." );
                return;
            }

            started = true;

            try {
                var registration = window.CFPasskey .startRegistration();

                if ( registration && typeof registration.catch === "function" ) {
                    registration.catch(
                        function (reason) {
                            var name = reason && reason.name ? String( reason.name ) : "";

                            if ( name === "NotAllowedError" || name === "AbortError" ) {
                                fail( "Passkey registration was cancelled." );
                                return;
                            }

                            fail( "We could not register this passkey." );
                        }
                    );
                }
            }
            catch (reason) { fail( "We could not register this passkey." ); }
        }

        window.setTimeout( startRegistration, 200 );
    })();
</script>

Why poll for CFPasskey instead of calling it immediately? The native function injects and initializes ColdFusion’s browser-side code as part of the response. Depending on how the page is assembled, our script may run before that initialization is complete. I originally assumed the object would always be waiting for me. Computers enjoy confidence. It gives them something to punish.

The polling is bounded. After approximately twelve seconds, the page stops waiting and shows a recoverable error instead of displaying a spinner until the user retires. On success, ColdFusion redirects the browser to the callback path we supplied in buildConfig().

Processing the Callback

Create auth/passkey-callback.cfm:

<cfscript>
    param name = "url.passkey_token" default = "";
    pending = session.pending_passkey_registration ?: {};
    structDelete( session, "pending_passkey_registration" );

    if ( structIsEmpty( pending ) ) {
        location( url = "/account/security.cfm?passkey=invalid_state", addToken = false );
    }

    if (
        !isDate( pending.createdAt )
        || dateDiff( "s", pending.createdAt, now() ) > 300
    ) {
        location( url = "/account/security.cfm?passkey=expired", addToken = false );
    }

    result = application.passkeys.interpretResult( url.passkey_token );

    if ( !result.success || result.action != "registration" ) {
        location( url = "/account/security.cfm?passkey=failed", addToken = false );
    }

    if ( compareNoCase( result.userId, toString( pending.userId ) ) != 0 ) {
        writeLog(
            type = "error",
            file = "authentication",
            text = "PASSKEY_REGISTRATION_USER_MISMATCH"
        );

        location( url = "/account/security.cfm?passkey=user_mismatch", addToken = false );
    }

    /*
     * ColdFusion has already validated and stored the credential.
     * Update any application-level security status here.
     */
    location( url = "/account/security.cfm?passkey=registered", addToken = false );
</cfscript>

There are several deliberate checks here. First, we require a pending registration in the current session. Second, we give that pending registration a short lifetime. A callback discovered in an elderly browser tab shouldn't remain valid until someone accidentally clicks it during an estate sale. Third, the native result must report a successful registration action. Finally, the returned user identifier must match the user who initiated registration. We don't trust the callback simply because it contains a valid passkey result. We verify that the result belongs to the operation our application expected. After passkeyGetResult() reports success, ColdFusion’s backend has already stored the credential. The application doesn't need to write the credential itself. It may update its own account-security state, audit record or user interface, but the Web Authentication credential remains owned by the native passkey backend.

Don't Log the Result Token

The passkey_token value is short-lived, but that doesn't make it decorative.

Don't log it.

Don't include it in exception dumps.

Don't send it to analytics.

Don't preserve the complete callback address in access logs if those logs are broadly available.

The same rule applies to credential identifiers, challenges, authenticator data and client-data represented as JSON. Log outcomes and safe diagnostic codes:

PASSKEY_REGISTRATION_STARTED
PASSKEY_REGISTRATION_COMPLETED
PASSKEY_REGISTRATION_CANCELLED
PASSKEY_REGISTRATION_USER_MISMATCH

Avoid logging the ceremony material itself. A useful log tells us what happened without quietly assembling an authentication museum in /var/log.

A Small Test for the Service

Most of the browser ceremony requires a real browser and authenticator, but we can still test our configuration-building code. This assumes you're using the ColdBox framework, so if you're using anything else, you'll need to craft your own tests.

Or skip testing entirely like an animal.

Create tests/nativePasskeyServiceSpec.cfc:

component extends="testbox.system.BaseSpec" {
    function run() {

        describe(
            "nativePasskeyService",
            function() {

                beforeEach( function() {
                    variables.service =
                        new models.nativePasskeyService(
                            rpName = "Example Application",
                            rpId = "app.example.com"
                        );
                } );


                it(
                    "builds a server-controlled registration structure",
                    function() {
                        var user = {
                            id: "019cc835-28c5-7eec-a8ab-9c4fe4ac4934",
                            email: "ada@example.com",
                            firstName: "Ada",
                            lastName: "Lovelace"
                        };

                        var result =
                            variables.service
                                .buildRegistrationUser( user );

                        expect( result.id )
                            .toBe( user.id );

                        expect( result.name )
                            .toBe( user.email );

                        expect( result.displayName )
                            .toBe( "Ada Lovelace" );

                        expect( result.rpName )
                            .toBe( "Example Application" );

                        expect( result.rpId )
                            .toBe( "app.example.com" );

                        expect( result.attestation )
                            .toBe( "none" );
                    }
                );


                it(
                    "uses the application-local service path",
                    function() {
                        var config =
                            variables.service.buildConfig();

                        expect( config.service )
                            .toBe(
                                "/__cf_passkey/DatabasePasskey.cfc"
                            );
                    }
                );


                it(
                    "normalizes ColdFusion composite user identifiers",
                    function() {
                        var raw =
                            "019cc835-28c5-7eec-a8ab-9c4fe4ac4934"
                            & "::|::2";

                        expect(
                            variables.service.normalizeUserId( raw )
                        ).toBe(
                            "019cc835-28c5-7eec-a8ab-9c4fe4ac4934"
                        );
                    }
                );

            }
        );

    }

}

This doesn't prove that fingerprint recognition will work through your production load balancer. Nothing that convenient exists. It does protect the application-level contract: stable user identifiers, a fixed relying party identifier, a fixed callback and an application-local service path. The real ceremony should also be tested manually using every browser and device family you intend to support.

Trying It

With the engine configured and the application running over HTTPS:

  1. Sign in using your existing authentication method.
  2. Visit /account/add-passkey.cfm.
  3. Approve the browser’s passkey prompt.
  4. Allow ColdFusion to redirect to the callback.
  5. Confirm that the security page reports success.
  6. Confirm that the native credential table contains a row for the user.

Don't test only the happy path. Also try:

  • Cancelling the operating-system prompt
  • Reloading the ceremony page
  • Calling the callback without a token
  • Calling the callback after clearing the session
  • Using an unsupported browser
  • Changing the hostname
  • Allowing the pending registration to expire

Security code has a remarkable ability to work perfectly when approached politely. Users, browsers and the internet are under no obligation to be polite.

What We Built

We now have a complete native passkey registration flow:

  • ColdFusion creates and validates the Web Authentication ceremony.
  • ColdFusion’s DatabasePasskey backend stores the credential.
  • Our application controls the relying party identifier and callback.
  • Registration is limited to an existing authenticated user.
  • The callback verifies that the result belongs to the expected registration.
  • Passkey internals stay out of our database code and logs.

More importantly, we didn't implement Web Authentication cryptography ourselves. I consider that a feature.

In part two, we'll use the stored credential to authenticate a user with passkeyAuthenticate(). We'll add discoverable login, establish the application session, handle stale credentials and give users a safe way to view and remove their passkeys. At that point, we'll have passwordless login. Then we'll put it behind a reverse proxy and discover how many different definitions of “origin” can exist in a system that supposedly has only one web address.

It is more than you would hope.