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

Руководство по ключам доступа: как внедрить ключи доступа в веб-приложения

В этом руководстве объясняется, как внедрить ключи доступа (passkeys) в ваше веб-приложение. Мы используем Node.js (TypeScript), SimpleWebAuthn, Vanilla HTML / JavaScript и 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. Введение: как внедрить ключи доступа#

В этом руководстве мы поможем вам с внедрением ключей доступа, предложив пошаговую инструкцию по добавлению passkeys на ваш сайт.

Demo Icon

Want to try passkeys yourself in a passkeys demo?

Try Passkeys

Современная, надежная и удобная для пользователя аутентификация — это ключ к созданию отличного веб-сайта или приложения. Ключи доступа (passkeys) стали ответом на этот вызов. Являясь новым стандартом для входа в систему, они обещают будущее без недостатков традиционных паролей, обеспечивая по-настоящему беспарольный вход (что не только безопасно, но и очень удобно).

Что действительно говорит о потенциале ключей доступа, так это поддержка, которую они получили. Все основные браузеры, будь то Chrome, Firefox, Safari или Edge, и все важные производители устройств (Apple, Microsoft, Google) внедрили их поддержку. Это единодушное принятие показывает, что ключи доступа — это новый стандарт для входа в систему.

Да, уже существуют руководства по интеграции ключей доступа в веб-приложения. Будь то для фронтенд-фреймворков, таких как React, Vue.js или Next.js, существует множество руководств, призванных облегчить трудности и ускорить внедрение ключей доступа. Однако полноценного руководства, которое оставалось бы минималистичным и низкоуровневым, не хватает. Многие разработчики обращались к нам с просьбой о руководстве, которое внесло бы ясность во внедрение ключей доступа для веб-приложений.

Именно поэтому мы и создали это руководство. Наша цель? Создать минимально жизнеспособную конфигурацию для ключей доступа, охватывающую фронтенд, бэкенд и уровень базы данных (последний часто упускают из виду, хотя он может вызвать серьезные головные боли).

PasskeyAssessment Icon

Get a free passkey assessment in 15 minutes.

Book free consultation

К концу этого пути вы создадите минимально жизнеспособное веб-приложение, в котором сможете:

  • Создавать ключ доступа
  • Использовать ключ доступа для входа в систему

Для тех, кто торопится или хочет иметь под рукой готовый пример, вся кодовая база доступна на GitHub.

Интересно, как выглядит конечный результат? Вот краткий обзор финального проекта (признаем, он выглядит очень просто, но самое интересное скрыто под капотом):

Мы полностью осознаем, что части кода и проекта можно реализовать иначе или более изощренно, но мы хотели сосредоточиться на самом необходимом. Поэтому мы намеренно сделали все простым и ориентированным на ключи доступа.

StateOfPasskeys Icon

Want to find out how many people use passkeys?

View Adoption Data

Как добавить ключи доступа на мой рабочий сайт?

Это очень минималистичный пример аутентификации с помощью ключей доступа. Следующие моменты НЕ рассматриваются / не реализованы в этом руководстве или реализованы очень базово:

  • Условный UI / Условная медиация / автозаполнение ключей доступа
  • Управление устройствами
  • Управление сессиями
  • Безопасное добавление нескольких устройств к аккаунту
  • Обратная совместимость
  • Правильная кроссплатформенная и кросс-устройственная поддержка
  • Резервная аутентификация
  • Правильная обработка ошибок
  • Страница управления ключами доступа

Получение полной поддержки всех этих функций требует значительно больших усилий по разработке. Тем, кто заинтересован, мы рекомендуем ознакомиться с этой статьей о заблуждениях разработчиков относительно ключей доступа.

Slack Icon

Become part of our Passkeys Community for updates & support.

Join

2. Предварительные требования для интеграции ключей доступа#

Прежде чем углубляться в реализацию ключей доступа, давайте рассмотрим необходимые навыки и инструменты. Вот что вам понадобится для начала:

2.1 Фронтенд: Vanilla HTML и JavaScript#

