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

Tutorial de Passkeys: Como Implementar Passkeys em Aplicações Web

Este tutorial explica como implementar passkeys na sua aplicação web. Usamos Node.js (TypeScript), SimpleWebAuthn, HTML / JavaScript Vanilla e MySQL.

Vincent Delitz

Vincent

Created: June 20, 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. Introdução: Como Implementar Passkeys#

Neste tutorial, ajudamos você em seus esforços de implementação de passkeys, oferecendo um guia passo a passo sobre como adicionar passkeys ao seu site.

Demo Icon

Want to try passkeys yourself in a passkeys demo?

Try Passkeys

Ter uma autenticação moderna, robusta e amigável ao usuário é fundamental quando você quer construir um ótimo site ou aplicativo. As passkeys surgiram como a resposta para este desafio. Servindo como o novo padrão para logins, elas prometem um futuro sem as desvantagens das senhas tradicionais, proporcionando uma experiência de login genuinamente sem senha (que não é apenas segura, mas também altamente conveniente).

O que realmente expressa o potencial das passkeys é o endosso que elas receberam. Todos os navegadores significativos, seja Chrome, Firefox, Safari ou Edge, e todos os fabricantes de dispositivos importantes (Apple, Microsoft, Google) incorporaram suporte. Essa adesão unânime mostra que as passkeys são o novo padrão para logins.

Sim, já existem tutoriais sobre a integração de passkeys em aplicações web. Seja para frameworks de frontend como React, Vue.js ou Next.js, há uma infinidade de guias projetados para mitigar desafios e acelerar suas implementações de passkeys. No entanto, falta um tutorial de ponta a ponta que permaneça minimalista e básico. Muitos desenvolvedores nos procuraram e pediram um tutorial que trouxesse clareza sobre a implementação de passkeys para aplicações web.

É precisamente por isso que criamos este guia. Nosso objetivo? Criar uma configuração mínima viável para passkeys, abrangendo a camada de frontend, backend e banco de dados (esta última muitas vezes negligenciada, embora possa causar sérias dores de cabeça).

PasskeyAssessment Icon

Get a free passkey assessment in 15 minutes.

Book free consultation

Ao final desta jornada, você terá construído uma aplicação web mínima viável, onde poderá:

  • Criar uma passkey
  • Usar a passkey para fazer login

Para aqueles com pressa ou que desejam uma referência, todo o código está disponível no GitHub.

Curioso para saber como fica o resultado final? Aqui está uma prévia do projeto final (admitimos que parece muito básico, mas o interessante está sob a superfície):

Estamos plenamente cientes de que partes do código e do projeto podem ser feitas de maneira diferente ou mais sofisticada, mas queríamos focar no essencial. É por isso que mantivemos as coisas intencionalmente simples e centradas nas passkeys.

StateOfPasskeys Icon

Want to find out how many people use passkeys?

View Adoption Data

Como adicionar passkeys ao meu site de produção?

Este é um exemplo muito mínimo de autenticação com passkey. As seguintes coisas NÃO são consideradas / implementadas neste tutorial ou apenas de forma muito básica:

  • UI Condicional / Mediação Condicional / preenchimento automático de passkey
  • Gerenciamento de dispositivos
  • Gerenciamento de sessão
  • Adicionar múltiplos dispositivos de forma segura a uma conta
  • Compatibilidade com versões anteriores
  • Suporte adequado entre plataformas e dispositivos
  • Autenticação de fallback
  • Tratamento de erros adequado
  • Página de gerenciamento de passkeys

Obter suporte completo para todos esses recursos requer um esforço de desenvolvimento tremendamente maior. Para os interessados, recomendamos uma olhada neste artigo sobre equívocos de desenvolvedores de passkeys.

Slack Icon

Become part of our Passkeys Community for updates & support.

Join

2. Pré-requisitos para Integrar Passkeys#

Antes de mergulhar fundo na implementação de passkeys, vamos dar uma olhada nas habilidades e ferramentas necessárias. Aqui está o que você precisa para começar:

2.1 Frontend: HTML & JavaScript Vanilla#

