Whatever message this page gives is out now! Go check it out!

Passkeys in ColdFusion

Last update:
May 18, 2026
Add passwordless authentication to ColdFusion applications using the native WebAuthn and FIDO2 passkey APIs.

Introduction

Passkeys are a passwordless authentication method built on the WebAuthn and FIDO2 standards. Instead of passwords, users authenticate with biometrics (fingerprint, face), device PIN, or hardware security keys. ColdFusion 2025.0.08 adds native support for passkey registration and authentication. Credentials are unique per site, per domain, cannot be phished, and do not require remembering passwords.
ColdFusion passkeys provide:
  • Phishing resistance: Passkeys are bound to the domain they were created for.
  • No shared secrets: Private keys never leave the user's device.
  • Stronger security: Resistant to credential stuffing and phishing.
  • Better UX: No passwords to remember or type.

Problem statement

Traditional authentication has evolved through several phases:
  • Passwords: Easy to forget, reuse, or leak.
  • SAML: Works for federated identity but requires XML and identity provider setup.
  • OAuth/JWT: Good for third-party identity but requires user accounts on external platforms.
  • 2FA/3FA : Adds security but adds friction and complexity.
These methods work well for corporate or federated scenarios. For retail and customer-facing applications, users often cannot or should not be required to follow complex identity flows.
Passkeys address this by letting users sign in with cryptographic keys stored on their device. Credentials are unique per site, cannot be phished, and do not require remembering passwords. ColdFusion 2025 adds native APIs so you can implement passkeys without building WebAuthn from scratch.

Use cases

Audiences and corresponding use cases for passkey authentication.
AudienceUse case
Development teamsAdd passwordless authentication for new or existing ColdFusion apps.
Retail and customer-facing appsSimplified sign-in for users without corporate SSO.
Enterprise appsOffer passkeys as an alternative or replacement for passwords.
Compliance teamsMeet security requirements (for example, phishing-resistant MFA).
End usersSign in with biometrics or PIN instead of passwords.

Prerequisites

  • ColdFusion version: ColdFusion 2025.0.08 or later.
  • Browser support: Chrome, Firefox, Safari, Edge (latest versions with WebAuthn).
  • User device: Biometric sensor, device PIN, or hardware security key (for example, USB key).

How passkeys work

Passkeys broadly have two workflows:
  • Registration: A keypair is created. The private key stays with the authenticator. This can be a Credential Manager on the device, a cloud authenticator hosted in the cloud, or an authenticator using a hardware device that can be plugged in. The public key is sent to ColdFusion and stored.
  • Authentication: The server sends a challenge. The user signs it with the private key. The server verifies the signature with the public key.
Two credential models are supported:
Discoverable credentials and user ID driven credentials differ in how the browser selects a passkey.
ModelDescription
Discoverable credentialsAll passkeys for the site are available. Omit username when calling PasskeyAuthenticate. The user chooses which account to sign in with.
User ID driven credentialsPasskeys for a specific user are only available. Pass username when calling PasskeyAuthenticate to restrict the credential lookup.
Passkeys created for one domain are not usable on another. The rpId (Relying Party ID) binds the credential to your domain.

Workflows

Workflow 1: Registration (associate passkey with account)
  1. User is on an authenticated page (for example, after password login or account creation).
  2. Call PasskeyRegister(userStruct, configStruct) with username and config.
  3. Call CFPasskey.startRegistration() in JavaScript to initiate the registration workflow.
  4. Browser prompts user to create passkey (biometric, PIN, or security key).
  5. User completes the ceremony.
  6. Server stores the public key.
  7. User is redirected to redirectUrl with ?passkey_token=xxx if redirectUrl is specified.
  8. Callback page calls PasskeyGetResult(token) and handles success.
Workflow 2: Authentication (sign in with passkey)
  1. User visits the login page.
  2. Call PasskeyAuthenticate(userStruct, configStruct).
  3. Call CFPasskey.startAuthentication() in JavaScript.
  4. Browser prompts user to select and authenticate with passkey.
  5. User completes the ceremony.
  6. Server verifies the signature.
  7. User is redirected to redirectUrl with ?passkey_token=xxx.
  8. Callback page calls PasskeyGetResult(token) and logs the user in (for example, via <cflogin>).
