Les clés d’accès natives dans Adobe ColdFusion 2025 - Partie 1 : Enregistrer votre première clé d’accès

Parce que les mots de passe ont assez souffert

Je construis des systèmes d'authentification depuis que HTML a introduit les formulaires dans HTML 2. Les mots de passe. Les liens de réinitialisation de mot de passe. Les questions de sécurité. Les codes à usage unique. L'authentification multifacteur. Les verrouillages de compte. Les règles de complexité des mots de passe, soigneusement conçues pour garantir que chaque utilisateur finisse par choisir Winter2026!.

Nous avons passé des décennies à essayer de rendre les mots de passe plus sécuritaires. Surtout en les rendant plus agaçants.

Les clés d'accès offrent un meilleur modèle. L'utilisateur s'authentifie à l'aide d'une information d'identification détenue par son appareil ou son gestionnaire de mots de passe. La clé privée n'atteint jamais notre application. Nous n'avons aucun mot de passe à hacher, à divulguer, à réinitialiser, à consigner accidentellement ou à découvrir dans un tableur nommé passwords-final-FINAL.xlsx.

Une clé d'accès est une information d'identification cryptographique qui permet à un appareil de prouver que la personne qui l'utilise contrôle un compte inscrit.

Lorsqu'une clé d'accès est créée, l'appareil génère une paire de clés publique et privée. L'application stocke la clé publique. La clé privée reste sur l'appareil ou dans le gestionnaire de mots de passe de confiance de l'utilisateur.

Pendant la connexion, l'application envoie un défi unique. L'appareil demande à l'utilisateur d'approuver la demande à l'aide de la reconnaissance faciale, de la reconnaissance d'empreinte digitale, de Windows Hello, d'un numéro d'identification personnel ou d'une autre méthode de vérification locale. Il utilise ensuite la clé privée pour signer le défi. L'application vérifie cette signature à l'aide de la clé publique qu'elle possède déjà.

Les données biométriques ne sont jamais envoyées à l'application. Nous ne recevons pas d'empreinte digitale, de scan facial ni même le numéro d'identification personnel de l'appareil. L'appareil nous indique simplement, de manière cryptographique, que la personne qui détient l'information d'identification a satisfait à ses exigences de sécurité locales.

Sur les appareils pris en charge, les clés privées et les opérations de signature peuvent être protégées par du matériel comme Secure Enclave d'Apple, un Trusted Platform Module ou un environnement sécurisé similaire. Les clés d'accès synchronisées peuvent également être protégées et transférées par l'intermédiaire d'un écosystème de gestionnaires de mots de passe chiffré de bout en bout.

L'application ne reçoit pas le message suivant : « Cette empreinte digitale appartient à David. » Elle reçoit plutôt : « La personne qui utilise cet appareil a déverrouillé avec succès l'information d'identification enregistrée pour ce site web, et voici la preuve mathématique. »

C'est à la fois plus privé et considérablement plus difficile à falsifier que la plupart des mots de passe créés par les utilisateurs.

ColdFusion 2025 inclut maintenant des fonctions natives de clés d'accès :

passkeyRegister()
passkeyAuthenticate()
passkeyGetResult()

Cela signifie que nous pouvons ajouter des clés d'accès sans avoir à implémenter nous-mêmes le protocole Web Authentication. J'aime apprendre comment les choses fonctionnent, mais je n'ai aucune envie d'analyser personnellement les données de l'authentificateur, de valider les hachages des données client et de construire un décodeur de Concise Binary Object Representation tout en marmonnant : « Je suis sûr que tout va bien. »

ColdFusion rend les choses difficiles faciles.

Dans cette série en quatre parties, je vais créer une implémentation complète des clés d'accès. Je commencerai par l'inscription, j'ajouterai l'authentification, je déplacerai la cérémonie vers une origine d'authentification centrale et, éventuellement, je ferai en sorte que le tout résiste aux proxys inverses et à plusieurs nœuds ColdFusion.

Faites-moi confiance... C'est dans cette dernière partie que l'espoir meurt.

