Passkeys are one of those features that feel like they should be boring by now. The user clicks a button, Face ID or Touch ID does its magic, nobody has to remember whether their password needs a hieroglyph and a seasonal vegetable, and everyone moves on with their lives.

That is the dream.

The production reality was a little more… archaeological.

This is not a “ColdFusion passkeys are bad” post. Quite the opposite. The feature is promising, and once the pieces were in the right places, the ceremony worked. But getting there exposed a few sharp edges that are worth documenting for anyone trying to run this in a real application behind a load balancer with multiple ColdFusion nodes.

The setup

The application is a ColdFusion 2025 application running on two app nodes behind an AWS Application Load Balancer.

The relevant production shape looked roughly like this:

  • Adobe ColdFusion 2025 Update 8
  • Two ColdFusion app nodes
  • Apache with the ColdFusion connector
  • AWS ALB in front
  • Redis-backed shared application sessions
  • PostgreSQL datasource
  • Native ColdFusion passkey support using Adobe’s DatabasePasskey.cfc

This is not an unusual deployment shape. It is exactly the kind of setup where native platform support should shine.

The first surprise: /CFIDE is still /CFIDE

ColdFusion’s native passkey examples point to a service path like this:

/CFIDE/passkey/DatabasePasskey.cfc/CFIDE/passkey/DatabasePasskey.cfc

That is understandable from Adobe’s perspective. The passkey service lives under CFIDE.

But in a production app, publicly exposing CFIDE is usually a non-starter. In my case, the ColdFusion web server connector was also explicitly blocking requests that started with /CFIDE.

The logs were not subtle:

Blocking this uri: [/CFIDE/passkey/DatabasePasskey.cfc] since its starting with cfideBlocking this uri: [/CFIDE/passkey/DatabasePasskey.cfc] since its starting with cfide

That is one of those messages that is both helpful and deeply annoying. Yes, it is blocked. Yes, it should be blocked. No, that does not help the passkey ceremony complete.

The first instinct might be to copy DatabasePasskey.cfc somewhere into the application. Do not do that.

That file depends on sibling Adobe files such as IPasskey.cfc, and copying vendor-managed CFIDE files into your app tree is a maintenance trap. It will break on upgrades, drift from Adobe’s implementation, and leave you owning code you should absolutely not own.

The better solution was to expose only the passkey service through a non-CFIDE application path.

In my case, that became:

/__cf_passkey/DatabasePasskey.cfc/__cf_passkey/DatabasePasskey.cfc

with a symlink under the application webroot:

/var/www/rosterli/app/__cf_passkey -> /opt/coldfusion2025/cfusion/wwwroot/CFIDE/passkey/var/www/rosterli/app/__cf_passkey -> /opt/coldfusion2025/cfusion/wwwroot/CFIDE/passkey

The application-level service path was changed to use:

/__cf_passkey/DatabasePasskey.cfc/__cf_passkey/DatabasePasskey.cfc

instead of:

/CFIDE/passkey/DatabasePasskey.cfc/CFIDE/passkey/DatabasePasskey.cfc

That avoided exposing CFIDE broadly and avoided copying Adobe’s CFCs.

The second surprise: Apache Alias can accidentally defeat your application context

At one point, Apache had an Alias like this:

Alias /__cf_passkey/ /opt/coldfusion2025/cfusion/wwwroot/CFIDE/passkey/Alias /__cf_passkey/ /opt/coldfusion2025/cfusion/wwwroot/CFIDE/passkey/

That looked reasonable. It was not.

The service technically resolved, but it resolved directly to the physical CFIDE directory instead of through the application webroot symlink. That matters because ColdFusion application context matters. The passkey ceremony depends on ColdFusion session and state behavior. Resolving outside the intended app context is a lovely way to get errors that look like application bugs but are actually routing/context bugs wearing a fake mustache.

Removing the Alias and letting the path resolve through the app webroot symlink was the correct move.

The Apache config ended up needing only the rewrite and connector mapping for the passkey service path, not a direct Alias to CFIDE.

The third surprise: the credentials table is auto-created

Adobe’s DatabasePasskey.cfc stores credentials in a table named passkey_credentials

That table is auto-created on first use which is convenient, but it has an operational implication: your datasource user needs enough permission to create that table the first time the passkey service initializes.

In my production setup, the application datasource does not normally have CREATE permissions. That is intentional. Production application users should not generally be wandering around creating tables like a raccoon with a keyboard.