Необходимо твердое знание основ веба — HTML, CSS и JavaScript. Мы намеренно все упростили, воздержавшись от современных JavaScript-фреймворков и полагаясь на Vanilla JavaScript / HTML. Единственная более сложная вещь, которую мы используем, — это библиотека-обертка для WebAuthn @simplewebauthn/browser.

2.2 Бэкенд: Node.js (Express) на TypeScript + SimpleWebAuthn#

Для нашего бэкенда мы используем сервер на Node.js (Express), написанный на TypeScript. Мы также решили работать с серверной реализацией WebAuthn от SimpleWebAuthn (@simplewebauthn/server вместе с @simplewebauthn/typescript-types). Существует множество серверных реализаций WebAuthn, так что вы, конечно, можете использовать любую из них. Поскольку мы выбрали сервер WebAuthn на TypeScript, требуются базовые знания Node.js и npm.

2.3 База данных: MySQL#

Все данные пользователей и публичные ключи passkeys хранятся в базе данных. Мы выбрали MySQL в качестве технологии баз данных. Базовое понимание MySQL и реляционных баз данных будет полезным, хотя мы проведем вас по всем шагам.

Далее мы часто будем использовать термины WebAuthn и passkeys как взаимозаменяемые, хотя официально они могут означать не одно и то же. Однако для лучшего понимания, особенно в части кода, мы делаем это допущение.

Имея эти предварительные знания, вы готовы погрузиться в мир ключей доступа.

3. Обзор архитектуры: пример реализации ключей доступа#

Прежде чем переходить к коду и конфигурациям, давайте рассмотрим архитектуру системы, которую мы собираемся построить. Вот разбивка архитектуры, которую мы будем настраивать:

  • Фронтенд: Он состоит из двух кнопок: одна для регистрации пользователя (создание ключа доступа), а другая для аутентификации (вход с использованием ключа доступа).
  • Устройство и браузер: Как только на фронтенде инициируется действие, в игру вступают устройство и браузер. Они облегчают создание и проверку ключа доступа, выступая посредниками между пользователем и бэкендом.
  • Бэкенд: Бэкенд — это место, где в нашем приложении происходит настоящая магия. Он обрабатывает все запросы, инициированные фронтендом. Этот процесс включает создание и проверку ключей доступа. В основе операций бэкенда лежит сервер WebAuthn. Вопреки тому, что может показаться из названия, это не отдельный сервер. Вместо этого это библиотека или пакет, реализующий стандарт WebAuthn. Две основные функции: Регистрация (Sign-up), где новые пользователи создают свои ключи доступа, и Аутентификация (Login): где существующие пользователи входят в систему с помощью своих ключей доступа. В своей простейшей форме сервер WebAuthn предоставляет четыре публичных конечных точки API, разделенных на две категории: две для регистрации и две для аутентификации. Они предназначены для получения данных в определенном формате, которые затем обрабатываются сервером WebAuthn. Сервер WebAuthn отвечает за все необходимые криптографические операции. Важно отметить, что эти конечные точки API должны обслуживаться по протоколу HTTPS.
  • База данных MySQL: Выступая в качестве нашего хранилища, база данных MySQL отвечает за хранение данных пользователей и их соответствующих учетных данных.
Analyzer Icon

Are your users passkey-ready?

Test Passkey-Readiness

С этим обзором архитектуры у вас должна быть концептуальная карта того, как взаимодействуют компоненты нашего приложения. По мере продвижения мы будем углубляться в каждый из этих компонентов, подробно описывая их настройку, конфигурацию и взаимодействие.

Следующая диаграмма описывает ход процесса во время регистрации (sign-up):

Следующая диаграмма описывает ход процесса во время аутентификации (login):

Кроме того, здесь вы найдете структуру проекта (только самые важные файлы):