Workflow 3: Discoverable credentials (no username hint)
  1. Omit username in userStruct when calling PasskeyAuthenticate.
  2. Browser shows all passkeys registered for the site.
  3. User selects which account to sign in with.
  4. Authentication proceeds as in Workflow 2.
Workflow 4: User ID driven credentials (username hint)
  1. Pass username in userStruct when calling PasskeyAuthenticate.
  2. Browser shows only passkeys for that user.
  3. User authenticates with the selected passkey.
  4. Authentication proceeds as in Workflow 2.
Workflow 5: Registration plus immediate authentication
  1. User completes registration.
  2. Callback page receives result.action == "registration".
  3. Redirect user to the authentication page.
  4. User can sign in immediately with the new passkey.
Workflow 6: Hybrid (password and passkey)
  1. User signs in with password for the first time.
  2. After successful login, prompt user to register a passkey.
  3. User completes registration.
  4. Next time, user can choose sign in with passkey or password.

Quick start

Minimal registration
<cfscript>
    userStruct = { name: "user@example.com" };
    configStruct = {
        successHandler: "onRegisterSuccess",
        errorHandler:   "onRegisterError",
        redirectUrl:    "/callback.cfm",
        service:        "/CFIDE/passkey/DefaultPasskey.cfc"
    };
    PasskeyRegister(userStruct, configStruct);
</cfscript>

<script>
function onRegisterSuccess(data, serverResult) { /* handle success */ }
function onRegisterError(errorData) { /* handle error */ }

function startRegistrationWhenReady() {
    if (typeof CFPasskey !== 'undefined'
        && typeof CFPasskey.startRegistration === 'function') {
        CFPasskey.startRegistration();
    } else { setTimeout(startRegistrationWhenReady, 200); }
}
document.addEventListener('DOMContentLoaded', function() {
    setTimeout(startRegistrationWhenReady, 200);
});
</script>
Callback page (retrieve result)
<cfparam name="url.passkey_token" default="">
<cfif len(trim(url.passkey_token))>
    <cfset result = PasskeyGetResult(url.passkey_token)>
    <cfif result.success>
        <cfif result.action == "registration">
            <!--- Registration complete.
                  result.username, result.credentialId available.
                  Typical next step: redirect to the sign-in page. --->
        <cfelseif result.action == "authentication">
            <!--- Authentication complete. Establish a session here
                  (see Integration with cflogin below). --->
        </cfif>
    </cfif>
</cfif>
Always branch on result.action before acting on the result. The same callback URL receives both registration and authentication tokens, and treating one as the other (for example, calling <cfloginuser> on a registration result) creates a subtle account-takeover risk.
Minimal authentication
<cfscript>
    userStruct = { rpId: cgi.server_name };
    configStruct = {
        successHandler: "onAuthSuccess",
        errorHandler:   "onAuthError",
        redirectUrl:    "/callback.cfm",
        service:        "/CFIDE/passkey/DefaultPasskey.cfc"
    };
    PasskeyAuthenticate(userStruct, configStruct);
</cfscript>

<script>
function onAuthSuccess(assertionData, serverResult) { /* handle success */ }
function onAuthError(errorData) { /* handle error */ }

function startAuthWhenReady() {
    if (typeof CFPasskey !== 'undefined'
        && typeof CFPasskey.startAuthentication === 'function') {
        CFPasskey.startAuthentication();
    } else { setTimeout(startAuthWhenReady, 200); }
}
document.addEventListener('DOMContentLoaded', function() {
    setTimeout(startAuthWhenReady, 200);
});
</script>

API reference

PasskeyRegister(userStruct, configStruct)

Initiates passkey registration. The browser prompts the user to create a passkey (biometric, PIN, or security key).
userStruct parameters
Parameters accepted by userStruct for PasskeyRegister.
ParameterTypeRequiredDescription
namestringYesUsername for registration (for example, email).
displayNamestringNoHuman-readable display name.
idstringNoUnique user identifier (auto-generated if omitted).
rpNamestringNoRelying Party name (your app name).
rpIdstringNoRelying Party ID (defaults to cgi.server_name).
userVerificationstringNo"required", "preferred", "discouraged".
authenticatorAttachmentstringNo"platform" (Touch ID, Windows Hello) or "cross-platform" (USB key).
requireResidentKeybooleanNoWhether to require resident key.
attestationstringNo"none", "indirect", "direct", "enterprise".
configStruct parameters
Parameters accepted by configStruct for PasskeyRegister.
ParameterTypeRequiredDescription
successHandlerstringNoJavaScript function name for success callback.
errorHandlerstringNoJavaScript function name for error callback.
redirectUrlstringNoPath with leading / (for example, /callback.cfm) or full http(s) URL.
servicestringNoCFC path for credential storage (default: CFIDE/passkey/DefaultPasskey.cfc).