Um sólido conhecimento dos blocos de construção da web — HTML, CSS e JavaScript — é essencial. Mantivemos as coisas intencionalmente diretas, abstendo-nos de qualquer framework JavaScript moderno e confiando em JavaScript / HTML Vanilla. A única coisa mais sofisticada que usamos é a biblioteca wrapper WebAuthn @simplewebauthn/browser.

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

Para nosso backend, usamos um servidor Node.js (Express) escrito em TypeScript. Também decidimos trabalhar com a implementação do servidor WebAuthn do SimpleWebAuthn (@simplewebauthn/server juntamente com @simplewebauthn/typescript-types). Existem inúmeras implementações de servidor WebAuthn disponíveis, então você pode, é claro, usar qualquer uma delas. Como decidimos pelo servidor WebAuthn em TypeScript, é necessário conhecimento básico de Node.js e npm.

2.3 Banco de Dados: MySQL#

Todos os dados do usuário e as chaves públicas das passkeys são armazenados em um banco de dados. Selecionamos o MySQL como tecnologia de banco de dados. Um entendimento fundamental de MySQL e bancos de dados relacionais é benéfico, embora o guiaremos pelos passos individuais.

A seguir, usamos frequentemente os termos WebAuthn e passkeys de forma intercambiável, embora oficialmente possam não significar a mesma coisa. Para melhor compreensão, especialmente na parte do código, fazemos essa suposição.

Com esses pré-requisitos em vigor, você está pronto para mergulhar no mundo das 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.

Mais de 10.000 desenvolvedores confiam na Corbado e tornam a Internet mais segura com passkeys. Tem perguntas? Escrevemos mais de 150 posts de blog sobre passkeys.

Junte-se à Comunidade Passkeys

3. Visão Geral da Arquitetura: Exemplo de Implementação de Passkey#

Antes de entrar no código e nas configurações, vamos dar uma olhada na arquitetura do sistema que queremos construir. Aqui está um detalhamento da arquitetura que estaremos configurando:

  • Frontend: Consiste em dois botões — um para registro de usuário (criando uma passkey) e outro para autenticação (fazendo login usando a passkey).
  • Dispositivo e Navegador: Uma vez que uma ação é acionada no frontend, o dispositivo e o navegador entram em jogo. Eles facilitam a criação e verificação da passkey, atuando como intermediários entre o usuário e o backend.
  • Backend: O backend é onde a verdadeira mágica acontece em nossa aplicação. Ele lida com todas as solicitações iniciadas pelo frontend. Este processo envolve a criação e verificação de passkeys. No centro das operações do backend está o servidor WebAuthn. Ao contrário do que o nome pode sugerir, não é um servidor autônomo. Em vez disso, é uma biblioteca ou pacote que implementa o padrão WebAuthn. As duas funções principais são: Registro (Cadastro), onde novos usuários criam suas passkeys, e Autenticação (Login), onde usuários existentes fazem login usando suas passkeys. Em sua forma mais simples, o servidor WebAuthn fornece quatro endpoints de API públicos, divididos em duas categorias: dois para registro e dois para autenticação. Eles são projetados para receber dados em um formato específico, que é então processado pelo servidor WebAuthn. O servidor WebAuthn é responsável por todas as operações criptográficas necessárias. Um aspecto essencial a ser observado é que esses endpoints de API devem ser servidos sobre HTTPS.
  • Banco de Dados MySQL: Atuando como nossa espinha dorsal de armazenamento, o banco de dados MySQL é responsável por manter os dados do usuário e suas credenciais correspondentes.
Analyzer Icon

Are your users passkey-ready?

Test Passkey-Readiness

Com esta visão geral da arquitetura, você deve ter um mapa conceitual de como os componentes de nossa aplicação funcionam. À medida que avançamos, mergulharemos mais fundo em cada um desses componentes, detalhando sua configuração, ajustes e interação.

O gráfico a seguir descreve o fluxo do processo durante o registro (cadastro):

O gráfico a seguir descreve o fluxo do processo durante a autenticação (login):

Além disso, você encontra a estrutura do projeto aqui (apenas os arquivos mais importantes):