Pour l'instant, nous allons garder les choses simples :

  • Une application ColdFusion
  • Un nom d'hôte
  • Une instance ColdFusion
  • Un utilisateur authentifié
  • Un flux d'inscription de clé d'accès

À la fin de cet article, un utilisateur existant pourra ajouter une clé d'accès à son compte. Point final.

Ce que ColdFusion fait pour nous

Une cérémonie d'inscription Web Authentication comporte plusieurs éléments mobiles. Le serveur crée un défi. Le navigateur transmet ce défi à l'authentificateur. L'authentificateur crée une information d'identification et signe la réponse. Le serveur valide la réponse et stocke l'information d'identification.

Conceptuellement, notre flux ressemble à ceci :

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 gère le travail propre au protocole :

  • Créer le défi Web Authentication
  • Appeler les interfaces de navigateur pour les informations d'identification
  • Valider la réponse de l'authentificateur
  • Stocker l'information d'identification
  • Retourner à notre rappel un jeton de résultat à courte durée de vie

Notre application demeure responsable des décisions propres à l'application :

  • Quel utilisateur inscrit la clé d'accès
  • Quel identifiant de partie de confiance nous approuvons
  • Où le rappel est autorisé à aller
  • Si le résultat de l'inscription appartient à l'utilisateur attendu
  • Ce qui se passe après la réussite ou l'échec de l'inscription

C'est une distinction importante. ColdFusion prouve que la cérémonie de clé d'accès a réussi, mais il ne décide pas si la personne qui lance cette cérémonie devrait être autorisée à lier une information d'identification à un compte particulier.

Cette partie-là reste notre problème. Les systèmes d'authentification sont généreux de cette façon.

Avant d'écrire du code

Vous avez besoin d'une installation complètement mise à jour de ColdFusion 2025 qui expose les trois fonctions natives de clés d'accès.

Ne liez pas de façon permanente votre application à un numéro de mise à jour précis. Les mises à jour de ColdFusion continuent d'évoluer, et les correctifs de sécurité ont la fâcheuse habitude de devenir importants immédiatement après que vous avez reporté leur installation.

Nous pouvons vérifier les fonctions directement :

<cfscript>
    functions = getFunctionList();

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

    writeDump( passkeys_supported );
</cfscript>

Vous aurez également besoin de :

  • Gestion des sessions activée
  • Une source de données configurée
  • Un nom d'hôte stable utilisant HTTPS
  • L'accès à l'Administrateur ColdFusion
  • Un utilisateur authentifié existant

L'authentification Web dépend d'un contexte de navigateur sécurisé. En production, cela signifie HTTPS. Le développement local peut parfois utiliser des exceptions localhost prises en charge par le navigateur, mais un vrai nom d'hôte avec une connexion chiffrée réduira les occasions de passer une soirée à blâmer Safari pour quelque chose que vous avez mal configuré.

Demandez-moi comment je le sais.

En fait, ne le faites pas.

Configuration du backend DatabasePasskey

ColdFusion fournit un backend DatabasePasskey qui stocke les identifiants dans une source de données ColdFusion. La configuration des passkeys relève de ColdFusion. Ce n'est pas un paramètre de Application.cfc.

Le script ponctuel suivant le configure :

<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>

Levez maintenant la main droite et répétez après moi.

« Je ne mettrai pas le mot de passe de mon administrateur ColdFusion dans le contrôle de source. Je ne laisserai pas ce script accessible publiquement. Je ne lui donnerai pas le nom configure-passkeys-public-do-not-run.cfm en espérant que les attaquants respecteront le nom du fichier. Je l'exécuterai uniquement par un processus administratif protégé, je confirmerai la configuration et je le retirerai de la racine Web. »

Pour ce premier article, nous utilisons le magasin de défis memory parce que nous n'avons qu'une instance ColdFusion. Un défi créé sur cette instance sera complété sur la même instance.

Cela deviendra insuffisant lorsque nous ajouterons plusieurs nœuds. Nous traiterons des caches partagés et de la persistance de session derrière le répartiteur de charge dans la quatrième partie, après avoir développé assez de résilience émotionnelle pour gérer les cicatrices que cela causera.