passkeys-tutorial ├── src # Содержит весь исходный код бэкенда на TypeScript │ ├── controllers # Бизнес-логика для обработки определенных типов запросов │ │ ├── authentication.ts # Логика аутентификации с помощью ключа доступа │ │ └── registration.ts # Логика регистрации с помощью ключа доступа │ ├── middleware │ │ ├── customError.ts # Добавление пользовательских сообщений об ошибках в стандартизированном виде │ │ └── errorHandler.ts # Общий обработчик ошибок │ ├── public │ │ ├── index.html # Основной HTML-файл фронтенда │ │ ├── css │ │ │ └── style.css # Базовые стили │ │ └── js │ │ └── script.js # Логика JavaScript (включая WebAuthn API) │ ├── routes # Определения маршрутов API и их обработчиков │ │ └── routes.ts # Специфичные маршруты для ключей доступа │ ├── services │ │ ├── credentialService.ts# Взаимодействует с таблицей учетных данных │ │ └── userService.ts # Взаимодействует с таблицей пользователей │ ├── utils # Вспомогательные функции и утилиты │ | ├── constants.ts # Некоторые константы (например, rpID) │ | └── utils.ts # Вспомогательная функция │ ├── database.ts # Создает соединение от Node.js к базе данных MySQL │ ├── index.ts # Точка входа сервера Node.js │ └── server.ts # Управляет всеми настройками сервера ├── config.json # Некоторые конфигурации для проекта Node.js ├── docker-compose.yml # Определяет сервисы, сети и тома для контейнеров Docker ├── Dockerfile # Создает Docker-образ проекта ├── init-db.sql # Определяет схему нашей базы данных MySQL ├── package.json # Управляет зависимостями и скриптами проекта Node.js └── tsconfig.json # Настраивает, как TypeScript компилирует ваш код

4. Настройка базы данных MySQL#

При внедрении ключей доступа настройка базы данных является ключевым компонентом. Наш подход использует Docker-контейнер с MySQL, предлагая простую и изолированную среду, необходимую для надежного тестирования и развертывания.

Наша схема базы данных намеренно минималистична и содержит всего две таблицы. Эта простота способствует более ясному пониманию и облегчает обслуживание.

Подробная структура таблиц

1. Таблица Credentials: Центральная для аутентификации с помощью ключей доступа, эта таблица хранит учетные данные passkey. Критически важные столбцы:

  • credential_id: Уникальный идентификатор для каждого учетного данного. Выбор правильного типа данных для этого поля жизненно важен, чтобы избежать ошибок форматирования.
  • public_key: Хранит публичный ключ для каждого учетного данного. Как и в случае с credential_id, важны соответствующий тип данных и форматирование.

2. Таблица Users: Связывает учетные записи пользователей с их соответствующими учетными данными.

Обратите внимание, что мы назвали первую таблицу credentials, так как, согласно нашему опыту и рекомендациям других библиотек, это более подходящее название (в отличие от предложения SimpleWebAuthn назвать ее authenticator или authenticator_device).

Типы данных для credential_id и public_key имеют решающее значение. Ошибки часто возникают из-за неверных типов данных, кодировки или форматирования (особенно разница между Base64 и Base64URL является частой причиной ошибок), что может нарушить весь процесс регистрации или аутентификации.

Все необходимые SQL-команды для настройки этих таблиц содержатся в файле init-db.sql. Этот скрипт обеспечивает быструю и безошибочную инициализацию базы данных.

Для более сложных случаев вы можете добавить credential_device_type или credential_backed_up, чтобы хранить больше информации об учетных данных и улучшить пользовательский опыт. Однако в этом руководстве мы от этого воздержимся.

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

После создания этого файла мы создаем новый файл docker-compose.yml на корневом уровне проекта:

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

Этот файл запускает базу данных MySQL на порту 3306 и создает определенную структуру базы данных. Важно отметить, что имя и пароль для базы данных, используемые здесь, сделаны простыми для демонстрационных целей. В производственной среде следует использовать более сложные учетные данные для повышения безопасности.

Далее мы переходим к запуску нашего Docker-контейнера. На данный момент наш файл docker-compose.yml включает только этот один контейнер, но позже мы добавим больше компонентов. Чтобы запустить контейнер, используйте следующую команду:

docker compose up -d

Как только контейнер будет запущен, нам нужно проверить, работает ли база данных как ожидалось. Откройте терминал и выполните следующую команду для взаимодействия с базой данных MySQL:

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