PasskeyAuthenticate(userStruct, configStruct)

Initiates passkey authentication. The browser prompts the user to authenticate with an existing passkey.
userStruct parameters
Parameters accepted by userStruct for PasskeyAuthenticate.
ParameterTypeRequiredDescription
usernamestringNoUsername hint for credential lookup (omit for discoverable credentials).
rpIdstringNoRelying Party ID (defaults to cgi.server_name).
userVerificationstringNo"required", "preferred", "discouraged".
configStruct parameters
Parameters accepted by configStruct for PasskeyAuthenticate.
ParameterTypeRequiredDescription
successHandlerstringNoJavaScript function name for success callback.
errorHandlerstringNoJavaScript function name for error callback.
redirectUrlstringNoPath with leading / (for example, /callback.cfm) or full http(s) URL.
servicestringNoCFC path for credential storage (default: CFIDE/passkey/DefaultPasskey.cfc).
Note: The default CFIDE/passkey/DefaultPasskey.cfc writes credentials to JSON files on the local server and is intended for development and single-server use only. Production and clustered deployments should override service to /CFIDE/passkey/DatabasePasskey.cfc. The Quick Start examples use the file-based default purely to keep the snippets self-contained. Do not copy them straight into production without changing the service path.

PasskeyGetResult(token)

Retrieves the result of a completed registration or authentication. Call this on your callback page when the user is redirected with ?passkey_token=xxx.
Returns
Keys returned by PasskeyGetResult.
KeyTypeDescription
successbooleanWhether the operation succeeded.
actionstring"registration" or "authentication".
usernamestringUsername (for successful operations).
credentialIdstringCredential ID (for successful operations).
userIdstringUser ID (for successful operations).
messagestringError message (if failed).
Note: Tokens are single-use. After retrieval, the token is invalidated. Tokens expire. Use them promptly.

Backend storage

Passkey public keys must be stored server-side. ColdFusion provides two backends.
DefaultPasskey (file-based)
  • Path: /CFIDE/passkey/DefaultPasskey.cfc
  • Storage: JSON files in {cf-root}/passkeys/
  • Use case: Development, single-server, low traffic.
  • Config: No configuration required.
configStruct.service = "/CFIDE/passkey/DefaultPasskey.cfc";
DatabasePasskey (database-based)
  • Path: /CFIDE/passkey/DatabasePasskey.cfc
  • Storage: Relational database via CF datasource.
  • Use case: Production, multi-server, clustered environments.
  • Config: Set the datasource via ColdFusion Administrator > Security > Settings (preferred), or use the Admin API: security.setPasskeyConfig({datasource: "maria"}). Do not set variables.datasourceName directly inside DatabasePasskey.cfc. That file is overwritten on upgrade.
configStruct.service = "/CFIDE/passkey/DatabasePasskey.cfc";
Supported databases: SQL Server, PostgreSQL, MySQL/MariaDB, Oracle, DB2, Informix, Apache Derby. The table passkey_credentials is auto-created on first use.

Challenge store configuration

WebAuthn challenges are stored temporarily. Configure via CF Admin > Server Settings > Passkey/WebAuthn or the Admin API.
Configurable challenge store settings and their defaults.
SettingOptionsDefault
Challenge storememory, ehcache, servercache memory
Challenge timeout30–600 seconds60
Admin API (requires Admin login):
adminObj  = createObject("component", "CFIDE.adminapi.administrator");
adminObj.login("admin-password");

security  = createObject("component", "CFIDE.adminapi.security");
config    = security.getPasskeyConfig();

security.setPasskeyConfig({
    challengeStore : "servercache",
    challengeTtl   : 120,
    datasource     : "maria"
});

JavaScript callbacks

The SDK (cfpasskey.js) is loaded automatically. You must call CFPasskey.startRegistration() or CFPasskey.startAuthentication() to begin the flow.
Registration callbacks
function onRegisterSuccess(data, serverResult) {
    // data:         credential object from WebAuthn
    // serverResult: server response
}