Le backend de ColdFusion crée et gère sa propre table d'identifiants dans la source de données configurée. Traitez cette table comme appartenant au moteur. Notre application utilisera les fonctions natives plutôt que d'essayer d'insérer elle-même des lignes d'identifiants.

Garder le service Passkey à l'intérieur de l'application

Le code passkey côté navigateur de ColdFusion envoie une requête HTTP à DatabasePasskey.cfc pendant la cérémonie. Cette requête doit s'exécuter dans la même application et la même session qui a lancé l'enregistrement.

Cela compte, parce que l'implémentation des passkeys de ColdFusion comprend une protection liée à la session contre la falsification de requêtes intersites. Si la requête du service aboutit dans une autre application ColdFusion ou dans un autre moteur CommandBox, elle reçoit une session différente. Le point de terminaison existe, le navigateur y accède, et la cérémonie échoue quand même avec CSRF_INVALID.

Rappelez-vous... nous construisons ici un système d'authentification. La sécurité est primordiale.

Ce sont mes échecs préférés : tout fonctionne techniquement, sauf la partie qui intéresse qui que ce soit.

Adobe installe le service ici :

/CFIDE/passkey/DatabasePasskey.cfc

La documentation d'Adobe indique aussi au navigateur d'y accéder par ce chemin.

Cela soulève un petit désaccord philosophique entre deux conseils par ailleurs sensés :

  1. Le service passkey d'Adobe doit être accessible par le navigateur.
  2. Le répertoire de service interne de ColdFusion ne devrait pas être accessible depuis Internet.

Les deux sont vrais, ce qui est toujours pratique.

Ma solution de contournement consiste à bloquer /CFIDE tout en exposant un alias local à l'application :

/__cf_passkey/DatabasePasskey.cfc

Sur Linux, cet alias peut être créé en liant la racine Web de l'application au répertoire passkey appartenant au même moteur ColdFusion :

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

Une installation Adobe conventionnelle et une installation gérée par CommandBox ne placeront pas nécessairement ces fichiers au même endroit. Un serveur exécutant plusieurs instances CommandBox peut également avoir plusieurs copies. Le lien symbolique doit pointer vers le répertoire CFIDE/passkey appartenant au moteur qui sert cette application.

Le serveur Web doit ensuite acheminer cette adresse exacte vers ColdFusion :

/__cf_passkey/DatabasePasskey.cfc

Toute autre adresse sous /__cf_passkey devrait être refusée. Le lien symbolique cible un répertoire parce que DatabasePasskey.cfc hérite du code des composants ColdFusion voisins, mais ces fichiers voisins n'ont pas besoin d'être accessibles via le protocole Hypertext Transfer Protocol.

Pour être parfaitement clair, il s'agit d'une solution de contournement, pas d'une bonne pratique que je reproduirais enthousiasmement ailleurs. Elle rend publiquement accessible un composant ColdFusion distant fourni par Adobe parce que le logiciel de navigateur d'Adobe en a besoin. Le composant valide la protection d'Adobe liée à la session contre la falsification de requêtes intersites et la cérémonie d'authentification Web, donc ce n'est pas un point de terminaison de base de données sans protection. Cela ne fait pas magiquement de l'exposition de code géré par le fournisseur depuis /CFIDE une architecture élégante.

N'effectuez pas de copie du composant ColdFusion dans votre application. Une version copiée prendra discrètement du retard par rapport aux mises à jour de sécurité de ColdFusion, devenant éventuellement une relique archéologique avec accès réseau. Faites un lien vers la version gérée par le moteur, exposez uniquement l'adresse requise et vérifiez la configuration après chaque mise à jour de ColdFusion.

La solution la plus propre doit venir d'Adobe - un point de terminaison de service local à l'application pris en charge qui s'exécute dans l'application et la session qui l'ont initié, sans nécessiter l'exposition d'une quelconque partie de /CFIDE.

En attendant, c'est le pont le moins mauvais entre l'implémentation d'Adobe et un serveur verrouillé. Le routage par proxy inverse mérite plus d'attention que cette configuration raccourcie, alors nous lui consacrerons un traitement complet dans la quatrième partie.