Вам будет предложено ввести пароль root, который в нашем примере my-secret-pw. После входа выберите базу данных webauthn_db и отобразите таблицы с помощью этих команд:

use webauthn_db; show tables;

На этом этапе вы должны увидеть две таблицы, определенные в нашем скрипте. Изначально эти таблицы будут пустыми, что указывает на то, что наша настройка базы данных завершена и готова к следующим шагам по внедрению ключей доступа.

5. Внедрение ключей доступа: шаги по интеграции бэкенда#

Бэкенд является ядром любого приложения с ключами доступа, выступая в качестве центрального узла для обработки запросов на аутентификацию пользователей с фронтенда. Он взаимодействует с библиотекой сервера WebAuthn для обработки запросов на регистрацию и аутентификацию, а также взаимодействует с вашей базой данных MySQL для хранения и извлечения учетных данных пользователей. Ниже мы проведем вас через настройку вашего бэкенда с использованием Node.js (Express) с TypeScript, который будет предоставлять публичный API для обработки всех запросов.

5.1 Инициализация сервера Node.js (Express)#

Сначала создайте новый каталог для вашего проекта и перейдите в него с помощью терминала или командной строки.

Выполните команду

npx create-express-typescript-application passkeys-tutorial

Это создает базовый скелет кода приложения Node.js (Express), написанного на TypeScript, который мы можем использовать для дальнейших адаптаций.

Вашему проекту потребуется несколько ключевых пакетов, которые нам нужно установить дополнительно:

  • @simplewebauthn/server: Серверная библиотека для облегчения операций WebAuthn, таких как регистрация пользователя и аутентификация.
  • express-session: Промежуточное ПО для Express.js для управления сессиями, хранения данных сессий на стороне сервера и обработки cookie.
  • uuid: Утилита для генерации универсально уникальных идентификаторов (UUID), обычно используемых для создания уникальных ключей или идентификаторов в приложениях.
  • mysql2: Клиент Node.js для MySQL, предоставляющий возможности для подключения и выполнения запросов к базам данных MySQL.

Перейдите в новый каталог и установите их с помощью следующих команд (мы также устанавливаем необходимые типы TypeScript):

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

Чтобы убедиться, что все установлено правильно, выполните

npm run dev:nodemon

Это должно запустить ваш сервер Node.js в режиме разработки с Nodemon, который автоматически перезапускает сервер при любых изменениях файлов.

Совет по устранению неполадок: Если вы столкнулись с ошибками, попробуйте обновить ts-node до версии 10.8.1 в файле package.json, а затем выполните npm i для установки обновлений.

Ваш файл server.ts имеет базовую настройку и промежуточное ПО для приложения Express. Чтобы интегрировать функциональность ключей доступа, вам нужно будет добавить:

  • Маршруты: Определить новые маршруты для регистрации и аутентификации с помощью ключей доступа.
  • Контроллеры: Создать контроллеры для обработки логики этих маршрутов.
  • Промежуточное ПО: Интегрировать промежуточное ПО для обработки запросов и ошибок.
  • Сервисы: Создать сервисы для извлечения и хранения данных в базе данных.
  • Вспомогательные функции: Включить вспомогательные функции для эффективных операций с кодом.

Эти усовершенствования являются ключом к включению аутентификации с помощью ключей доступа в бэкенде вашего приложения. Мы настроим их позже.

Debugger Icon

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

Try for Free

5.2 Подключение к базе данных MySQL#

После того как мы создали и запустили базу данных в разделе 4, нам нужно убедиться, что наш бэкенд может к ней подключиться. Для этого мы создаем новый файл database.ts в папке /src и добавляем следующее содержимое:

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

Этот файл позже будет использоваться нашим сервером для доступа к базе данных.

5.3 Конфигурация сервера приложений#

Давайте кратко рассмотрим наш config.json, где уже определены две переменные: порт, на котором мы запускаем приложение, и среда:

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

package.json может остаться как есть и должен выглядеть так:

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 выглядит так:

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