So the passkey flow got far enough to trigger the biometric ceremony, then failed with:

Initialization failed: Failed to create passkey credentials tableInitialization failed: Failed to create passkey credentials table

The fix was to temporarily allow creation, let Adobe create the passkey_credentials table, then revoke CREATE again and leave only the needed DML permissions.

The durable permission state should be closer to:

SELECT
INSERT
UPDATE
DELETE

on passkey_credentials, not permanent schema-level CREATE.

ColdFusion’s auto-create behavior is useful, but for production teams with locked-down database permissions, this needs to be part of the deployment checklist.

The fourth surprise: Redis sessions did not save me from stickiness

This was the big one.

After the routing and table problems were fixed, the ceremony still failed with:

{
"code": "CSRF_INVALID",
"success": false,
"error": "CSRF token is invalid or has expired."
}

At first glance, this looked like my application’s CSRF handling. It was not.

The response did not come from my application. It came from Adobe’s passkey service. ColdFusion’s native passkey system has its own built-in CSRF/state validation around the ceremony.

The real problem was the load balancer.

The registration page was served from one app node, but the passkey service POST went to the other node:

server-1 served the ceremony page
server-2 received POST /__cf_passkey/DatabasePasskey.cfc?method=generateRegistrationOptions

That broke Adobe’s internal passkey CSRF/challenge validation.

Even though the application uses Redis-backed shared sessions, the native passkey challenge/CSRF state was still effectively node-sensitive in this configuration.

Enabling ALB stickiness immediately fixed the passkey ceremony.

I set the stickiness duration short, around five minutes. That is long enough for:

registration page -> generateRegistrationOptions -> biometric ceremony -> callbackregistration page -> generateRegistrationOptions -> biometric ceremony -> callback

but not so long that normal traffic distribution gets unnecessarily sticky for hours.

Sticky sessions: acceptable workaround, not my favorite architecture

My instinct is still that sticky sessions should not be the final answer if the system can avoid them.

They solve this problem, but they do it by routing around the problem rather than eliminating it. If a feature requires node-local state, stickiness is a practical fix. But for a modern load-balanced application, I would rather have all ceremony/session/challenge state shared properly.

That said, five-minute stickiness for a passkey ceremony is not outrageous. It is a narrow operational compromise.

The cleaner long-term target is:

  • shared application sessions,
  • shared passkey challenge state,
  • shared cache behavior across nodes,
  • no dependency on a user staying pinned to one server.

ColdFusion’s passkey challenge store options include things like server cache, Ehcache, and in-process memory. For a load-balanced deployment, in-process memory is obviously the wrong answer. Ehcache is only appropriate if it is actually clustered. “Server cache” depends entirely on whether your ColdFusion server cache is backed by something shared across nodes.

That is the next thing I want to harden.

What finally worked

The working production shape was:

  • DatabasePasskey.cfc exposed through a non-CFIDE path: /__cf_passkey/DatabasePasskey.cfc/__cf_passkey/DatabasePasskey.cfc
  • No copied Adobe CFCs.
  • No broad public CFIDE exposure.
  • App webroot symlink to Adobe’s passkey directory.
  • ColdFusion connector routing for the specific passkey CFC.
  • passkey_credentials created by Adobe, then CREATE permission removed from the datasource user.
  • Short ALB stickiness so the page and Adobe passkey service call hit the same app node.

    Once those were in place, the passkey registration completed.

    Things I would like Adobe to document more clearly

    The feature itself is good. The documentation could use more production deployment guidance.

    Specifically:

    1. How should production apps expose DatabasePasskey.cfc without exposing all of CFIDE?

      Many production applications intentionally block CFIDE. That is normal and healthy.

    2. What exact table does DatabasePasskey.cfc create, and what permissions are required after creation?

      Production datasource users often do not have schema-level DDL permissions.

    3. Where is passkey challenge and CSRF state stored?

      This matters a lot in clustered or load-balanced deployments.

    4. Does native passkey registration require sticky sessions unless a specific shared cache configuration is used?

      If yes, say that clearly. If no, document the exact shared-cache configuration required.

    5. What is the recommended architecture for multiple ColdFusion nodes behind a load balancer?

      This is not an edge case. This is production.

    Final thoughts

    I am still happy ColdFusion has native passkey support. This is exactly the kind of feature the platform should provide.

    But production authentication features live in the messy intersection of application code, web server routing, datasource permissions, session storage, cache configuration, browser behavior, and load balancers. A clean demo is useful, but the production path needs to be explicit.

    The good news is that once the pieces were aligned, the passkey ceremony worked.

    The less-good news is that getting there involved /CFIDE, Apache rewrites, datasource DDL permissions, Adobe’s auto-created table, internal CSRF validation, node-local state, and ALB stickiness.

    So yes, native passkeys in ColdFusion 2025 are real… just bring a flashlight.