Créer un service de clé d'accès

Je préfère garder les appels natifs de ColdFusion derrière un petit composant.

Pouvons-nous appeler passkeyRegister() directement depuis chaque contrôleur et chaque modèle? Oui. Nous pourrions aussi disperser les identifiants de partie de confiance, les chemins de rappel et les adresses de service dans toute l'application jusqu'à ce que modifier l'un d'eux ressemble à désamorcer une bombe assemblée par notre ancien moi.

J'ai suffisamment été cet ancien moi dans le passé pour ne pas recommencer.

Créez 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"
        );
    }

}

Ce composant centralise quatre décisions :

  • Si le moteur installé prend en charge les clés d'accès natives
  • Comment notre application identifie l'utilisateur
  • Quelle configuration de partie de confiance nous envoyons à ColdFusion
  • Comment nous normalisons le résultat renvoyé par ColdFusion

L'identifiant de la partie de confiance est fourni délibérément lors de la création du composant. Il ne provient ni d'un paramètre d'adresse, ni d'un champ de formulaire, ni de l'en-tête host qui se présente à la requête.

Le serveur décide de quelle partie de confiance il s'agit.

Un navigateur ne devrait pas pouvoir envoyer :

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

et convaincre notre application de hausser les épaules et de s'en accommoder.

Comprendre la structure de l'utilisateur d'inscription

La structure transmise à passkeyRegister() contient à la fois les renseignements sur l'utilisateur et les options d'authentification Web :

{
    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"
}

Les propriétés abrégées rpName et rpId sont requises par la fonction native de ColdFusion. Nous pouvons expliquer les termes quand nous en parlerons, mais la structure elle-même doit utiliser les noms de propriétés que ColdFusion attend.

La valeur id mérite une attention particulière.

Utilisez un identifiant interne stable pour l'utilisateur. N'utilisez pas un nom d'affichage modifiable. J'évite aussi d'utiliser l'adresse courriel comme identifiant parce que les adresses courriel changent, sont corrigées, sont parfois partagées et il arrive parfois qu'elles aient été saisies par quelqu'un dont le rapport à l'orthographe est essentiellement théorique.

L'exemple utilise un identifiant universellement unique.

Les autres valeurs contrôlent la cérémonie du navigateur :

  • userVerification: "preferred" demande la vérification de l'utilisateur lorsque c'est possible.
  • authenticatorAttachment: "platform" privilégie un authentificateur intégré à l'appareil actuel.
  • attestation: "none" évite de demander à l'authentificateur des renseignements d'attestation permettant de l'identifier.

Si vous voulez permettre des clés de sécurité itinérantes, vous pouvez choisir d'omettre ou de modifier authenticatorAttachment. Le bon choix dépend des utilisateurs et du modèle de sécurité de votre application.

Pour cette implémentation, nous commençons avec des authentificateurs de plateforme - reconnaissance faciale, reconnaissance d'empreinte digitale, Windows Hello et autres identifiants semblables liés à l'appareil.

Initialiser le service

Créez le service une seule fois pendant le démarrage de l'application.

Dans 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;
    }

}

Modifiez ceci pour qu'il corresponde aux renseignements de votre application. L'identifiant de la partie de confiance doit correspondre à l'origine où le navigateur effectue la cérémonie.

Nous rendrons éventuellement cela plus souple, mais la souplesse n'est pas toujours notre amie lors de la première implémentation sensible à la sécurité.

Parfois, la souplesse n'est que de l'ambiguïté habillée en tenue décontractée d'affaires.

Commencer l'inscription

Le point de terminaison d'inscription doit exiger un utilisateur authentifié. Ce n'est pas un point de terminaison d'inscription de compte. Il ajoute une passkey à un compte qui a déjà été authentifié au moyen d'une autre méthode de confiance.

Créez 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>

L'enregistrement d'inscription en attente lie la cérémonie à l'utilisateur qui l'a lancée.

Nous ne mettons pas le jeton d'identifiants, le défi ou le jeton de résultat dans la session. ColdFusion gère l'état de la cérémonie. Nous ne conservons que suffisamment d'état applicatif pour répondre à une question lorsque le rappel arrive :