passkeys-tutorial ├── src # Contém todo o código-fonte TypeScript do backend │ ├── controllers # Lógica de negócios para lidar com tipos específicos de solicitações │ │ ├── authentication.ts # Lógica de autenticação de passkey │ │ └── registration.ts # Lógica de registro de passkey │ ├── middleware │ │ ├── customError.ts # Adiciona mensagens de erro personalizadas de maneira padronizada │ │ └── errorHandler.ts # Manipulador de erros geral │ ├── public │ │ ├── index.html # Arquivo HTML principal no frontend │ │ ├── css │ │ │ └── style.css # Estilização básica │ │ └── js │ │ └── script.js # Lógica JavaScript (incl. API WebAuthn) │ ├── routes # Definições de rotas de API e seus manipuladores │ │ └── routes.ts # Rotas específicas de passkey │ ├── services │ │ ├── credentialService.ts# Interage com a tabela de credenciais │ │ └── userService.ts # Interage com a tabela de usuários │ ├── utils # Funções auxiliares e utilitários │ | ├── constants.ts # Algumas constantes (ex. rpID) │ | └── utils.ts # Função auxiliar │ ├── database.ts # Cria a conexão do Node.js para o banco de dados MySQL │ ├── index.ts # Ponto de entrada do servidor Node.js │ └── server.ts # Gerencia todas as configurações do servidor ├── config.json # Algumas configurações para o projeto Node.js ├── docker-compose.yml # Define serviços, redes e volumes para contêineres Docker ├── Dockerfile # Cria uma imagem Docker do projeto ├── init-db.sql # Define nosso esquema de banco de dados MySQL ├── package.json # Gerencia dependências e scripts do projeto Node.js └── tsconfig.json # Configura como o TypeScript compila seu código

4. Configuração do Banco de Dados MySQL#

Ao implementar passkeys, a configuração do banco de dados é um componente chave. Nossa abordagem usa um contêiner Docker executando MySQL, oferecendo um ambiente direto e isolado, essencial para testes e implantação confiáveis.

Nosso esquema de banco de dados é intencionalmente minimalista, apresentando apenas duas tabelas. Essa simplicidade ajuda a uma compreensão mais clara e manutenção mais fácil.

Estrutura Detalhada da Tabela

1. Tabela de Credenciais: Central para a autenticação com passkey, esta tabela armazena as credenciais da passkey. Colunas Críticas:

  • credential_id: Um identificador único para cada credencial. Selecionar o tipo de dados correto para este campo é vital para evitar erros de formatação.
  • public_key: Armazena a chave pública para cada credencial. Assim como com credential_id, o tipo de dados e a formatação apropriados são cruciais.

2. Tabela de Usuários: Vincula as contas de usuário às suas credenciais correspondentes.

Note que nomeamos a primeira tabela como credentials, pois isso está de acordo com nossa experiência e o que outras bibliotecas recomendam como mais adequado (ao contrário da sugestão do SimpleWebAuthn de nomeá-la authenticator ou authenticator_device).

Os tipos de dados para credential_id e public_key são cruciais. Erros frequentemente surgem de tipos de dados, codificação ou formatação incorretos (especialmente a diferença entre Base64 e Base64URL é uma causa comum de erros), o que pode interromper todo o processo de registro (cadastro) ou autenticação (login).

Todos os comandos SQL necessários para configurar essas tabelas estão contidos no arquivo init-db.sql. Este script garante uma inicialização rápida e sem erros do banco de dados.

Para casos mais sofisticados, você pode adicionar credential_device_type ou credential_backed_up para armazenar mais informações sobre as credenciais e melhorar a experiência do usuário. No entanto, nos abstemos disso neste tutorial.

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

Depois de criar este arquivo, criamos um novo arquivo docker-compose.yml no nível raiz do projeto:

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 arquivo inicia o banco de dados MySQL na porta 3306 e cria a estrutura de banco de dados definida. É importante notar que o nome e a senha do banco de dados usados aqui são mantidos simples para fins de demonstração. Em um ambiente de produção, você deve usar credenciais mais complexas para maior segurança.

Em seguida, passamos a executar nosso contêiner Docker. Neste ponto, nosso arquivo docker-compose.yml inclui apenas este único contêiner, mas adicionaremos mais componentes posteriormente. Para iniciar o contêiner, use o seguinte comando:

docker compose up -d

Uma vez que o contêiner esteja em execução, precisamos verificar se o banco de dados está funcionando como esperado. Abra um terminal e execute o seguinte comando para interagir com o banco de dados MySQL:

docker exec -it <ID do contêiner> mysql -uroot -p

Você será solicitado a inserir a senha root, que é my-secret-pw em nosso exemplo. Após o login, selecione o banco de dados webauthn_db e exiba as tabelas usando estes comandos:

use webauthn_db; show tables;

Nesta fase, você deve ver as duas tabelas definidas em nosso script. Inicialmente, essas tabelas estarão vazias, indicando que nossa configuração de banco de dados está completa e pronta para os próximos passos na implementação de passkeys.

5. Implementando Passkeys: Passos de Integração do Backend#

O backend é o núcleo de qualquer aplicação de passkey, atuando como o hub central para processar solicitações de autenticação de usuário do frontend. Ele se comunica com a biblioteca do servidor WebAuthn para lidar com solicitações de registro (cadastro) e autenticação (login), e interage com seu banco de dados MySQL para armazenar e recuperar credenciais de usuário. Abaixo, guiaremos você na configuração do seu backend usando Node.js (Express) com TypeScript, que exporá uma API pública para lidar com todas as solicitações.

5.1 Inicializar o Servidor Node.js (Express)#

Primeiro, crie um novo diretório para o seu projeto e navegue até ele usando seu terminal ou prompt de comando.

Execute o comando

npx create-express-typescript-application passkeys-tutorial

Isso cria um esqueleto de código básico de um aplicativo Node.js (Express) escrito em TypeScript que podemos usar para adaptações futuras.

Seu projeto requer vários pacotes chave que precisamos instalar adicionalmente:

  • @simplewebauthn/server: Uma biblioteca do lado do servidor para facilitar as operações WebAuthn, como registro de usuário (cadastro) e autenticação (login).
  • express-session: Middleware para Express.js para gerenciar sessões, armazenando dados de sessão do lado do servidor e lidando com cookies.
  • uuid: Um utilitário para gerar identificadores únicos universais (UUIDs), comumente usados para criar chaves ou identificadores únicos em aplicações.
  • mysql2: Um cliente Node.js para MySQL, fornecendo capacidades para conectar e executar consultas em bancos de dados MySQL.

Mude para o novo diretório e instale-os com os seguintes comandos (também instalamos os tipos TypeScript necessários):

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

Para confirmar que tudo está instalado corretamente, execute

npm run dev:nodemon

Isso deve iniciar seu servidor Node.js em modo de desenvolvimento com o Nodemon, que reinicia automaticamente o servidor após qualquer alteração de arquivo.

Dica de solução de problemas: Se você encontrar erros, tente atualizar o ts-node para a versão 10.8.1 no arquivo package.json e depois execute npm i para instalar as atualizações.

Seu arquivo server.ts tem a configuração básica e o middleware para uma aplicação Express. Para integrar a funcionalidade de passkey, você precisará adicionar:

  • Rotas: Definir novas rotas para registro (cadastro) e autenticação (login) com passkey.
  • Controladores: Criar controladores para lidar com a lógica para essas rotas.
  • Middleware: Integrar middleware para manipulação de solicitações e erros.
  • Serviços: Construir serviços para recuperar e armazenar dados no banco de dados.
  • Funções Utilitárias: Incluir funções utilitárias para operações de código eficientes.

Essas melhorias são fundamentais para habilitar a autenticação com passkey no backend da sua aplicação. Nós as configuraremos mais tarde.

Debugger Icon

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

Try for Free

5.2 Conexão com o Banco de Dados MySQL#

Depois de criarmos e iniciarmos o banco de dados na seção 4, agora precisamos garantir que nosso backend possa se conectar ao banco de dados MySQL. Para isso, criamos um novo arquivo database.ts na pasta /src e adicionamos o seguinte conteúdo:

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 arquivo será usado posteriormente pelo nosso servidor para acessar o banco de dados.

5.3 Configuração do Servidor da Aplicação#

Vamos dar uma breve olhada no nosso config.json, onde duas variáveis já estão definidas: a porta em que executamos a aplicação e o ambiente:

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