All Comments
Sort by:  Most Recent
2026-06-10 23:23:44

Hey, David. As always it’s great to see you diving into features and sharing your experience. And while I’d noticed that CF2025 update 8 had added the new passkey support, I had not yet explored it beyond cursory observation of the docs. As such, you already have more experience than me here.

 

Still, I have some info to share (some of which you asked about), and I also have some questions, that might even bring a different perspective on much of what you’ve shared.

 

1) First, it may help some readers following along if we point them to the docs that were added for this specific feature (again, only available since update 8 of CF2025–and only for CF2025, for now).

 

Next, it’s indeed true that the CFIDE path (within CF’s cfusion/wwwroot folder) is blocked by the CF web server configuration tool, and so should not be accessible via external web servers like IIS or Apache.  As some readers may recall, this has been so since CF2016, when the public cf_scripts folder within it was moved out of it and made a sibling (which IS accessible). Access to the CF admin (one of the remaining folders under CFIDE) was therefore blocked, acessible only via CF’s built-in web server (such as at port 8500).

 

2) But my first question is whether you found that the code you wrote really was leading to your browser to try to access those passkey CFCs. (It’s not clear if perhaps you were just trying to accessthem remotely on your own, such as using the CFC explorer feature, to get to know them.)

 

It’s also not clear from reading the docs and even the sample code that CF necessarily would end up having the browser code call back to the CFCs. I inferred from reading that it would instead by your CFML code that would be called, and then THAT code (running on the server) would be referring to instances of the CFCs, and that’s NOT restricted from our access (though of course things like the cfc’s in the adminapi folder within CFIDE WOULD be restricted from common use). 

 

But if you DID confirm that attempts were made from a browser running the code to access those CFCs remotely, then yes this is unfortunate to discover. It wouldn’t be TOO surprising that the engineers or testers of the feature might have been using only the built-in web server, which would NOT have blocked that access. But do let us know. (I don’t want to hold up asking this while hoping to find time to get the sample code and config going.)

 

3) Along the same lines, I can see how lamentable it would seem that they might only have the first use of the feature implicitly create the db tables. Indeed, you lamented not being able to know the db schema (tables, columns).

 

But here’s good news: while it’s not documented in the docs, the table schema IS documented within that databasepasskey.cfc. In fact it has comments showing the exact CREATE TABLE sql (for the different supported DBs), as well as sql to create indexes, etc. It even clarifies that the table name is selectable, including in code. I agree that the docs could have perhaps pointed both these things out. Might you be considering a tracker bug report on this concern, already? I don’t want to step on that effort.

 

4) You also lament the idea that people may be tempted to copy those CFCs, which I agree would be unwise. But note that the docs DO refer to instead EXTENDing those CFCs and overriding their methods as interfaces.  There aren’t too many CFML features that have us do that (the little-used event gateways were one).

 

5) You also ask where the challenge is stored. That is in fact a separate location. See the discussion in that docs page for “Challenge Store Configuration”. That’s what leverages the Admin setting (which can support storage that is global to all apps or not, and offers those caching options you mentioned, also configured in the admin.)

 

The docs also clarify how for a cluster, it would be recommended to use the external caching solutions like redis or memcached. If the multiple servers share the same cache, I don’t know that it would be critical to use stickiness…but you may have found an issue. FWIW, neither the docs linked to above nor the docs for configuring the Admin for this passkey support mentions stickiness at all. Again, if it proves necessary that would be a good thing to open a tracker ticket for.

 

6) Finally, to your last bulleted point I will note that the docs DO at least disucss SOME about choices (for using this passkey feature) when running in development vs production (and indeed a cluster). I don’t think it’s quite that they were ignoring the mattters, but as always it’s possible to improve things.

 

All that said, it’s not clear that anyone from Adobe will see and respond here, though they indeed may. Until then, I hope something I’ve offered might help you or other readers–and I of course welcome correction if I’ve erred.

Like