В server.ts нам нужно адаптировать еще несколько вещей. Кроме того, необходим временный кэш какого-либо рода (например, redis, memcache или express-session) для хранения временных вызовов, по которым пользователи могут аутентифицироваться. Мы решили использовать express-session и объявляем модуль express-session вверху, чтобы все работало с express-session. Кроме того, мы оптимизируем маршрутизацию и пока убираем обработку ошибок (это будет добавлено в промежуточное ПО позже):

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 Сервисы CredentialService и UserService#

Для эффективного управления данными в наших двух созданных таблицах мы разработаем два отдельных сервиса в новом каталоге src/services: authenticatorService.ts и userService.ts.

Каждый сервис будет инкапсулировать методы CRUD (Create, Read, Update, Delete), что позволит нам взаимодействовать с базой данных модульным и организованным способом. Эти сервисы облегчат хранение, извлечение и обновление данных в таблицах authenticator и user. Вот как должна быть организована структура этих необходимых файлов:

userService.ts выглядит так:

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 выглядит следующим образом:

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

Для централизованной обработки ошибок и облегчения отладки мы добавляем файл 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 }); };

Кроме того, мы добавляем новый файл customError.ts, так как позже мы хотим иметь возможность создавать пользовательские ошибки, чтобы быстрее находить баги:

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 Утилиты#

В папке utils мы создаем два файла: constants.ts и utils.ts.

constant.ts содержит некоторую базовую информацию о сервере WebAuthn, такую как имя проверяющей стороны, ID проверяющей стороны и origin:

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

utils.ts содержит две функции, которые нам понадобятся позже для кодирования и декодирования данных:

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 Контроллеры ключей доступа с SimpleWebAuthn#

Теперь мы подходим к сердцу нашего бэкенда: контроллерам. Мы создаем два контроллера: один для создания нового ключа доступа (registration.ts) и один для входа с помощью ключа доступа (authentication.ts).

registration.ts выглядит так:

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

Давайте рассмотрим функциональность наших контроллеров, которые обрабатывают две ключевые конечные точки в процессе регистрации WebAuthn. Здесь же кроется одно из самых больших отличий от аутентификации на основе паролей: для каждой попытки регистрации или аутентификации требуется два вызова API бэкенда, которые требуют специфического контента на фронтенде между ними. Паролям обычно нужна только одна конечная точка.

1. Конечная точка handleRegisterStart:

Эта конечная точка запускается фронтендом, получая имя пользователя для создания нового ключа доступа и аккаунта. В этом примере мы разрешаем создание нового аккаунта / ключа доступа только в том случае, если аккаунт еще не существует. В реальных приложениях вам нужно было бы обрабатывать это таким образом, чтобы пользователям сообщалось, что ключ доступа уже существует, и добавление с того же устройства невозможно (но пользователь мог бы добавить ключи доступа с другого устройства после некоторой формы подтверждения). Для простоты мы упускаем это в этом руководстве.

Подготавливаются PublicKeyCredentialCreationOptions. residentKey устанавливается в preferred, а attestationType в direct, собирая больше данных от аутентификатора для потенциального хранения в базе данных.

В общем, PublicKeyCredentialCreationOptions состоят из следующих данных:

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: Представляет информацию о проверяющей стороне (веб-сайте или сервисе), обычно включая ее имя (rp.name) и домен (rp.id).
  • user: Содержит данные учетной записи пользователя, такие как user.name, user.id и user.displayName.
  • challenge: Безопасное, случайное значение, созданное сервером WebAuthn для предотвращения атак повторного воспроизведения во время процесса регистрации.
  • pubKeyCredParams: Указывает тип создаваемого учетного данного с открытым ключом, включая используемый криптографический алгоритм.
  • timeout: Необязательно, устанавливает время в миллисекундах, которое есть у пользователя для завершения взаимодействия.
  • excludeCredentials: Список учетных данных, которые следует исключить; используется для предотвращения многократной регистрации ключа доступа для одного и того же устройства / аутентификатора.
  • authenticatorSelection: Критерии для выбора аутентификатора, например, должен ли он поддерживать верификацию пользователя или как следует поощрять использование резидентных ключей.
  • attestation: Указывает желаемое предпочтение передачи аттестации, например, "none", "indirect" или "direct".
  • extensions: Необязательно, позволяет использовать дополнительные клиентские расширения.