O package.json pode permanecer como está e deve se parecer com:

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 parece com:

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

Em server.ts, precisamos adaptar mais algumas coisas. Além disso, um cache temporário de algum tipo (por exemplo, redis, memcache ou express-session) é necessário para armazenar desafios temporários contra os quais os usuários podem se autenticar. Decidimos usar express-session e declarar o módulo express-session no topo para que as coisas funcionem com express-session. Adicionalmente, simplificamos o roteamento e removemos o tratamento de erros por enquanto (isso será adicionado ao middleware posteriormente):

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 Serviço de Credenciais e Serviço de Usuário#

Para gerenciar efetivamente os dados em nossas duas tabelas criadas, desenvolveremos dois serviços distintos em um novo diretório src/services: authenticatorService.ts e userService.ts.

Cada serviço encapsulará métodos CRUD (Criar, Ler, Atualizar, Excluir), permitindo-nos interagir com o banco de dados de forma modular e organizada. Esses serviços facilitarão o armazenamento, a recuperação e a atualização de dados nas tabelas de autenticador e usuário. Veja como a estrutura desses arquivos necessários deve ser organizada:

userService.ts se parece com isto:

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 parece com o seguinte:

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 lidar com erros centralmente e também facilitar a depuração, adicionamos um arquivo 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 }); };

Além disso, adicionamos um novo arquivo customError.ts, pois mais tarde queremos ser capazes de criar erros personalizados para nos ajudar a encontrar bugs mais rapidamente:

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 Utilitários#

Na pasta utils, criamos dois arquivos constants.ts e utils.ts.

constant.ts contém algumas informações básicas do servidor WebAuthn, como o nome da relying party, o ID da relying party e a origem:

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

utils.ts contém duas funções que precisaremos mais tarde para codificar e decodificar dados:

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 com SimpleWebAuthn#

Agora, chegamos ao coração do nosso backend: os controladores. Criamos dois controladores, um para criar uma nova passkey (registration.ts) e outro para fazer login com uma passkey (authentication.ts).

registration.ts se parece com isto:

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

Vamos revisar as funcionalidades de nossos controladores, que lidam com os dois endpoints chave no processo de registro (cadastro) do WebAuthn. É aqui também que reside uma das maiores diferenças em relação à autenticação baseada em senha: para cada tentativa de registro (cadastro) ou autenticação (login), são necessárias duas chamadas de API de backend, que exigem conteúdo de frontend específico entre elas. As senhas geralmente precisam de apenas um endpoint.

1. Endpoint handleRegisterStart:

Este endpoint é acionado pelo frontend, recebendo um nome de usuário para criar uma nova passkey e conta. Neste exemplo, permitimos apenas a criação de uma nova conta/passkey se ainda não existir uma conta. Em aplicações do mundo real, você precisaria lidar com isso de forma que os usuários sejam informados de que uma passkey já existe e que adicionar do mesmo dispositivo não é possível (mas o usuário poderia adicionar passkeys de um dispositivo diferente após alguma forma de confirmação). Por simplicidade, ignoramos isso neste tutorial.

As PublicKeyCredentialCreationOptions são preparadas. residentKey é definido como preferred, e attestationType como direct, coletando mais dados do autenticador para possível armazenamento no banco de dados.

Em geral, as PublicKeyCredentialCreationOptions consistem nos seguintes dados:

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 as informações da relying party (site ou serviço), geralmente incluindo seu nome (rp.name) e o domínio (rp.id).
  • user: Contém detalhes da conta do usuário como user.name, user.id e user.displayName.
  • challenge: Um valor aleatório e seguro criado pelo servidor WebAuthn para prevenir ataques de repetição durante o processo de registro.
  • pubKeyCredParams: Especifica o tipo de credencial de chave pública a ser criada, incluindo o algoritmo criptográfico usado.
  • timeout: Opcional, define o tempo em milissegundos que o usuário tem para completar a interação.
  • excludeCredentials: Uma lista de credenciais a serem excluídas; usada para evitar o registro de uma passkey para o mesmo dispositivo/autenticador várias vezes.
  • authenticatorSelection: Critérios para selecionar o autenticador, como se ele deve suportar verificação do usuário ou como as chaves residentes devem ser incentivadas.
  • attestation: Especifica a preferência de transmissão de atestado desejada, como "none", "indirect" ou "direct".
  • extensions: Opcional, permite extensões de cliente adicionais.