Is this result for the same user who started registration?

L'entrée de journal évite aussi d'extraire l'objet d'exception complet. Les erreurs d'authentification ont la fâcheuse tendance à contenir exactement l'information que nous ne devrions pas conserver dans un fichier journal jusqu'à la mort thermique de l'univers.

Appeler passkeyRegister() injecte dans la réponse la configuration de passkey de ColdFusion et le logiciel côté navigateur.

Commencer la cérémonie du navigateur

Notre page doit encore lancer la cérémonie d'inscription.

Créez includes/passkey-ceremony.cfm :

<div id="passkey-status" role="status">
    En attente de votre appareil…
</div>

<div id="passkey-error" role="alert" hidden>
    Nous n’avons pas pu enregistrer cette clé d’accès.
</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(
                    "Ce navigateur ne prend pas en charge les clés d’accès."
                );

                return;
            }

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

                if (attempts > 60) {
                    fail(
                        "Le service de clé d’accès est indisponible."
                    );

                    return;
                }

                window.setTimeout(
                    startRegistration,
                    200
                );

                return;
            }

            if (
                typeof window.CFPasskey.startRegistration
                !== "function"
            ) {
                fail(
                    "L’enregistrement de la clé d’accès est indisponible."
                );

                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(
                                    "L’enregistrement de la clé d’accès a été annulé."
                                );

                                return;
                            }

                            fail(
                                "Nous n’avons pas pu enregistrer cette clé d’accès."
                            );
                        }
                    );
                }
            }
            catch (reason) {
                fail(
                    "Nous n’avons pas pu enregistrer cette clé d’accès."
                );
            }
        }

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

Pourquoi interroger CFPasskey au lieu de l’appeler immédiatement?

La fonction native injecte et initialise le code côté navigateur de ColdFusion dans la réponse. Selon la façon dont la page est assemblée, notre script peut s’exécuter avant que cette initialisation soit terminée.

Je pensais au départ que l’objet m’attendrait toujours.

Les ordinateurs aiment la confiance. Cela leur donne quelque chose à punir.

L’interrogation est limitée. Après environ douze secondes, la page cesse d’attendre et affiche une erreur récupérable au lieu d’afficher une roue qui tourne jusqu’à la retraite de l’utilisateur.

En cas de réussite, ColdFusion redirige le navigateur vers le chemin de rappel que nous avons fourni dans 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>

Il y a plusieurs vérifications délibérées ici.

Premièrement, nous exigeons une inscription en attente dans la session en cours.

Deuxièmement, nous imposons une courte durée de vie à cette inscription en attente. Un rappel trouvé dans un vieux onglet de navigateur ne devrait pas rester valide jusqu'à ce que quelqu'un clique dessus par accident pendant une vente de succession.

Troisièmement, le résultat natif doit signaler une action registration réussie.

Enfin, l'identifiant de l'utilisateur retourné doit correspondre à l'utilisateur qui a lancé l'inscription.

Nous ne faisons pas confiance au rappel simplement parce qu'il contient un résultat de passkey valide. Nous vérifions que le résultat appartient à l'opération attendue par notre application.

Après que passkeyGetResult() signale une réussite, le backend de ColdFusion a déjà stocké l'identifiant. L'application n'a pas besoin d'écrire elle-même l'identifiant.

Elle peut mettre à jour son propre état de sécurité du compte, son dossier d'audit ou son interface utilisateur, mais l'identifiant d'authentification Web demeure la propriété du backend natif de passkey.

Ne consignez pas le jeton de résultat

La valeur passkey_token est de courte durée, mais ce n'est pas une raison pour la traiter comme un élément décoratif.

Ne la consignez pas.

Ne l'incluez pas dans les vidages d'exception.

Ne l'envoyez pas à l'analytique.

Ne conservez pas l'adresse de rappel complète dans les journaux d'accès si ces journaux sont largement accessibles.

La même règle s'applique aux identifiants d'identifiants, aux défis, aux données de l'authentificateur et aux données client représentées en JSON. Consignez les résultats et les codes de diagnostic sécuritaires :