function onRegisterError(errorData) {
    // errorData.error:        message
    // errorData.code:         NOT_SUPPORTED | SAVE_FAILED | NOT_LOADED
    // errorData.serverResult: optional server response
}
Authentication callbacks
function onAuthSuccess(assertionData, serverResult) {
    // assertionData: assertion from WebAuthn
    // serverResult:  server response
}

function onAuthError(errorData) {
    // errorData.error:        message
    // errorData.code:         NOT_SUPPORTED | AUTH_FAILED | NOT_LOADED
    // errorData.serverResult: optional server response
}
Manual redirect (when redirectUrl is empty)
If you handle the redirect in JavaScript, prefer reading the token from serverResult (the public callback argument) rather than from CFPasskey._config. The leading underscore on _config marks it as a private SDK field and is not part of the supported API surface. It can change between releases.
function onRegisterSuccess(data, serverResult) {
    // Preferred: read the token from the server response payload.
    // The exact field name depends on your backend's response shape;
    // DefaultPasskey and DatabasePasskey both return it as `csrfToken`.
    var token = (serverResult && serverResult.csrfToken)
                || (serverResult && serverResult.token);

    if (!token) {
        console.error("No passkey token returned by server");
        return;
    }
    window.location.href = "/callback.cfm?passkey_token="
                         + encodeURIComponent(token);
}

// Fallback (not recommended): CFPasskey._config.csrfToken is a private
// SDK field. It works today but may change without notice.
// var token = CFPasskey._config.csrfToken;

Integration with cflogin

After successful authentication, call PasskeyGetResult() and then use <cflogin> to establish the session. The password attribute is set to an empty string because passkey authentication does not use a password. The cryptographic ceremony has already verified the user's identity. The idletimeout and applicationtoken attributes for <cflogin> are inherited from <cfapplication>.
<cfparam name="url.passkey_token" default="">
<cfif len(trim(url.passkey_token))>
    <cfset result = PasskeyGetResult(url.passkey_token)>
    <cfif result.success && result.action == "authentication">
        <!--- Look up the user's actual roles from your user store --->
        <cfset userRoles = getUserRoles(result.username)>
        <cflogin>
            <cfloginuser
                name="#result.username#"
                password=""
                roles="#userRoles#">
        </cflogin>
        <cflocation url="/dashboard.cfm" addtoken="false">
    </cfif>
</cfif>

Complete callback handling

Handle both registration and authentication on a single callback page:
<cfparam name="url.passkey_token" default="">

<cfif structKeyExists(url, "passkey_token") && len(trim(url.passkey_token))>
    <cfset result = PasskeyGetResult(url.passkey_token)>
    <cfif result.success>
        <cfif result.action == "authentication">
            <cfset userRoles = getUserRoles(result.username)>
            <cflogin>
                <cfloginuser name="#result.username#"
                             password=""
                             roles="#userRoles#">
            </cflogin>
            <cflocation url="/dashboard.cfm" addtoken="false">
        <cfelseif result.action == "registration">
            <cfset session.passkeyRegistered = true>
            <cflocation url="/passkey-authenticate.cfm" addtoken="false">
        </cfif>
    <cfelse>
        <!--- Token invalid, expired, or already consumed --->
        <cfset session.passkeyError = result.message>
        <cflocation url="/login.cfm" addtoken="false">
    </cfif>
<cfelse>
    <!--- No token: user landed here without completing a passkey flow --->
    <cflocation url="/login.cfm" addtoken="false">
</cfif>

Custom passkey service

You can implement a custom CFC to store passkeys in your own backend. Extend CFIDE/passkey/IPasskey.cfc and override the following abstract methods:
Abstract methods that a custom passkey service must override.
MethodSignature
savePublicKeyremote struct savePublicKey(struct credentialData, string csrfToken)
verifyPasskeySignatureremote struct verifyPasskeySignature(struct assertionData, string csrfToken)
getPasskeysByUserarray getPasskeysByUser(string userId)
getPasskeysByUsernamearray getPasskeysByUsername(string username)
deletePasskeystruct deletePasskey(string userId)
deletePasskeysstruct deletePasskeys(string userId)
The interface also provides the following default helpers that your custom CFC inherits and can rely on. There is no need to reimplement them:
  • validateRegistrationResponse
  • validateAssertionData
  • decodeClientDataJSON
  • validateCsrfToken
  • sanitizeKey
  • getChallengeStore
  • getChallengeTtl