O ID do usuário e o desafio são armazenados em um objeto de sessão, simplificando o processo para fins de tutorial. Além disso, a sessão é limpa após cada tentativa de registro (cadastro) ou autenticação (login).

2. Endpoint handleRegisterFinish:

Este endpoint recupera o ID do usuário e o desafio definidos anteriormente. Ele verifica a RegistrationResponse com o desafio. Se for válido, ele armazena uma nova credencial para o usuário. Uma vez armazenado no banco de dados, o ID do usuário e o desafio são removidos da sessão.

Dica: Ao depurar sua aplicação, recomendamos fortemente o uso do Chrome como navegador e seus recursos integrados para melhorar a experiência do desenvolvedor de aplicações baseadas em passkey, por exemplo, autenticador WebAuthn virtual e log de dispositivo (veja nossas dicas para desenvolvedores abaixo para mais informações)

Em seguida, passamos para authentication.ts, que tem uma estrutura e funcionalidade semelhantes.

authentication.ts se parece com isto:

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

Nosso processo de autenticação (login) envolve dois endpoints:

1. Endpoint handleLoginStart:

Este endpoint é ativado quando um usuário tenta fazer login. Ele primeiro verifica se o nome de usuário existe no banco de dados, retornando um erro se não for encontrado. Em um cenário do mundo real, você poderia oferecer a criação de uma nova conta.

Para usuários existentes, ele recupera o ID do usuário do banco de dados, armazena-o na sessão e gera opções de PublicKeyCredentialRequestOptions. allowCredentials é deixado vazio para não restringir o uso de credenciais. É por isso que todas as passkeys disponíveis para esta relying party podem ser selecionadas no modal de passkey.

O desafio gerado também é armazenado na sessão e as PublicKeyCredentialRequestOptions são enviadas de volta para o frontend.

As PublicKeyCredentialRequestOptions consistem nos seguintes dados:

dictionary PublicKeyCredentialRequestOptions { required BufferSource challenge; unsigned long timeout; USVString rpId; sequence<PublicKeyCredentialDescriptor> allowCredentials = []; DOMString userVerification = "preferred"; AuthenticationExtensionsClientInputs extensions; };
  • challenge: Um valor aleatório e seguro do servidor WebAuthn usado para prevenir ataques de repetição durante o processo de autenticação.
  • timeout: Opcional, define o tempo em milissegundos que o usuário tem para responder à solicitação de autenticação.
  • rpId: O ID da relying party, geralmente o domínio do serviço.
  • allowCredentials: Uma lista opcional de descritores de credenciais, especificando quais credenciais podem ser usadas para esta autenticação (login).
  • userVerification: Especifica o requisito para verificação do usuário, como "required", "preferred" ou "discouraged".
  • extensions: Opcional, permite extensões de cliente adicionais.

2. Endpoint handleLoginFinish:

Este endpoint recupera o currentChallenge e loggedInUserId da sessão.

Ele consulta o banco de dados pela credencial correta usando o ID da credencial do corpo da requisição. Se a credencial for encontrada, isso significa que o usuário associado a este ID de credencial pode agora ser autenticado (logado). Então, podemos consultar o usuário na tabela de usuários através do ID do usuário que obtemos da credencial e verificar a authenticationResponse usando o desafio e o corpo da requisição. Se tudo for bem-sucedido, mostramos a mensagem de sucesso de login. Se nenhuma credencial correspondente for encontrada, um erro é enviado.

Além disso, se a verificação for bem-sucedida, o contador da credencial é atualizado, o desafio usado e o loggedInUserId são removidos da sessão.

Além disso, podemos excluir a pasta src/app e src/constant juntamente com todos os arquivos nela contidos.

Nota: O gerenciamento adequado de sessões e a proteção de rotas, cruciais em aplicações da vida real, são omitidos aqui por simplicidade neste tutorial.

5.8 Rotas de Passkey#