PASSKEY_REGISTRATION_STARTED
PASSKEY_REGISTRATION_COMPLETED
PASSKEY_REGISTRATION_CANCELLED
PASSKEY_REGISTRATION_USER_MISMATCH

Évitez de consigner le matériel de la cérémonie lui-même.

Un journal utile nous dit ce qui s'est passé sans constituer discrètement un musée de l'authentification dans /var/log.

Un petit test pour le service

La majeure partie de la cérémonie côté navigateur nécessite un vrai navigateur et un authentificateur, mais nous pouvons quand même tester notre code de construction de configuration. Cela suppose que vous utilisez le cadre ColdBox, alors si vous utilisez autre chose, vous devrez créer vos propres tests.

Ou sautez complètement les tests comme un animal.

Créez 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(
                    "construit une structure d'inscription contrôlée par le serveur",
                    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(
                    "utilise le chemin de service local à l'application",
                    function() {
                        var config =
                            variables.service.buildConfig();

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


                it(
                    "normalise les identifiants composites d'utilisateur de ColdFusion",
                    function() {
                        var raw =
                            "019cc835-28c5-7eec-a8ab-9c4fe4ac4934"
                            & "::|::2";

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

            }
        );

    }

}

Cela ne prouve pas que la reconnaissance d'empreinte fonctionnera à travers votre répartiteur de charge de production.

Rien d'aussi pratique n'existe.

Cela protège bel et bien le contrat au niveau de l'application : des identifiants d'utilisateur stables, un identifiant de partie de confiance fixe, un rappel fixe et un chemin de service local à l'application.

La véritable cérémonie devrait aussi être testée manuellement avec chaque famille de navigateurs et d'appareils que vous comptez prendre en charge.

Essayer

Une fois le moteur configuré et l'application exécutée en HTTPS :

  1. Connectez-vous avec votre méthode d'authentification existante.
  2. Visitez /account/add-passkey.cfm.
  3. Approuvez l'invite de passkey du navigateur.
  4. Permettez à ColdFusion de rediriger vers le rappel.
  5. Confirmez que la page de sécurité indique une réussite.
  6. Confirmez que la table des identifiants natifs contient une ligne pour l'utilisateur.

Ne testez pas seulement le cas heureux.

Essayez aussi :

  • Annuler l'invite du système d'exploitation
  • Recharger la page de cérémonie
  • Appeler le rappel sans jeton
  • Appeler le rappel après avoir effacé la session
  • Utiliser un navigateur non pris en charge
  • Changer le nom d'hôte
  • Permettre à l'inscription en attente d'expirer

Le code de sécurité a une capacité remarquable à fonctionner parfaitement lorsqu'on l'aborde poliment.

Les utilisateurs, les navigateurs et Internet n'ont aucune obligation d'être polis.

Ce que nous avons construit

Nous avons maintenant un flux complet d'inscription native de passkey :

  • ColdFusion crée et valide la cérémonie d'authentification Web.
  • Le backend DatabasePasskey de ColdFusion stocke l'identifiant.
  • Notre application contrôle l'identifiant de partie de confiance et le rappel.
  • L'inscription est limitée à un utilisateur authentifié existant.
  • Le rappel vérifie que le résultat appartient à l'inscription attendue.
  • Les détails internes de passkey restent hors de notre code de base de données et de nos journaux.

Plus important encore, nous n'avons pas implémenté nous-mêmes la cryptographie de l'authentification Web.

Je considère cela comme une fonctionnalité.

Dans la deuxième partie, nous utiliserons l'identifiant stocké pour authentifier un utilisateur avec passkeyAuthenticate(). Nous ajouterons une connexion découvrable, établirons la session de l'application, traiterons les identifiants périmés et offrirons aux utilisateurs un moyen sûr de consulter et de supprimer leurs passkeys.

À ce stade, nous aurons une connexion sans mot de passe.

Ensuite, nous la placerons derrière un proxy inverse et découvrirons combien de définitions différentes de "origine" peuvent exister dans un système qui, en théorie, n'a qu'une seule adresse Web.

C'est plus que vous ne l'espéreriez.