ID пользователя и вызов хранятся в объекте сессии, что упрощает процесс для целей руководства. Кроме того, сессия очищается после каждой попытки регистрации или аутентификации.

2. Конечная точка handleRegisterFinish:

Эта конечная точка извлекает ранее установленные ID пользователя и вызов. Она проверяет RegistrationResponse с помощью вызова. Если он действителен, она сохраняет новое учетное данное для пользователя. После сохранения в базе данных ID пользователя и вызов удаляются из сессии.

Совет: При отладке вашего приложения мы настоятельно рекомендуем использовать браузер Chrome и его встроенные функции для улучшения опыта разработки приложений на основе ключей доступа, например, виртуальный аутентификатор WebAuthn и лог устройства (см. наши советы для разработчиков ниже для получения дополнительной информации)

Далее мы переходим к authentication.ts, который имеет схожую структуру и функциональность.

authentication.ts выглядит так:

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

Наш процесс аутентификации включает две конечные точки:

1. Конечная точка handleLoginStart:

Эта конечная точка активируется, когда пользователь пытается войти в систему. Сначала она проверяет, существует ли имя пользователя в базе данных, возвращая ошибку, если оно не найдено. В реальном сценарии вы могли бы предложить создать новый аккаунт.

Для существующих пользователей она извлекает ID пользователя из базы данных, сохраняет его в сессии и генерирует опции PublicKeyCredentialRequestOptions. allowCredentials остается пустым, чтобы не ограничивать использование учетных данных. Поэтому в модальном окне passkey могут быть выбраны все доступные ключи доступа для этой проверяющей стороны.

Сгенерированный вызов также сохраняется в сессии, а PublicKeyCredentialRequestOptions отправляются обратно на фронтенд.

PublicKeyCredentialRequestOptions состоят из следующих данных:

dictionary PublicKeyCredentialRequestOptions { required BufferSource challenge; unsigned long timeout; USVString rpId; sequence<PublicKeyCredentialDescriptor> allowCredentials = []; DOMString userVerification = "preferred"; AuthenticationExtensionsClientInputs extensions; };
  • challenge: Безопасное, случайное значение от сервера WebAuthn, используемое для предотвращения атак повторного воспроизведения во время процесса аутентификации.
  • timeout: Необязательно, устанавливает время в миллисекундах, которое есть у пользователя для ответа на запрос аутентификации.
  • rpId: ID проверяющей стороны, обычно домен сервиса.
  • allowCredentials: Необязательный список дескрипторов учетных данных, указывающий, какие учетные данные могут быть использованы для этой аутентификации.
  • userVerification: Указывает требование к верификации пользователя, например, "required", "preferred" или "discouraged".
  • extensions: Необязательно, позволяет использовать дополнительные клиентские расширения.

2. Конечная точка handleLoginFinish:

Эта конечная точка извлекает currentChallenge и loggedInUserId из сессии.

Она запрашивает в базе данных нужное учетное данное, используя ID учетного данного из тела запроса. Если учетное данное найдено, это означает, что пользователь, связанный с этим ID учетного данного, теперь может быть аутентифицирован. Затем мы можем запросить пользователя из таблицы пользователей по ID пользователя, который мы получаем из учетного данного, и проверить authenticationResponse, используя вызов и тело запроса. Если все успешно, мы показываем сообщение об успешном входе. Если соответствующее учетное данное не найдено, отправляется ошибка.

Кроме того, если проверка проходит успешно, счетчик учетного данного обновляется, а использованный вызов и loggedInUserId удаляются из сессии.

Кроме того, мы можем удалить папки src/app и src/constant вместе со всеми файлами в них.

Примечание: Правильное управление сессиями и защита маршрутов, критически важные в реальных приложениях, здесь опущены для простоты в этом руководстве.

5.8 Маршруты для ключей доступа#

И последнее, но не менее важное: нам нужно убедиться, что наши контроллеры доступны, добавив соответствующие маршруты в routes.ts, который находится в новом каталоге 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. Интеграция ключей доступа во фронтенд#