Por último, mas não menos importante, precisamos garantir que nossos controladores sejam alcançáveis, adicionando as rotas apropriadas a routes.ts, que está em um novo diretório 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 no Frontend#

Esta parte do tutorial de passkeys foca em como suportar passkeys no frontend da sua aplicação. Temos um frontend muito básico consistindo em três arquivos: index.html, styles.css e script.js. Todos os três arquivos estão em uma nova pasta src/public.

O arquivo index.html contém um campo de entrada para o nome de usuário e dois botões para registrar e fazer login. Além disso, importamos o script @simplewebauthn/browser que simplifica a interação com a API de Autenticação Web do navegador no arquivo js/script.js.

index.html se parece com isto:

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 parece com o seguinte:

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

Em script.js, existem três funções principais:

1. Função showMessage:

Esta é uma função utilitária usada principalmente para exibir mensagens de erro, auxiliando na depuração.

2. Função Register:

Acionada quando o usuário clica em "Register". Ela extrai o nome de usuário do campo de entrada e o envia para o endpoint passkeyRegisterStart. A resposta inclui PublicKeyCredentialCreationOptions, que são convertidas para JSON e passadas para SimpleWebAuthnBrowser.startRegistration. Esta chamada ativa o autenticador do dispositivo (como Face ID ou Touch ID). Após a autenticação local bem-sucedida, o desafio assinado é enviado de volta para o endpoint passkeyRegisterFinish, completando o processo de criação da passkey.

Durante o processo de registro (cadastro), o objeto de atestado desempenha um papel crucial, então vamos dar uma olhada mais de perto nele.

O objeto de atestado consiste principalmente em três componentes: fmt, attStmt e authData. O elemento fmt significa o formato da declaração de atestado, enquanto attStmt representa a própria declaração de atestado. Em cenários onde o atestado é considerado desnecessário, o fmt será designado como "none", levando a um attStmt vazio.

O foco está no segmento authData dentro desta estrutura. Este segmento é fundamental para recuperar elementos essenciais como o ID da relying party, flags, contador e dados de credencial atestada em nosso servidor. Em relação às flags, de particular interesse são BS (Backup State) e BE (Backup Eligibility), que fornecem mais informações se uma passkey é sincronizada (por exemplo, via iCloud Keychain ou 1Password). Além disso, UV (User Verification) e UP (User Presence) fornecem informações mais úteis.

É importante notar que várias partes do objeto de atestado, incluindo os dados do autenticador, o ID da relying party e a declaração de atestado, são ou hasheadas ou assinadas digitalmente pelo autenticador usando sua chave privada. Este processo é integral para manter a integridade geral do objeto de atestado.

3. Função Login:

Ativada quando o usuário clica em "Login". Semelhante à função de registro, ela extrai o nome de usuário e o envia para o endpoint passkeyLoginStart. A resposta, contendo PublicKeyCredentialRequestOptions, é convertida para JSON e usada com SimpleWebAuthnBrowser.startAuthentication. Isso aciona a autenticação local no dispositivo. O desafio assinado é então enviado de volta para o endpoint passkeyLoginFinish. Uma resposta bem-sucedida deste endpoint indica que o usuário fez login no aplicativo com sucesso.

Além disso, o arquivo CSS que acompanha fornece um estilo simples para a aplicação:

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. Executar o Aplicativo de Exemplo de Passkey#

Para ver sua aplicação em ação, compile e execute seu código TypeScript com:

npm run dev

Seu servidor agora deve estar em execução em http://localhost:8080.

Considerações para Produção:

Lembre-se, o que cobrimos foi um esboço básico. Ao implantar uma aplicação de passkey em um ambiente de produção, você precisa se aprofundar em:

  • Medidas de Segurança: Implemente práticas de segurança robustas para proteger os dados do usuário.
  • Tratamento de Erros: Garanta que sua aplicação lide e registre erros de forma elegante.
  • Gerenciamento de Banco de Dados: Otimize as operações do banco de dados para escalabilidade e confiabilidade.

8. Integração DevOps de Passkey#

Já configuramos um contêiner Docker para nosso banco de dados. Em seguida, expandiremos nossa configuração do Docker Compose para incluir o servidor com backend e frontend. Seu arquivo docker-compose.yml deve ser atualizado de acordo.

