Get your free and exclusive 80-page Banking Passkey Report
passkey tutorial how to implement passkeys

Tutorial de Passkeys: Cómo Implementar Passkeys en Aplicaciones Web

Este tutorial explica cómo implementar passkeys en tu aplicación web. Usamos Node.js (TypeScript), SimpleWebAuthn, HTML / JavaScript Vanilla y MySQL.

Vincent Delitz

Vincent

Created: June 17, 2025

Updated: June 20, 2025


We aim to make the Internet a safer place using passkeys. That's why we want to support developers with tutorials on how to implement passkeys.

1. Introducción: Cómo Implementar Passkeys#

En este tutorial, te ayudamos en tus esfuerzos de implementación de passkeys, ofreciendo una guía paso a paso sobre cómo añadir passkeys a tu sitio web.

Demo Icon

Want to try passkeys yourself in a passkeys demo?

Try Passkeys

Tener una autenticación moderna, robusta y fácil de usar es clave cuando quieres construir un gran sitio web o aplicación. Las passkeys han surgido como la respuesta a este desafío. Sirviendo como el nuevo estándar para los inicios de sesión, prometen un futuro sin las desventajas de las contraseñas tradicionales, proporcionando una experiencia de inicio de sesión genuinamente sin contraseña (que no solo es segura sino también muy conveniente).

Lo que realmente expresa el potencial de las passkeys es el respaldo que han obtenido. Todos los navegadores importantes, ya sea Chrome, Firefox, Safari o Edge, y todos los fabricantes de dispositivos importantes (Apple, Microsoft, Google) han incorporado soporte. Esta adopción unánime demuestra que las passkeys son el nuevo estándar para los inicios de sesión.

Sí, ya existen tutoriales sobre la integración de passkeys en aplicaciones web. Ya sea para frameworks de frontend como React, Vue.js o Next.js, hay una plétora de guías diseñadas para mitigar los desafíos y acelerar tus implementaciones de passkeys. Sin embargo, falta un tutorial de extremo a extremo que se mantenga minimalista y básico. Muchos desarrolladores se han acercado a nosotros y nos han pedido un tutorial que aclare la implementación de passkeys para aplicaciones web.

Es precisamente por eso que hemos creado esta guía. ¿Nuestro objetivo? Crear una configuración mínima viable para passkeys, que abarque la capa de frontend, backend y base de datos (esta última a menudo descuidada, aunque puede causar serios dolores de cabeza).

PasskeyAssessment Icon

Get a free passkey assessment in 15 minutes.

Book free consultation

Al final de este viaje, habrás construido una aplicación web mínima viable, donde podrás:

  • Crear una passkey
  • Usar la passkey para iniciar sesión

Para aquellos con prisa o que deseen una referencia, todo el código base está disponible en GitHub.

¿Curioso por ver el resultado final? Aquí tienes un adelanto del proyecto final (admitimos que parece muy básico, pero lo interesante está bajo la superficie):

Somos plenamente conscientes de que partes del código y del proyecto pueden hacerse de manera diferente o más sofisticada, pero queríamos centrarnos en lo esencial. Por eso, mantuvimos las cosas simples y centradas en las passkeys intencionadamente.

StateOfPasskeys Icon

Want to find out how many people use passkeys?

View Adoption Data

¿Cómo añadir passkeys a mi sitio web de producción?

Este es un ejemplo muy mínimo de autenticación con passkeys. Los siguientes aspectos NO se consideran / implementan en este tutorial o solo de forma muy básica:

  • UI Condicional / Mediación Condicional / autocompletado de passkeys
  • Gestión de dispositivos
  • Gestión de sesiones
  • Añadir múltiples dispositivos de forma segura a una cuenta
  • Compatibilidad con versiones anteriores
  • Soporte adecuado multiplataforma y multidispositivo
  • Autenticación de respaldo
  • Manejo de errores adecuado
  • Página de gestión de passkeys

Obtener soporte completo para todas estas características requiere un esfuerzo de desarrollo tremendamente mayor. Para los interesados, recomendamos echar un vistazo a este artículo sobre conceptos erróneos de los desarrolladores de passkeys.

Slack Icon

Become part of our Passkeys Community for updates & support.

Join

2. Prerrequisitos para Integrar Passkeys#

Antes de sumergirnos en la implementación de passkeys, echemos un vistazo a las habilidades y herramientas necesarias. Esto es lo que necesitas para empezar:

2.1 Frontend: HTML y JavaScript Vanilla#

Un sólido conocimiento de los componentes básicos de la web —HTML, CSS y JavaScript— es esencial. Hemos mantenido las cosas sencillas intencionadamente, absteniéndonos de cualquier framework de JavaScript moderno y hemos confiado en JavaScript / HTML Vanilla. Lo único más sofisticado que usamos es la biblioteca contenedora de WebAuthn @simplewebauthn/browser.

2.2 Backend: Node.js (Express) en TypeScript + SimpleWebAuthn#

Para nuestro backend, usamos un servidor Node.js (Express) escrito en TypeScript. También hemos decidido trabajar con la implementación de servidor WebAuthn de SimpleWebAuthn (@simplewebauthn/server junto con @simplewebauthn/typescript-types). Hay numerosas implementaciones de servidor WebAuthn disponibles, así que por supuesto también puedes usar cualquiera de ellas. Como hemos optado por el servidor WebAuthn de TypeScript, se requieren conocimientos básicos de Node.js y npm.

2.3 Base de datos: MySQL#

Todos los datos de usuario y las claves públicas de las passkeys se almacenan en una base de datos. Hemos seleccionado MySQL como tecnología de base de datos. Un conocimiento fundamental de MySQL y las bases de datos relacionales es beneficioso, aunque te guiaremos a través de los pasos individuales.

A continuación, a menudo usamos los términos WebAuthn y passkeys de manera intercambiable, aunque oficialmente no signifiquen lo mismo. Sin embargo, para una mejor comprensión, especialmente en la parte del código, hacemos esta suposición.

Con estos prerrequisitos en su lugar, estás listo para sumergirte en el mundo de las passkeys.

Ben Gould Testimonial

Ben Gould

Head of Engineering

I’ve built hundreds of integrations in my time, including quite a few with identity providers and I’ve never been so impressed with a developer experience as I have been with Corbado.

Más de 10.000 desarrolladores confían en Corbado y hacen que Internet sea más seguro con passkeys. ¿Tienes preguntas? Hemos escrito más de 150 entradas de blog sobre passkeys.

Únete a la Comunidad de Passkeys

3. Resumen de la Arquitectura: Ejemplo de Implementación de Passkey#