Эта часть руководства по ключам доступа посвящена тому, как поддерживать passkeys на фронтенде вашего приложения. У нас очень простой фронтенд, состоящий из трех файлов: index.html, styles.css и script.js. Все три файла находятся в новой папке src/public.

Файл index.html содержит поле ввода для имени пользователя и две кнопки для регистрации и входа. Кроме того, мы импортируем скрипт @simplewebauthn/browser, который упрощает взаимодействие с API веб-аутентификации браузера в файле js/script.js.

index.html выглядит так:

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 выглядит следующим образом:

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

В script.js есть три основные функции:

1. Функция showMessage:

Это вспомогательная функция, используемая в основном для отображения сообщений об ошибках, что помогает в отладке.

2. Функция Register:

Запускается, когда пользователь нажимает «Register». Она извлекает имя пользователя из поля ввода и отправляет его на конечную точку passkeyRegisterStart. Ответ включает PublicKeyCredentialCreationOptions, которые преобразуются в JSON и передаются в SimpleWebAuthnBrowser.startRegistration. Этот вызов активирует аутентификатор устройства (например, Face ID или Touch ID). После успешной локальной аутентификации подписанный вызов отправляется обратно на конечную точку passkeyRegisterFinish, завершая процесс создания ключа доступа.

Во время процесса регистрации объект аттестации играет решающую роль, поэтому давайте рассмотрим его поближе.

Объект аттестации в основном состоит из трех компонентов: fmt, attStmt и authData. Элемент fmt обозначает формат заявления об аттестации, а attStmt представляет собой само заявление об аттестации. В сценариях, где аттестация считается ненужной, fmt будет обозначен как "none", что приведет к пустому attStmt.

Основное внимание уделяется сегменту authData в этой структуре. Этот сегмент является ключом для извлечения на нашем сервере таких важных элементов, как ID проверяющей стороны, флаги, счетчик и данные аттестованных учетных данных. Что касается флагов, особый интерес представляют BS (Backup State) и BE (Backup Eligibility), которые предоставляют больше информации о том, синхронизирован ли ключ доступа (например, через iCloud Keychain или 1Password). Кроме того, UV (User Verification) и UP (User Presence) предоставляют более полезную информацию.

Важно отметить, что различные части объекта аттестации, включая данные аутентификатора, ID проверяющей стороны и заявление об аттестации, либо хэшируются, либо подписываются цифровой подписью аутентификатора с использованием его закрытого ключа. Этот процесс является неотъемлемой частью поддержания общей целостности объекта аттестации.

3. Функция Login:

Активируется, когда пользователь нажимает «Login». Подобно функции регистрации, она извлекает имя пользователя и отправляет его на конечную точку passkeyLoginStart. Ответ, содержащий PublicKeyCredentialRequestOptions, преобразуется в JSON и используется с SimpleWebAuthnBrowser.startAuthentication. Это запускает локальную аутентификацию на устройстве. Затем подписанный вызов отправляется обратно на конечную точку passkeyLoginFinish. Успешный ответ от этой конечной точки указывает на то, что пользователь успешно вошел в приложение.

Кроме того, сопутствующий CSS-файл предоставляет простое оформление для приложения:

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. Запуск примера приложения с ключами доступа#

Чтобы увидеть ваше приложение в действии, скомпилируйте и запустите ваш TypeScript-код с помощью:

npm run dev

Ваш сервер теперь должен быть запущен и работать по адресу http://localhost:8080.

Соображения для производственной среды:

Помните, что мы рассмотрели базовый план. При развертывании приложения с ключами доступа в производственной среде вам необходимо глубже изучить:

  • Меры безопасности: Внедрите надежные практики безопасности для защиты данных пользователей.
  • Обработка ошибок: Убедитесь, что ваше приложение корректно обрабатывает и регистрирует ошибки.
  • Управление базой данных: Оптимизируйте операции с базой данных для масштабируемости и надежности.

8. Интеграция DevOps для ключей доступа#

Мы уже настроили Docker-контейнер для нашей базы данных. Далее мы расширим нашу конфигурацию Docker Compose, включив в нее сервер с бэкендом и фронтендом. Ваш файл docker-compose.yml должен быть обновлен соответствующим образом.