Para containerizar nossa aplicação, criamos um novo Dockerfile que instala os pacotes necessários e inicia o servidor de desenvolvimento:

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

Então, também estendemos o arquivo docker-compose.yml para iniciar este contêiner:

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

Se você agora executar docker compose up em seu terminal e acessar http://localhost:8080, deverá ver a versão funcional do seu aplicativo web de passkey (aqui rodando no Windows 11 23H2 + Chrome 119):

9. Dicas Adicionais de Passkey para Desenvolvedores#

Como trabalhamos há algum tempo com implementações de passkeys, encontramos alguns desafios ao trabalhar em aplicativos de passkey da vida real:

  • Compatibilidade e suporte de dispositivo/plataforma
  • Integração e educação do usuário
  • Lidar com dispositivos perdidos ou alterados
  • Autenticação entre plataformas
  • Mecanismos de fallback
  • Complexidade de codificação: A codificação é muitas vezes a parte mais difícil, pois você tem que lidar com JSON, CBOR, uint8arrays, buffers, blobs, diferentes bancos de dados, base64 e base64url, onde muitos erros podem ocorrer
  • Gerenciamento de passkeys (por exemplo, para adicionar, excluir ou renomear passkeys)

Além disso, temos as seguintes dicas para desenvolvedores quando se trata da parte de implementação:

Utilize o Depurador de Passkeys

O depurador de Passkeys ajuda a testar diferentes configurações do servidor WebAuthn e respostas do cliente. Além disso, ele fornece um ótimo analisador para respostas do autenticador.

Depure com o Recurso de Log de Dispositivo do Chrome

Use o log de dispositivo do Chrome (acessível via chrome://device-log/) para monitorar chamadas FIDO/WebAuthn. Este recurso fornece logs em tempo real do processo de autenticação (login), permitindo que você veja os dados sendo trocados e solucione quaisquer problemas que surjam.

Outro atalho muito útil para obter todas as suas passkeys no Chrome é usar chrome://settings/passkeys.

Use o Autenticador WebAuthn Virtual do Chrome

Para evitar o uso do prompt do Touch ID, Face ID ou Windows Hello durante o desenvolvimento, o Chrome vem com um autenticador WebAuthn virtual muito útil que emula um autenticador real. Recomendamos fortemente o uso para acelerar as coisas. Encontre mais detalhes aqui.

Teste em Diferentes Plataformas e Navegadores

Garanta a compatibilidade e a funcionalidade em vários navegadores e plataformas. O WebAuthn se comporta de maneira diferente em diferentes navegadores, portanto, testes completos são fundamentais.

Teste em Diferentes Dispositivos

Aqui é especialmente útil trabalhar com ferramentas como o ngrok, onde você pode tornar sua aplicação local acessível em outros dispositivos (móveis).

Defina a Verificação do Usuário como preferred

Ao definir as propriedades para userVerification nas PublicKeyCredentialRequestOptions, opte por defini-las como preferred, pois este é um bom equilíbrio entre usabilidade e segurança. Isso significa que as verificações de segurança estão em vigor em dispositivos adequados, enquanto a facilidade de uso é mantida em dispositivos sem capacidades biométricas.

10. Conclusão: Tutorial de Passkey#

Esperamos que este tutorial de passkeys forneça uma compreensão clara de como implementar passkeys de forma eficaz. Ao longo do tutorial, percorremos os passos essenciais para criar uma aplicação de passkey, focando em conceitos fundamentais e implementação prática. Embora este guia sirva como ponto de partida, há muito mais a explorar e refinar no mundo do WebAuthn.

Incentivamos os desenvolvedores a se aprofundarem nas nuances das passkeys (por exemplo, adicionar múltiplas passkeys, verificar a prontidão para passkeys nos dispositivos ou oferecer soluções de recuperação). É uma jornada que vale a pena embarcar, oferecendo tanto desafios quanto imensas recompensas na melhoria da autenticação do usuário. Com as passkeys, você não está apenas construindo um recurso; você está contribuindo para um mundo digital mais seguro e amigável ao usuário.

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