Use DefaultPasskey.cfc or DatabasePasskey.cfc as a reference implementation.

Error handling

Common error codes (JavaScript)
JavaScript error codes returned by passkey registration and authentication callbacks.
CodeDescriptionApplies to
NOT_SUPPORTEDBrowser or device does not support WebAuthn.Registration and authentication
NOT_LOADEDSDK not loaded when start was called.Registration and authentication
SAVE_FAILEDServer failed to save the credential.Registration only
AUTH_FAILEDAuthentication failed. User may have canceled, selected incorrect passkey, or deleted a public key.Authentication only

Security considerations

  • Private keys never leave the user's device. ColdFusion only stores public keys.
  • CSRF protection is built-in. The SDK uses tokens for state validation.
  • Path traversal is prevented in file-based storage.
  • Parameterized queries are used in database storage (SQL injection protection).
  • Signature counter prevents replay attacks.

Best practices

  • Use HTTPS in production. WebAuthn requires a secure context.
  • Use DatabasePasskey in production. File-based storage is for development and single-server only.
  • Set rpId explicitly. Use cgi.server_name or your domain. Do not derive it from user input.
  • Validate redirectUrl. Use fixed paths or allowlisted URLs. Never use user-supplied redirect URLs.
  • Handle errors gracefully. Show clear messages for NOT_SUPPORTED, AUTH_FAILED, and SAVE_FAILED.
  • Wait for SDK load. Use a retry loop or DOMContentLoaded before calling startRegistration() or startAuthentication().
  • Branch on result.action in callback pages. A single callback URL receives both registration and authentication tokens. Always verify the action before establishing a session.
  • Use a cache that supports clustered configuration, like Redis or ehcache in cluster mode.
  • Offer both passkey and password. During migration, let users choose their preferred method.

Vulnerabilities and mitigations

Common authentication risks and their mitigations in ColdFusion passkeys.
RiskMitigation
PhishingPasskeys are bound to the domain they were created for. Users cannot be tricked into entering credentials on a fake site.
Credential theftPrivate keys never leave the device. There is no password to steal.
Replay attacksWebAuthn uses challenge-response. Each authentication uses a unique challenge. Signature counter prevents replay.
Session fixationCall sessionRotate() after passkey authentication to prevent session fixation.
CSRFUse the built-in CSRF tokens. Do not bypass or override them.
rpId mismatchEnsure rpId matches your domain. Do not use rpId from user input.
Redirect URL injectionUse fixed paths or allowlisted URLs for redirectUrl. Do not use user input.
Token reuseTokens are single-use. Do not reuse them after PasskeyGetResult().
Insecure storageUse DatabasePasskey in production. Avoid file-based storage for multi-server deployments.

Troubleshooting

Common passkey issues and how to resolve them.
IssuePossible solution
"Passkey not available" or NOT_LOADEDSDK may not be loaded. Wait for CFPasskey to be defined before calling startRegistration() or startAuthentication(). Use a retry loop or DOMContentLoaded with a short delay.
NOT_SUPPORTEDBrowser or device does not support WebAuthn. Ensure HTTPS is used (or localhost). Check browser compatibility.
SAVE_FAILED
Server failed to save the public key. Check storage backend (file permissions, datasource, or DatabasePasskey config).
AUTH_FAILEDAuthentication failed. User may have cancelled, wrong passkey selected, or credential was deleted.
Invalid token or expiredToken is single-use and expires. Use it promptly after redirect. Do not refresh or bookmark the callback URL.
Invalid rpIdrpId must be a valid hostname without protocol. Use cgi.server_name or your domain.
Invalid redirectUrlUse a path starting with / (for example, /callback.cfm) or a full http(s) URL. Do not use javascript: or other schemes.
PasskeyConfigurationExceptionFor DatabasePasskey, configure the datasource via ColdFusion Administrator > Security > Settings or security.setPasskeyConfig({datasource: "maria"}). Ensure the datasource exists and is configured.
Challenge timeoutIncrease the challenge timeout in CF Admin or via the Admin API. Valid range is 30–600 seconds. Default is 60 seconds.

Share this page

Was this page helpful?
We're glad. Tell us how this page helped.
We're sorry. Can you tell us what didn't work for you?
Thank you for your feedback. Your response will help improve this page.

On this page