Чтобы контейнеризировать наше приложение, мы создаем новый Dockerfile, который устанавливает необходимые пакеты и запускает сервер разработки:

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

Затем мы также расширяем файл docker-compose.yml, чтобы запустить этот контейнер:

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

Если вы теперь выполните docker compose up в своем терминале и перейдете по адресу http://localhost:8080, вы должны увидеть рабочую версию вашего веб-приложения с ключами доступа (здесь запущено на Windows 11 23H2 + Chrome 119):

9. Дополнительные советы по ключам доступа для разработчиков#

Поскольку мы уже довольно долго работаем с реализациями ключей доступа, мы столкнулись с рядом проблем при работе с реальными приложениями passkey:

  • Совместимость и поддержка устройств / платформ
  • Онбординг и обучение пользователей
  • Обработка утерянных или измененных устройств
  • Кроссплатформенная аутентификация
  • Резервные механизмы
  • Сложность кодирования: кодирование часто является самой сложной частью, так как приходится иметь дело с JSON, CBOR, uint8arrays, буферами, блобами, различными базами данных, base64 и base64url, где может возникнуть много ошибок
  • Управление ключами доступа (например, для добавления, удаления или переименования ключей доступа)

Кроме того, у нас есть следующие советы для разработчиков, когда дело доходит до части реализации:

Используйте Passkeys Debugger

Passkeys debugger помогает тестировать различные настройки сервера WebAuthn и ответы клиента. Кроме того, он предоставляет отличный парсер для ответов аутентификатора.

Отладка с помощью функции Chrome Device Log

Используйте лог устройства Chrome (доступен по адресу chrome://device-log/) для мониторинга вызовов FIDO/WebAuthn. Эта функция предоставляет логи процесса аутентификации в реальном времени, позволяя вам видеть обмениваемые данные и устранять возникающие проблемы.

Еще один очень полезный ярлык для доступа ко всем вашим ключам доступа в Chrome — это chrome://settings/passkeys.

Используйте виртуальный аутентификатор WebAuthn в Chrome

Чтобы избежать использования запросов Touch ID, Face ID или Windows Hello во время разработки, в Chrome есть очень удобный виртуальный аутентификатор WebAuthn, который эмулирует реальный аутентификатор. Мы настоятельно рекомендуем использовать его для ускорения работы. Подробнее можно узнать здесь.

Тестируйте на разных платформах и в разных браузерах

Обеспечьте совместимость и функциональность на различных браузерах и платформах. WebAuthn ведет себя по-разному в разных браузерах, поэтому тщательное тестирование является ключевым.

Тестируйте на разных устройствах

Здесь особенно полезно работать с такими инструментами, как ngrok, с помощью которых вы можете сделать ваше локальное приложение доступным на других (мобильных) устройствах.

Установите для верификации пользователя значение "Preferred"

При определении свойств для userVerification в PublicKeyCredentialRequestOptions, выберите значение preferred, так как это хороший компромисс между удобством использования и безопасностью. Это означает, что проверки безопасности будут действовать на подходящих устройствах, в то время как на устройствах без биометрических возможностей сохранится удобство для пользователя.

10. Заключение: руководство по ключам доступа#

Мы надеемся, что это руководство по ключам доступа дало вам ясное понимание того, как эффективно их внедрять. В ходе руководства мы рассмотрели основные шаги по созданию приложения с ключами доступа, сосредоточившись на фундаментальных концепциях и практической реализации. Хотя это руководство служит отправной точкой, в мире WebAuthn есть еще много чего исследовать и совершенствовать.

Мы призываем разработчиков глубже погрузиться в нюансы ключей доступа (например, добавление нескольких ключей доступа, проверка готовности устройств к работе с passkeys или предложение решений для восстановления). Это путешествие, которое стоит предпринять, предлагая как вызовы, так и огромные преимущества в улучшении аутентификации пользователей. С ключами доступа вы не просто создаете новую функцию; вы вносите свой вклад в более безопасный и удобный для пользователя цифровой мир.

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