Antes de entrar en el código y las configuraciones, echemos un vistazo a la arquitectura del sistema que queremos construir. Aquí hay un desglose de la arquitectura que vamos a configurar:

  • Frontend: Consiste en dos botones, uno para el registro de usuarios (crear una passkey) y el otro para la autenticación (iniciar sesión usando la passkey).
  • Dispositivo y Navegador: Una vez que se activa una acción en el frontend, el dispositivo y el navegador entran en juego. Facilitan la creación y verificación de la passkey, actuando como intermediarios entre el usuario y el backend.
  • Backend: El backend es donde se desarrolla la verdadera magia en nuestra aplicación. Maneja todas las solicitudes iniciadas por el frontend. Este proceso implica la creación y verificación de passkeys. En el núcleo de las operaciones del backend se encuentra el servidor WebAuthn. Contrariamente a lo que el nombre podría sugerir, no es un servidor independiente. En cambio, es una biblioteca o paquete que implementa el estándar WebAuthn. Las dos funciones principales son: Registro (Sign-up) donde los nuevos usuarios crean sus passkeys y Autenticación (Login): Donde los usuarios existentes inician sesión usando sus passkeys. En su forma más simple, el servidor WebAuthn proporciona cuatro puntos finales de API públicos, divididos en dos categorías: dos para el registro y dos para la autenticación. Están diseñados para recibir datos en un formato específico, que luego es procesado por el servidor WebAuthn. El servidor WebAuthn es responsable de todas las operaciones criptográficas necesarias. Un aspecto esencial a tener en cuenta es que estos puntos finales de API deben servirse a través de HTTPS.
  • Base de datos MySQL: Actuando como nuestra columna vertebral de almacenamiento, la base de datos MySQL es responsable de guardar los datos de los usuarios y sus credenciales correspondientes.
Analyzer Icon

Are your users passkey-ready?

Test Passkey-Readiness

Con este resumen arquitectónico, deberías tener un mapa conceptual de cómo funcionan los componentes de nuestra aplicación. A medida que avancemos, profundizaremos en cada uno de estos componentes, detallando su configuración e interacción.

El siguiente gráfico describe el flujo del proceso durante el registro (sign-up):

El siguiente gráfico describe el flujo del proceso durante la autenticación (login):

Además, aquí encuentras la estructura del proyecto (solo los archivos más importantes):

passkeys-tutorial ├── src # Contiene todo el código fuente de TypeScript del backend │ ├── controllers # Lógica de negocio para manejar tipos específicos de solicitudes │ │ ├── authentication.ts # Lógica de autenticación con passkey │ │ └── registration.ts # Lógica de registro con passkey │ ├── middleware │ │ ├── customError.ts # Añade mensajes de error personalizados de manera estandarizada │ │ └── errorHandler.ts # Manejador de errores general │ ├── public │ │ ├── index.html # Archivo HTML principal en el frontend │ │ ├── css │ │ │ └── style.css # Estilos básicos │ │ └── js │ │ └── script.js # Lógica de JavaScript (incl. API de WebAuthn) │ ├── routes # Definiciones de rutas de API y sus manejadores │ │ └── routes.ts # Rutas específicas de passkey │ ├── services │ │ ├── credentialService.ts# Interactúa con la tabla de credenciales │ │ └── userService.ts # Interactúa con la tabla de usuarios │ ├── utils # Funciones de ayuda y utilidades │ | ├── constants.ts # Algunas constantes (p. ej. rpID) │ | └── utils.ts # Función de ayuda │ ├── database.ts # Crea la conexión de Node.js a la base de datos MySQL │ ├── index.ts # Punto de entrada del servidor Node.js │ └── server.ts # Gestiona todas las configuraciones del servidor ├── config.json # Algunas configuraciones para el proyecto Node.js ├── docker-compose.yml # Define servicios, redes y volúmenes para contenedores Docker ├── Dockerfile # Crea una imagen Docker del proyecto ├── init-db.sql # Define nuestro esquema de base de datos MySQL ├── package.json # Gestiona las dependencias y scripts del proyecto Node.js └── tsconfig.json # Configura cómo TypeScript compila tu código

4. Configuración de la Base de Datos MySQL#

Al implementar passkeys, la configuración de la base de datos es un componente clave. Nuestro enfoque utiliza un contenedor Docker que ejecuta MySQL, ofreciendo un entorno sencillo y aislado esencial para pruebas y despliegues fiables.

Nuestro esquema de base de datos es intencionadamente minimalista, con solo dos tablas. Esta simplicidad ayuda a una comprensión más clara y un mantenimiento más fácil.

Estructura Detallada de las Tablas

1. Tabla Credentials: Central para la autenticación con passkeys, esta tabla almacena las credenciales de las passkeys. Columnas críticas:

  • credential_id: Un identificador único para cada credencial. Seleccionar el tipo de datos correcto para este campo es vital para evitar errores de formato.
  • public_key: Almacena la clave pública para cada credencial. Al igual que con credential_id, el tipo de datos y el formato adecuados son cruciales.

2. Tabla Users: Vincula las cuentas de usuario con sus credenciales correspondientes.

Ten en cuenta que nombramos la primera tabla credentials ya que esto se ajusta a nuestra experiencia y a lo que otras bibliotecas recomiendan como más adecuado (contrariamente a la sugerencia de SimpleWebAuthn de llamarla authenticator o authenticator_device).

Los tipos de datos para credential_id y public_key son cruciales. Los errores a menudo surgen de tipos de datos, codificación o formato incorrectos (especialmente la diferencia entre Base64 y Base64URL es una causa común de errores), lo que puede interrumpir todo el proceso de registro (sign-up) o autenticación (login).

Todos los comandos SQL necesarios para configurar estas tablas están contenidos en el archivo init-db.sql. Este script asegura una inicialización de la base de datos rápida y sin errores.

Para casos más sofisticados, puedes añadir credential_device_type o credential_backed_up para almacenar más información sobre las credenciales y mejorar la experiencia del usuario. Sin embargo, en este tutorial nos abstenemos de hacerlo.

init-db.sql
CREATE TABLE users ( id VARCHAR(255) PRIMARY KEY, username VARCHAR(255) NOT NULL UNIQUE ); CREATE TABLE credentials ( id INT AUTO_INCREMENT PRIMARY KEY, user_id VARCHAR(255) NOT NULL, credential_id VARCHAR(255) NOT NULL, public_key TEXT NOT NULL, counter INT NOT NULL, transports VARCHAR(255), FOREIGN KEY (user_id) REFERENCES users (id) );

Después de haber creado este archivo, creamos un nuevo archivo docker-compose.yml en el nivel raíz del proyecto:

docker-compose.yml
version: "3.1" services: db: image: mysql command: --default-authentication-plugin=mysql_native_password restart: always environment: MYSQL_ROOT_PASSWORD: my-secret-pw MYSQL_DATABASE: webauthn_db ports: - "3306:3306" volumes: - ./init-db.sql:/docker-entrypoint-initdb.d/init-db.sql

Este archivo inicia la base de datos MySQL en el puerto 3306 y crea la estructura de base de datos definida. Es importante tener en cuenta que el nombre y la contraseña de la base de datos utilizados aquí se mantienen simples para fines de demostración. En un entorno de producción, debes usar credenciales más complejas para una mayor seguridad.

A continuación, pasamos a ejecutar nuestro contenedor Docker. En este punto, nuestro archivo docker-compose.yml solo incluye este único contenedor, pero añadiremos más componentes más adelante. Para iniciar el contenedor, usa el siguiente comando:

docker compose up -d

Una vez que el contenedor esté en funcionamiento, necesitamos verificar si la base de datos funciona como se espera. Abre una terminal y ejecuta el siguiente comando para interactuar con la base de datos MySQL:

docker exec -it <container ID> mysql -uroot -p

Se te pedirá que introduzcas la contraseña de root, que es my-secret-pw en nuestro ejemplo. Después de iniciar sesión, selecciona la base de datos webauthn_db y muestra las tablas usando estos comandos:

use webauthn_db; show tables;

En esta etapa, deberías ver las dos tablas definidas en nuestro script. Inicialmente, estas tablas estarán vacías, lo que indica que nuestra configuración de la base de datos está completa y lista para los siguientes pasos en la implementación de passkeys.

5. Implementando Passkeys: Pasos de Integración del Backend#

El backend es el núcleo de cualquier aplicación de passkeys, actuando como el centro neurálgico para procesar las solicitudes de autenticación de usuario desde el frontend. Se comunica con la biblioteca del servidor WebAuthn para manejar las solicitudes de registro (sign-up) y autenticación (login), e interactúa con tu base de datos MySQL para almacenar y recuperar las credenciales de los usuarios. A continuación, te guiaremos a través de la configuración de tu backend usando Node.js (Express) con TypeScript, que expondrá una API pública para manejar todas las solicitudes.

5.1 Inicializar el Servidor Node.js (Express)#

Primero, crea un nuevo directorio para tu proyecto y navega hacia él usando tu terminal o línea de comandos.

Ejecuta el comando

npx create-express-typescript-application passkeys-tutorial

Esto crea un esqueleto de código básico de una aplicación Node.js (Express) escrita en TypeScript que podemos usar para adaptaciones posteriores.

Tu proyecto requiere varios paquetes clave que necesitamos instalar adicionalmente:

  • @simplewebauthn/server: Una biblioteca del lado del servidor para facilitar las operaciones de WebAuthn, como el registro de usuarios (sign-up) y la autenticación (login).
  • express-session: Middleware para Express.js para gestionar sesiones, almacenando datos de sesión del lado del servidor y manejando cookies.
  • uuid: Una utilidad para generar identificadores únicos universales (UUIDs), comúnmente utilizados para crear claves o identificadores únicos en aplicaciones.
  • mysql2: Un cliente de Node.js para MySQL, que proporciona capacidades para conectar y ejecutar consultas contra bases de datos MySQL.

Cambia al nuevo directorio e instálalos con los siguientes comandos (también instalamos los tipos de TypeScript requeridos):

cd passkeys-tutorial npm install @simplewebauthn/server mysql2 uuid express-session @types/express-session @types/uuid

Para confirmar que todo está instalado correctamente, ejecuta

npm run dev:nodemon

Esto debería iniciar tu servidor Node.js en modo de desarrollo con Nodemon, que reinicia automáticamente el servidor ante cualquier cambio en los archivos.

Consejo para la solución de problemas: Si encuentras errores, intenta actualizar ts-node a la versión 10.8.1 en el archivo package.json y luego ejecuta npm i para instalar las actualizaciones.

Tu archivo server.ts tiene la configuración básica y el middleware para una aplicación Express. Para integrar la funcionalidad de passkey, necesitarás añadir:

  • Rutas: Define nuevas rutas para el registro (sign-up) y la autenticación (login) con passkey.
  • Controladores: Crea controladores para manejar la lógica de estas rutas.
  • Middleware: Integra middleware para el manejo de solicitudes y errores.
  • Servicios: Construye servicios para recuperar y almacenar datos en la base de datos.
  • Funciones de utilidad: Incluye funciones de utilidad para operaciones de código eficientes.

Estas mejoras son clave para habilitar la autenticación con passkeys en el backend de tu aplicación. Las configuraremos más adelante.

Debugger Icon

Want to experiment with passkey flows? Try our Passkeys Debugger.

Try for Free

5.2 Conexión a la Base de Datos MySQL#

Después de crear e iniciar la base de datos en la sección 4, ahora debemos asegurarnos de que nuestro backend pueda conectarse a la base de datos MySQL. Por lo tanto, creamos un nuevo archivo database.ts en la carpeta /src y añadimos el siguiente contenido:

database.ts
import mysql from "mysql2"; // Create a MySQL pool const pool = mysql.createPool({ host: process.env.DB_HOST, user: process.env.DB_USER, password: process.env.DB_PASSWORD, database: process.env.DB_NAME, waitForConnections: true, connectionLimit: 10, queueLimit: 0, }); // Promisify for Node.js async/await. export const promisePool = pool.promise();

Este archivo será utilizado más tarde por nuestro servidor para acceder a la base de datos.

5.3 Configuración del Servidor de la Aplicación#

Echemos un vistazo breve a nuestro config.json, donde ya están definidas dos variables: el puerto en el que ejecutamos la aplicación y el entorno:

config.json
{ "PORT": 8080, "NODE_ENV": "development" }

package.json puede quedarse como está y debería verse así:

package.json
{ "name": "passkeys-tutorial", "version": "0.0.1", "description": "passkeys-tutorial initialised with create-express-typescript-application.", "main": "src/index.ts", "scripts": { "build": "tsc", "start": "node ./build/src/index.js", "dev": "ts-node ./src/index.ts", "dev:nodemon": "nodemon -w src -e ts,json -x ts-node ./src/index.ts", "test": "echo \"Error: no test specified\" && exit 1" }, "keywords": ["express", "typescript"], "devDependencies": { "@types/cors": "^2.8.17", "@types/express": "^4.17.21", "@types/morgan": "^1.9.9", "@types/node": "^14.18.63", "@typescript-eslint/eslint-plugin": "^4.33.0", "@typescript-eslint/parser": "^4.33.0", "eslint": "^7.32.0", "nodemon": "^2.0.22", "ts-node": "^10.8.1", "typescript": "^4.9.5" }, "dependencies": { "@simplewebauthn/server": "^8.3.5", "@types/express-session": "^1.17.10", "@types/uuid": "^9.0.7", "cors": "^2.8.5", "env-cmd": "^10.1.0", "express": "^4.18.2", "express-session": "^1.17.3", "fs": "^0.0.1-security", "helmet": "^4.6.0", "morgan": "^1.10.0", "mysql2": "^3.6.5", "uuid": "^9.0.1" } }

index.ts se ve así:

index.ts
import app from "./server"; import config from "../config.json"; // Start the application by listening to specific port const port = Number(process.env.PORT || config.PORT || 8080); app.listen(port, () => { console.info("Express application started on port: " + port); });

En server.ts, necesitamos adaptar algunas cosas más. Además, se necesita una caché temporal de algún tipo (p. ej. redis, memcache o express-session) para almacenar los desafíos temporales contra los que los usuarios pueden autenticarse. Decidimos usar express-session y declarar el módulo express-session en la parte superior para que funcione con express-session. Adicionalmente, simplificamos el enrutamiento y eliminamos el manejo de errores por ahora (esto se añadirá al middleware más tarde):

server.ts
import express, { Express } from "express"; import morgan from "morgan"; import helmet from "helmet"; import cors from "cors"; import config from "../config.json"; import { router as passkeyRoutes } from "./routes/routes"; import session from "express-session"; const app: Express = express(); declare module "express-session" { interface SessionData { currentChallenge?: string; loggedInUserId?: string; } } /************************************************************************************ * Basic Express Middlewares ***********************************************************************************/ app.set("json spaces", 4); app.use(express.json()); app.use(express.urlencoded({ extended: true })); app.use( session({ // @ts-ignore secret: process.env.SESSION_SECRET, saveUninitialized: true, resave: false, cookie: { maxAge: 86400000, httpOnly: true, // Ensure to not expose session cookies to clientside scripts }, }), ); // Handle logs in console during development if (process.env.NODE_ENV === "development" || config.NODE_ENV === "development") { app.use(morgan("dev")); app.use(cors()); } // Handle security and origin in production if (process.env.NODE_ENV === "production" || config.NODE_ENV === "production") { app.use(helmet()); } /************************************************************************************ * Register all routes ***********************************************************************************/ app.use("/api/passkey", passkeyRoutes); app.use(express.static("src/public")); export default app;

5.4 Servicio de Credenciales y Servicio de Usuario#

Para gestionar eficazmente los datos en nuestras dos tablas creadas, desarrollaremos dos servicios distintos en un nuevo directorio src/services: authenticatorService.ts y userService.ts.

Cada servicio encapsulará los métodos CRUD (Crear, Leer, Actualizar, Eliminar), permitiéndonos interactuar con la base de datos de una manera modular y organizada. Estos servicios facilitarán el almacenamiento, la recuperación y la actualización de datos en las tablas de authenticator y user. Así es como debe ser la estructura de estos archivos requeridos:

userService.ts se ve así:

userService.ts
import { promisePool } from "../database"; // Adjust the import path as necessary import { v4 as uuidv4 } from "uuid"; export const userService = { async getUserById(userId: string) { const [rows] = await promisePool.query("SELECT * FROM users WHERE id = ?", [ userId, ]); // @ts-ignore return rows[0]; }, async getUserByUsername(username: string) { try { const [rows] = await promisePool.query( "SELECT * FROM users WHERE username = ?", [username], ); // @ts-ignore return rows[0]; } catch (error) { return null; } }, async createUser(username: string) { const id = uuidv4(); await promisePool.query("INSERT INTO users (id, username) VALUES (?, ?)", [ id, username, ]); return { id, username }; }, };

credentialService.ts se ve de la siguiente manera:

credentialService.ts
import { promisePool } from "../database"; import type { AuthenticatorDevice } from "@simplewebauthn/typescript-types"; export const credentialService = { async saveNewCredential( userId: string, credentialId: string, publicKey: string, counter: number, transports: string, ) { try { await promisePool.query( "INSERT INTO credentials (user_id, credential_id, public_key, counter, transports) VALUES (?, ?, ?, ?, ?)", [userId, credentialId, publicKey, counter, transports], ); } catch (error) { console.error("Error saving new credential:", error); throw error; } }, async getCredentialByCredentialId( credentialId: string, ): Promise<AuthenticatorDevice | null> { try { const [rows] = await promisePool.query( "SELECT * FROM credentials WHERE credential_id = ? LIMIT 1", [credentialId], ); // @ts-ignore if (rows.length === 0) return null; // @ts-ignore const row = rows[0]; return { userID: row.user_id, credentialID: row.credential_id, credentialPublicKey: row.public_key, counter: row.counter, transports: row.transports ? row.transports.split(",") : [], } as AuthenticatorDevice; } catch (error) { console.error("Error retrieving credential:", error); throw error; } }, async updateCredentialCounter(credentialId: string, newCounter: number) { try { await promisePool.query( "UPDATE credentials SET counter = ? WHERE credential_id = ?", [newCounter, credentialId], ); } catch (error) { console.error("Error updating credential counter:", error); throw error; } }, };

5.5 Middleware#

Para manejar los errores de forma centralizada y también para facilitar la depuración, añadimos un archivo errorHandler.ts:

errorHandler.ts
import { Request, Response, NextFunction } from "express"; import { CustomError } from "./customError"; interface ErrorWithStatus extends Error { statusCode?: number; } export const handleError = ( err: CustomError, req: Request, res: Response, next: NextFunction, ) => { const statusCode = err.statusCode || 500; const message = err.message || "Internal Server Error"; console.log(message); res.status(statusCode).send({ error: message }); };

Además, añadimos un nuevo archivo customError.ts ya que más adelante querremos ser capaces de crear errores personalizados para ayudarnos a encontrar bugs más rápidamente:

customError.ts
export class CustomError extends Error { statusCode: number; constructor(message: string, statusCode: number = 500) { super(message); this.statusCode = statusCode; Object.setPrototypeOf(this, CustomError.prototype); } }

5.6 Utilidades#

En la carpeta utils, creamos dos archivos constants.ts y utils.ts.

constant.ts contiene información básica del servidor WebAuthn, como el nombre de la relying party, el ID de la relying party y el origen:

constant.ts
export const rpName: string = "Passkeys Tutorial"; export const rpID: string = "localhost"; export const origin: string = `http://${rpID}:8080`;

utils.ts contiene dos funciones que necesitaremos más tarde para codificar y decodificar datos:

utils.ts
export const uint8ArrayToBase64 = (uint8Array: Uint8Array): string => Buffer.from(uint8Array).toString("base64"); export const base64ToUint8Array = (base64: string): Uint8Array => new Uint8Array(Buffer.from(base64, "base64"));

5.7 Controladores de Passkey con SimpleWebAuthn#

Ahora, llegamos al corazón de nuestro backend: los controladores. Creamos dos controladores, uno para crear una nueva passkey (registration.ts) y otro para iniciar sesión con una passkey (authentication.ts).

registration.ts se ve así:

registration.ts
import { generateRegistrationOptions, verifyRegistrationResponse, } from "@simplewebauthn/server"; import { uint8ArrayToBase64 } from "../utils/utils"; import { rpName, rpID, origin } from "../utils/constants"; import { credentialService } from "../services/credentialService"; import { userService } from "../services/userService"; import { RegistrationResponseJSON } from "@simplewebauthn/typescript-types"; import { Request, Response, NextFunction } from "express"; import { CustomError } from "../middleware/customError"; export const handleRegisterStart = async ( req: Request, res: Response, next: NextFunction, ) => { const { username } = req.body; if (!username) { return next(new CustomError("Username empty", 400)); } try { let user = await userService.getUserByUsername(username); if (user) { return next(new CustomError("User already exists", 400)); } else { user = await userService.createUser(username); } const options = await generateRegistrationOptions({ rpName, rpID, userID: user.id, userName: user.username, timeout: 60000, attestationType: "direct", excludeCredentials: [], authenticatorSelection: { residentKey: "preferred", }, // Support for the two most common algorithms: ES256, and RS256 supportedAlgorithmIDs: [-7, -257], }); req.session.loggedInUserId = user.id; req.session.currentChallenge = options.challenge; res.send(options); } catch (error) { next( error instanceof CustomError ? error : new CustomError("Internal Server Error", 500), ); } }; export const handleRegisterFinish = async ( req: Request, res: Response, next: NextFunction, ) => { const { body } = req; const { currentChallenge, loggedInUserId } = req.session; if (!loggedInUserId) { return next(new CustomError("User ID is missing", 400)); } if (!currentChallenge) { return next(new CustomError("Current challenge is missing", 400)); } try { const verification = await verifyRegistrationResponse({ response: body as RegistrationResponseJSON, expectedChallenge: currentChallenge, expectedOrigin: origin, expectedRPID: rpID, requireUserVerification: true, }); if (verification.verified && verification.registrationInfo) { const { credentialPublicKey, credentialID, counter } = verification.registrationInfo; await credentialService.saveNewCredential( loggedInUserId, uint8ArrayToBase64(credentialID), uint8ArrayToBase64(credentialPublicKey), counter, body.response.transports, ); res.send({ verified: true }); } else { next(new CustomError("Verification failed", 400)); } } catch (error) { next( error instanceof CustomError ? error : new CustomError("Internal Server Error", 500), ); } finally { req.session.loggedInUserId = undefined; req.session.currentChallenge = undefined; } };

Revisemos las funcionalidades de nuestros controladores, que manejan los dos puntos finales clave en el proceso de registro (sign-up) de WebAuthn. Aquí también reside una de las mayores diferencias con la autenticación basada en contraseñas: por cada intento de registro (sign-up) o autenticación (login), se requieren dos llamadas a la API del backend, que necesitan contenido específico del frontend entre ellas. Las contraseñas generalmente solo necesitan un punto final.

1. Punto final handleRegisterStart:

Este punto final es activado por el frontend, recibiendo un nombre de usuario para crear una nueva passkey y cuenta. En este ejemplo, solo permitimos la creación de una nueva cuenta / passkey si aún no existe una cuenta. En aplicaciones del mundo real, necesitarías manejar esto de manera que se informe a los usuarios que ya existe una passkey y que no es posible añadirla desde el mismo dispositivo (pero el usuario podría añadir passkeys desde un dispositivo diferente después de alguna forma de confirmación). Por simplicidad, omitimos esto en este tutorial.

Se preparan las PublicKeyCredentialCreationOptions. residentKey se establece en preferred, y attestationType en direct, recopilando más datos del authenticator para un posible almacenamiento en la base de datos.

En general, las PublicKeyCredentialCreationOptions consisten en los siguientes datos:

dictionary PublicKeyCredentialCreationOptions { required PublicKeyCredentialRpEntity rp; required PublicKeyCredentialUserEntity user; required BufferSource challenge; required sequence<PublicKeyCredentialParameters> pubKeyCredParams; unsigned long timeout; sequence<PublicKeyCredentialDescriptor> excludeCredentials = []; AuthenticatorSelectionCriteria authenticatorSelection; DOMString attestation = "none"; AuthenticationExtensionsClientInputs extensions; };
  • rp: Representa la información de la relying party (sitio web o servicio), que normalmente incluye su nombre (rp.name) y el dominio (rp.id).
  • user: Contiene detalles de la cuenta de usuario como user.name, user.id y user.displayName.
  • challenge: Un valor aleatorio y seguro creado por el servidor WebAuthn para prevenir ataques de repetición durante el proceso de registro.
  • pubKeyCredParams: Especifica el tipo de credencial de clave pública a crear, incluyendo el algoritmo criptográfico utilizado.
  • timeout: Opcional, establece el tiempo en milisegundos que el usuario tiene para completar la interacción.
  • excludeCredentials: Una lista de credenciales a excluir; se usa para evitar registrar una passkey para el mismo dispositivo / authenticator varias veces.
  • authenticatorSelection: Criterios para seleccionar el authenticator, como si debe soportar la verificación del usuario o cómo se deben fomentar las claves residentes.
  • attestation: Especifica la preferencia de transmisión de attestation deseada, como "none", "indirect" o "direct".
  • extensions: Opcional, permite extensiones de cliente adicionales.

El ID de usuario y el challenge se almacenan en un objeto de sesión, simplificando el proceso para fines del tutorial. Además, la sesión se borra después de cada intento de registro (sign-up) o autenticación (login).

2. Punto final handleRegisterFinish:

Este punto final recupera el ID de usuario y el challenge establecidos anteriormente. Verifica la RegistrationResponse con el challenge. Si es válido, almacena una nueva credencial para el usuario. Una vez almacenado en la base de datos, el ID de usuario y el challenge se eliminan de la sesión.

Consejo: Al depurar tu aplicación, recomendamos encarecidamente usar Chrome como navegador y sus funciones integradas para mejorar la experiencia del desarrollador de aplicaciones basadas en passkeys, p. ej. el autenticador WebAuthn virtual y el registro de dispositivos (consulta nuestros consejos para desarrolladores a continuación para obtener más información).

A continuación, pasamos a authentication.ts, que tiene una estructura y funcionalidad similares.

authentication.ts se ve así:

authentication.ts
import { Request, Response, NextFunction } from "express"; import { generateAuthenticationOptions, verifyAuthenticationResponse, } from "@simplewebauthn/server"; import { uint8ArrayToBase64, base64ToUint8Array } from "../utils/utils"; import { rpID, origin } from "../utils/constants"; import { credentialService } from "../services/credentialService"; import { userService } from "../services/userService"; import { AuthenticatorDevice } from "@simplewebauthn/typescript-types"; import { isoBase64URL } from "@simplewebauthn/server/helpers"; import { VerifiedAuthenticationResponse, VerifyAuthenticationResponseOpts, } from "@simplewebauthn/server/esm"; import { CustomError } from "../middleware/customError"; export const handleLoginStart = async ( req: Request, res: Response, next: NextFunction, ) => { const { username } = req.body; try { const user = await userService.getUserByUsername(username); if (!user) { return next(new CustomError("User not found", 404)); } req.session.loggedInUserId = user.id; // allowCredentials is purposely for this demo left empty. This causes all existing local credentials // to be displayed for the service instead only the ones the username has registered. const options = await generateAuthenticationOptions({ timeout: 60000, allowCredentials: [], userVerification: "required", rpID, }); req.session.currentChallenge = options.challenge; res.send(options); } catch (error) { next( error instanceof CustomError ? error : new CustomError("Internal Server Error", 500), ); } }; export const handleLoginFinish = async ( req: Request, res: Response, next: NextFunction, ) => { const { body } = req; const { currentChallenge, loggedInUserId } = req.session; if (!loggedInUserId) { return next(new CustomError("User ID is missing", 400)); } if (!currentChallenge) { return next(new CustomError("Current challenge is missing", 400)); } try { const credentialID = isoBase64URL.toBase64(body.rawId); const bodyCredIDBuffer = isoBase64URL.toBuffer(body.rawId); const dbCredential: AuthenticatorDevice | null = await credentialService.getCredentialByCredentialId(credentialID); if (!dbCredential) { return next(new CustomError("Credential not registered with this site", 404)); } // @ts-ignore const user = await userService.getUserById(dbCredential.userID); if (!user) { return next(new CustomError("User not found", 404)); } // @ts-ignore dbCredential.credentialID = base64ToUint8Array(dbCredential.credentialID); // @ts-ignore dbCredential.credentialPublicKey = base64ToUint8Array( dbCredential.credentialPublicKey, ); let verification: VerifiedAuthenticationResponse; const opts: VerifyAuthenticationResponseOpts = { response: body, expectedChallenge: currentChallenge, expectedOrigin: origin, expectedRPID: rpID, authenticator: dbCredential, }; verification = await verifyAuthenticationResponse(opts); const { verified, authenticationInfo } = verification; if (verified) { await credentialService.updateCredentialCounter( uint8ArrayToBase64(bodyCredIDBuffer), authenticationInfo.newCounter, ); res.send({ verified: true }); } else { next(new CustomError("Verification failed", 400)); } } catch (error) { next( error instanceof CustomError ? error : new CustomError("Internal Server Error", 500), ); } finally { req.session.currentChallenge = undefined; req.session.loggedInUserId = undefined; } };

Nuestro proceso de autenticación (login) involucra dos puntos finales:

1. Punto final handleLoginStart:

Este punto final se activa cuando un usuario intenta iniciar sesión. Primero verifica si el nombre de usuario existe en la base de datos, devolviendo un error si no se encuentra. En un escenario del mundo real, podrías ofrecer crear una nueva cuenta en su lugar.

Para los usuarios existentes, recupera el ID de usuario de la base de datos, lo almacena en la sesión y genera opciones PublicKeyCredentialRequestOptions. allowCredentials se deja vacío para no restringir el uso de credenciales. Es por eso que todas las passkeys disponibles para esta relying party se pueden seleccionar en el modal de passkey.

El challenge generado también se almacena en la sesión y las PublicKeyCredentialRequestOptions se envían de vuelta al frontend.

Las PublicKeyCredentialRequestOptions consisten en los siguientes datos:

dictionary PublicKeyCredentialRequestOptions { required BufferSource challenge; unsigned long timeout; USVString rpId; sequence<PublicKeyCredentialDescriptor> allowCredentials = []; DOMString userVerification = "preferred"; AuthenticationExtensionsClientInputs extensions; };
  • challenge: Un valor aleatorio y seguro del servidor WebAuthn utilizado para prevenir ataques de repetición durante el proceso de autenticación.
  • timeout: Opcional, establece el tiempo en milisegundos que el usuario tiene para responder a la solicitud de autenticación.
  • rpId: El ID de la relying party, típicamente el dominio del servicio.
  • allowCredentials: Una lista opcional de descriptores de credenciales, especificando qué credenciales se pueden usar para esta autenticación (login).
  • userVerification: Especifica el requisito de verificación del usuario, como "required", "preferred" o "discouraged".
  • extensions: Opcional, permite extensiones de cliente adicionales.

2. Punto final handleLoginFinish:

Este punto final recupera el currentChallenge y loggedInUserId de la sesión.

Consulta la base de datos para la credencial correcta usando el ID de credencial del cuerpo de la solicitud. Si se encuentra la credencial, esto significa que el usuario asociado con este ID de credencial ahora puede ser autenticado (iniciar sesión). Luego, podemos consultar al usuario de la tabla de usuarios a través del ID de usuario que obtenemos de la credencial y verificar la authenticationResponse usando el challenge y el cuerpo de la solicitud. Si todo es exitoso, mostramos el mensaje de éxito de inicio de sesión. Si no se encuentra una credencial coincidente, se envía un error.

Además, si la verificación tiene éxito, se actualiza el contador de la credencial, y el challenge usado y loggedInUserId se eliminan de la sesión.

Además de eso, podemos eliminar la carpeta src/app y src/constant junto con todos los archivos que contienen.

Nota: La gestión adecuada de sesiones y la protección de rutas, cruciales en aplicaciones de la vida real, se omiten aquí por simplicidad en este tutorial.

5.8 Rutas de Passkey#

Por último, pero no menos importante, debemos asegurarnos de que nuestros controladores sean accesibles añadiendo las rutas apropiadas a routes.ts, que se encuentra en un nuevo directorio src/routes:

routes.ts
import express from "express"; import { handleError } from "../middleware/errorHandler"; import { handleRegisterStart, handleRegisterFinish } from "../controllers/registration"; import { handleLoginStart, handleLoginFinish } from "../controllers/authentication"; const router = express.Router(); router.post("/registerStart", handleRegisterStart); router.post("/registerFinish", handleRegisterFinish); router.post("/loginStart", handleLoginStart); router.post("/loginFinish", handleLoginFinish); router.use(handleError); export { router };
Substack Icon

Subscribe to our Passkeys Substack for the latest news.

Subscribe

6. Integrar Passkeys en el Frontend#

Esta parte del tutorial de passkeys se centra en cómo dar soporte a las passkeys en el frontend de tu aplicación. Tenemos un frontend muy básico que consta de tres archivos: index.html, styles.css y script.js. Los tres archivos están en una nueva carpeta src/public.

El archivo index.html contiene un campo de entrada para el nombre de usuario y dos botones para registrarse e iniciar sesión. Además, importamos el script @simplewebauthn/browser que simplifica la interacción con la API de Autenticación Web del navegador en el archivo js/script.js.

index.html se ve así:

index.html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Passkey Tutorial</title> <link rel="stylesheet" href="css/style.css" /> </head> <body> <div class="container"> <h1>Passkey Tutorial</h1> <div id="message"></div> <div class="input-group"> <input type="text" id="username" placeholder="Enter username" /> <button id="registerButton">Register</button> <button id="loginButton">Login</button> </div> </div> <script src="https://unpkg.com/@simplewebauthn/browser/dist/bundle/index.es5.umd.min.js"></script> <script src="js/script.js"></script> </body> </html>

script.js se ve de la siguiente manera:

script.js
document.getElementById("registerButton").addEventListener("click", register); document.getElementById("loginButton").addEventListener("click", login); function showMessage(message, isError = false) { const messageElement = document.getElementById("message"); messageElement.textContent = message; messageElement.style.color = isError ? "red" : "green"; } async function register() { // Retrieve the username from the input field const username = document.getElementById("username").value; try { // Get registration options from your server. Here, we also receive the challenge. const response = await fetch("/api/passkey/registerStart", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ username: username }), }); console.log(response); // Check if the registration options are ok. if (!response.ok) { throw new Error( "User already exists or failed to get registration options from server", ); } // Convert the registration options to JSON. const options = await response.json(); console.log(options); // This triggers the browser to display the passkey / WebAuthn modal (e.g. Face ID, Touch ID, Windows Hello). // A new attestation is created. This also means a new public-private-key pair is created. const attestationResponse = await SimpleWebAuthnBrowser.startRegistration(options); // Send attestationResponse back to server for verification and storage. const verificationResponse = await fetch("/api/passkey/registerFinish", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(attestationResponse), }); if (verificationResponse.ok) { showMessage("Registration successful"); } else { showMessage("Registration failed", true); } } catch (error) { showMessage("Error: " + error.message, true); } } async function login() { // Retrieve the username from the input field const username = document.getElementById("username").value; try { // Get login options from your server. Here, we also receive the challenge. const response = await fetch("/api/passkey/loginStart", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ username: username }), }); // Check if the login options are ok. if (!response.ok) { throw new Error("Failed to get login options from server"); } // Convert the login options to JSON. const options = await response.json(); console.log(options); // This triggers the browser to display the passkey / WebAuthn modal (e.g. Face ID, Touch ID, Windows Hello). // A new assertionResponse is created. This also means that the challenge has been signed. const assertionResponse = await SimpleWebAuthnBrowser.startAuthentication(options); // Send assertionResponse back to server for verification. const verificationResponse = await fetch("/api/passkey/loginFinish", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(assertionResponse), }); if (verificationResponse.ok) { showMessage("Login successful"); } else { showMessage("Login failed", true); } } catch (error) { showMessage("Error: " + error.message, true); } }

En script.js, hay tres funciones principales:

1. Función showMessage:

Esta es una función de utilidad utilizada principalmente para mostrar mensajes de error, ayudando en la depuración.

2. Función Register:

Se activa cuando el usuario hace clic en "Register". Extrae el nombre de usuario del campo de entrada y lo envía al punto final passkeyRegisterStart. La respuesta incluye PublicKeyCredentialCreationOptions, que se convierten a JSON y se pasan a SimpleWebAuthnBrowser.startRegistration. Esta llamada activa el authenticator del dispositivo (como Face ID o Touch ID). Tras una autenticación local exitosa, el challenge firmado se envía de vuelta al punto final passkeyRegisterFinish, completando el proceso de creación de la passkey.

Durante el proceso de registro (sign-up), el objeto attestation juega un papel crucial, así que echemos un vistazo más de cerca.

El objeto attestation consta principalmente de tres componentes: fmt, attStmt y authData. El elemento fmt significa el formato de la declaración de attestation, mientras que attStmt representa la declaración de atestación real en sí. En escenarios donde la attestation se considera innecesaria, el fmt se designará como "none", lo que lleva a un attStmt vacío.

El enfoque está en el segmento authData dentro de esta estructura. Este segmento es clave para recuperar elementos esenciales como el ID de la relying party, flags, contador y datos de credenciales atestiguadas en nuestro servidor. En cuanto a los flags, de particular interés son BS (Backup State) y BE (Backup Eligibility) que proporcionan más información si una passkey está sincronizada (p. ej. a través de iCloud Keychain o 1Password). Además, UV (User Verification) y UP (User Presence) proporcionan información más útil.

Es importante tener en cuenta que varias partes del objeto de atestación, incluidos los datos del autenticador, el ID de la relying party y la declaración de attestation, son hasheadas o firmadas digitalmente por el authenticator usando su clave privada. Este proceso es integral para mantener la integridad general del objeto de atestación.

3. Función Login:

Se activa cuando el usuario hace clic en "Login". Similar a la función de registro, extrae el nombre de usuario y lo envía al punto final passkeyLoginStart. La respuesta, que contiene PublicKeyCredentialRequestOptions, se convierte a JSON y se usa con SimpleWebAuthnBrowser.startAuthentication. Esto desencadena la autenticación local en el dispositivo. El challenge firmado se envía de vuelta al punto final passkeyLoginFinish. Una respuesta exitosa de este punto final indica que el usuario ha iniciado sesión en la aplicación con éxito.

Además, el archivo CSS adjunto proporciona un estilo simple para la aplicación:

body { font-family: "Helvetica Neue", Arial, sans-serif; text-align: center; padding: 40px; background-color: #f3f4f6; color: #333; } .container { max-width: 400px; margin: auto; background: white; padding: 20px; box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); border-radius: 8px; } h1 { color: #007bff; font-size: 24px; margin-bottom: 20px; } .input-group { margin-bottom: 20px; } input[type="text"] { padding: 10px; margin-bottom: 10px; border: 1px solid #ced4da; border-radius: 4px; width: calc(100% - 22px); } button { width: calc(50% - 20px); padding: 10px 0; margin: 5px; font-size: 16px; cursor: pointer; border: none; border-radius: 4px; background-color: #007bff; color: white; } button:hover { background-color: #0056b3; } #message { color: #dc3545; margin: 20px; }

7. Ejecutar la Aplicación de Ejemplo de Passkey#

Para ver tu aplicación en acción, compila y ejecuta tu código TypeScript con:

npm run dev

Tu servidor debería estar ahora en funcionamiento en http://localhost:8080.

Consideraciones para Producción:

Recuerda, lo que hemos cubierto es un esquema básico. Al desplegar una aplicación de passkey en un entorno de producción, necesitas profundizar en:

  • Medidas de Seguridad: Implementa prácticas de seguridad robustas para proteger los datos de los usuarios.
  • Manejo de Errores: Asegúrate de que tu aplicación maneje y registre los errores de manera elegante.
  • Gestión de la Base de Datos: Optimiza las operaciones de la base de datos para escalabilidad y fiabilidad.

8. Integración DevOps de Passkey#

Ya hemos configurado un contenedor Docker para nuestra base de datos. A continuación, ampliaremos nuestra configuración de Docker Compose para incluir el servidor con tanto el backend como el frontend. Tu archivo docker-compose.yml debe actualizarse en consecuencia.

Para contenerizar nuestra aplicación, creamos un nuevo Dockerfile que instala los paquetes requeridos e inicia el servidor de desarrollo:

Docker
# Use an official Node runtime as a parent image FROM node:20-alpine # Set the working directory in the container WORKDIR /usr/src/app # Copy package.json and package-lock.json COPY package*.json ./ # Install any needed packages RUN npm install # Bundle your app's source code inside the Docker image COPY . . # Make port 8080 available to the world outside this container EXPOSE 8080 # Define the command to run your app CMD ["npm", "run", "dev"]

Luego, también extendemos el archivo docker-compose.yml para iniciar este contenedor:

docker-compose.yml
version: "3.1" services: db: image: mysql command: --default-authentication-plugin=mysql_native_password restart: always environment: MYSQL_ROOT_PASSWORD: my-secret-pw MYSQL_DATABASE: webauthn_db ports: - "3306:3306" volumes: - ./init-db.sql:/docker-entrypoint-initdb.d/init-db.sql app: build: . ports: - "8080:8080" environment: - DB_HOST=db - DB_USER=root - DB_PASSWORD=my-secret-pw - DB_NAME=webauthn_db - SESSION_SECRET=secret123 depends_on: - db

Si ahora ejecutas docker compose up en tu terminal y accedes a http://localhost:8080, deberías ver la versión funcional de tu aplicación web de passkey (aquí ejecutándose en Windows 11 23H2 + Chrome 119):

9. Consejos Adicionales de Passkey para Desarrolladores#

Dado que hemos estado trabajando durante bastante tiempo con implementaciones de passkeys, nos hemos encontrado con un par de desafíos si trabajas en aplicaciones de passkey de la vida real:

  • Compatibilidad y soporte de dispositivos / plataformas
  • Incorporación y educación del usuario
  • Manejo de dispositivos perdidos o cambiados
  • Autenticación multiplataforma
  • Mecanismos de respaldo
  • Complejidad de la codificación: La codificación es a menudo la parte más difícil ya que tienes que lidiar con JSON, CBOR, uint8arrays, buffers, blobs, diferentes bases de datos, base64 y base64url donde pueden ocurrir muchos errores
  • Gestión de passkeys (p. ej. para añadir, eliminar o renombrar passkeys)

Además, tenemos los siguientes consejos para los desarrolladores en lo que respecta a la parte de la implementación:

Utiliza el Depurador de Passkeys

El depurador de Passkeys ayuda a probar diferentes configuraciones del servidor WebAuthn y respuestas del cliente. Además, proporciona un gran analizador para las respuestas del autenticador.

Depura con la Función de Registro de Dispositivos de Chrome

Usa el registro de dispositivos de Chrome (accesible a través de chrome://device-log/) para monitorear las llamadas FIDO/WebAuthn. Esta función proporciona registros en tiempo real del proceso de autenticación (login), permitiéndote ver los datos que se intercambian y solucionar cualquier problema que surja.

Otro atajo muy útil para obtener todas tus passkeys en Chrome es usar chrome://settings/passkeys.

Usa el Autenticador WebAuthn Virtual de Chrome

Para evitar usar el aviso de Touch ID, Face ID o Windows Hello durante el desarrollo, Chrome viene con un autenticador WebAuthn virtual muy práctico que emula un autenticador real. Recomendamos encarecidamente usarlo para acelerar las cosas. Encuentra más detalles aquí.

Prueba en Diferentes Plataformas y Navegadores

Asegura la compatibilidad y funcionalidad en varios navegadores y plataformas. WebAuthn se comporta de manera diferente en diferentes navegadores, por lo que las pruebas exhaustivas son clave.

Prueba en Diferentes Dispositivos

Aquí es especialmente útil trabajar con herramientas como ngrok, donde puedes hacer que tu aplicación local sea accesible en otros dispositivos (móviles).

Establece la Verificación del Usuario en preferred

Al definir las propiedades para userVerification en las PublicKeyCredentialRequestOptions, elige establecerlas en preferred, ya que es un buen equilibrio entre usabilidad y seguridad. Esto significa que hay controles de seguridad en los dispositivos adecuados, mientras que se mantiene la facilidad de uso en dispositivos sin capacidades biométricas.

10. Conclusión: Tutorial de Passkey#

Esperamos que este tutorial de passkeys proporcione una comprensión clara de cómo implementar passkeys de manera efectiva. A lo largo del tutorial, hemos recorrido los pasos esenciales para crear una aplicación de passkey, centrándonos en conceptos fundamentales e implementación práctica. Si bien esta guía sirve como punto de partida, hay mucho más por explorar y refinar en el mundo de WebAuthn.

Animamos a los desarrolladores a profundizar en los matices de las passkeys (p. ej. añadir múltiples passkeys, comprobar la preparación para passkeys en los dispositivos u ofrecer soluciones de recuperación). Es un viaje que vale la pena emprender, que ofrece tanto desafíos como inmensas recompensas en la mejora de la autenticación de usuarios. Con las passkeys, no solo estás construyendo una característica; estás contribuyendo a un mundo digital más seguro y fácil de usar.

Add passkeys to your app in <1 hour with our UI components, SDKs & guides.

Start for free

Share this article


LinkedInTwitterFacebook

Enjoyed this read?

🤝 Join our Passkeys Community

Share passkeys implementation tips and get support to free the world from passwords.

🚀 Subscribe to Substack

Get the latest news, strategies, and insights about passkeys sent straight to your inbox.

Related Articles