@auth-webauthn( 低调)
Cloudflare Workers / Pages 的 WebAuthn (passkey) 供应商. 跟着一样
{ type, ... }提供者形状为@basenative/auth,但没有依赖它。 滴入密码登录,有可插件存储和微小浏览器客户端.
部分 基础 生态系统。 从经过生产测试的密码实施中取消 t4bs (单位:千美元)。 。 。 .
为什么
现代道具(英語:Modern passkey auth)是一款有不少相纸剪接器的少量密码. 这个软件包处理剪纸(挑战TTL, cookie旗, Base64url 编码, WebAuthn 版本漂移,角色种子),所以您的应用代码仍然小.
安装
pnpm add @basenative/auth-webauthn @simplewebauthn/server
# Browser-side (only on the client):
pnpm add @simplewebauthn/browser
@simplewebauthn/server 被装入懒惰的- 同伴在第一次使用前不会被拉入你的工人捆绑.
快速启动( Cloudflares 页面函数+ D1)
1. 约束wrangler.toml(中文(简体) )
[[d1_databases]]
binding = "DB"
database_name = "myapp-db"
database_id = "..."
[vars]
RP_NAME = "MyApp"
RP_ID = "myapp.com" # use "localhost" in dev
RP_ORIGIN = "https://myapp.com" # use "http://localhost:8788" in dev
ADMIN_HANDLES = "warren,bob" # optional — see "Role seeding"
2. 应用计划
wrangler d1 execute DB --local --file=./node_modules/@basenative/auth-webauthn/migrations/0001_webauthn_schema.sql
wrangler d1 execute DB --remote --file=./node_modules/@basenative/auth-webauthn/migrations/0001_webauthn_schema.sql
或者把SQL复制到你自己的 migrations/ 目录.
3. 接通6名操作员
每条路由创建一个文件 functions/api/auth/它们都是一样的形状:
// functions/api/auth/_adapter.js
import {
webauthnAdapter,
d1WebAuthnStores,
} from '@basenative/auth-webauthn';
export const getAdapter = (env) =>
webauthnAdapter({
rp: { rpName: env.RP_NAME, rpID: env.RP_ID, origin: env.RP_ORIGIN },
stores: d1WebAuthnStores(env.DB),
});
// functions/api/auth/register-options.js
import { registerOptionsHandler } from '@basenative/auth-webauthn/handlers';
import { getAdapter } from './_adapter.js';
export const onRequestPost = registerOptionsHandler(getAdapter);
// functions/api/auth/register-verify.js
import { registerVerifyHandler } from '@basenative/auth-webauthn/handlers';
import { getAdapter } from './_adapter.js';
export const onRequestPost = registerVerifyHandler(getAdapter);
// functions/api/auth/login-options.js
import { loginOptionsHandler } from '@basenative/auth-webauthn/handlers';
import { getAdapter } from './_adapter.js';
export const onRequestPost = loginOptionsHandler(getAdapter);
// functions/api/auth/login-verify.js
import { loginVerifyHandler } from '@basenative/auth-webauthn/handlers';
import { seedRoles, parseHandleList, d1WebAuthnStores } from '@basenative/auth-webauthn';
import { getAdapter } from './_adapter.js';
export const onRequestPost = loginVerifyHandler(getAdapter, {
onLogin: async ({ env, user }) => {
if (!user) return;
const stores = d1WebAuthnStores(env.DB);
await seedRoles({
stores,
user,
seedMap: parseHandleList(env.ADMIN_HANDLES, 'admin'),
changedBy: 'seed:ADMIN_HANDLES',
});
},
});
// functions/api/auth/me.js
import { meHandler } from '@basenative/auth-webauthn/handlers';
import { getAdapter } from './_adapter.js';
export const onRequestGet = meHandler(getAdapter);
// functions/api/auth/logout.js
import { logoutHandler } from '@basenative/auth-webauthn/handlers';
import { getAdapter } from './_adapter.js';
export const onRequestPost = logoutHandler(getAdapter);
这是整个服务器侧.
客户端使用量
import {
isPasskeySupported,
registerPasskey,
loginPasskey,
me,
logout,
} from '@basenative/auth-webauthn/client';
if (isPasskeySupported()) {
await registerPasskey('alice'); // → /me payload
await loginPasskey('alice'); // or loginPasskey('') for usernameless
const { user } = await me();
await logout();
}
自定义端点路径 :
await registerPasskey('alice', {
paths: {
registerOptions: '/auth/passkey/register/start',
registerVerify: '/auth/passkey/register/finish',
me: '/auth/whoami',
},
});
角色种子
如果您的应用程序有管理员/ 调制解调器, 请将它们列在一个 env var( 或任何地方) 中 。 登录时, 自动升级匹配控件 :
import { seedRoles, parseHandleList } from '@basenative/auth-webauthn';
await seedRoles({
stores,
user,
seedMap: parseHandleList(env.ADMIN_HANDLES, 'admin'),
changedBy: 'seed:ADMIN_HANDLES',
});
当用户已经处于或高于目标角色时——管理员不会被降级,这是不公开的. 使用 seedMap 直接用于任意绘图{ alice: 'admin', mod-bob: 'moderator' }) (中文(简体) ).
存储接口(合同)
webauthnAdapter 使用任意 stores 与这四个成员为对象:
interface WebAuthnStores {
users: {
getByHandle(handle): Promise<User | null>;
getById(id): Promise<User | null>;
create({ id, handle }): Promise<User>; // returns { id, handle, role: 'user' }
setRole?(id, role, changedBy): Promise<void>; // optional, used by seedRoles
};
credentials: {
listByUser(userId): Promise<Credential[]>;
getById(credId): Promise<Credential & { userId } | null>;
create({ id, userId, publicKey, counter, transports }): Promise<void>;
updateCounter(id, counter): Promise<void>;
};
challenges: {
create({ challenge, userId, purpose, ttlSeconds }): Promise<void>;
consume(challenge, purpose): Promise<{ userId } | null>; // single-use; deletes on read
};
userSessions: {
create({ id, userId, ttlSeconds }): Promise<void>;
getUser(token): Promise<User | null>; // returns null if expired
destroy(token): Promise<void>;
};
}
d1WebAuthnStores(DB) 是引用执行; 将您自己写为 Postgres、 MySQL、 Redis 或 持久对象。 这个 migration 常数导出自 @basenative/auth-webauthn/d1-stores 包含与迁移文件相同的 SQL,如果您想要在程序上应用它.
配置
webauthnAdapter({
rp: {
rpName: 'MyApp',
rpID: 'myapp.com',
origin: 'https://myapp.com',
},
stores,
ttl: {
sessionSeconds: 30 * 24 * 3600, // default
challengeSeconds: 300, // default; clamped to ≤ 300
},
cookieName: 'bn_auth', // default
secureCookie: true, // default; set false for localhost http://
userVerification: 'preferred', // default; also 'required' | 'discouraged'
});
会话饼干总是 HttpOnly; SameSite=Lax。 。 。 . Secure 默认状态.
userVerification
控制仪式要求(和检查)用户核查的强烈程度—— PIN、生物鉴别技术或设备密码——而不只是用户 存在 (接触) 或水龙头。 一个选项驱动双方,让他们不能不同意:
- 登记:
authenticatorSelection.userVerification - 认证 :
userVerification - 核查:
requireUserVerification: userVerification === 'required'
| 数值 | 仪式要求紫外线 | 核查强制紫外线 |
|---|---|---|
'preferred' (默认) | 是, 如果验证器支持它 | 没有 |
'required' | 对 | 对 - 驳回没有紫外线的说法 |
'discouraged' | 没有 | 没有 |
安全说明: 'preferred' 是默认,因为它符合已有
行为和保持先前登记的每张密码——有些
认证器(特别是更古老的安全密钥)从不执行紫外线,只有用户
存在。 设定 'required' 当你的威胁模型需要第二个因素烘烤
(例如共享设备、高值账户):
更强,但它会拒绝任何认证者认证
无法执行紫外线,包括你打开前注册的证书
继续 只有在确认用户的验证器支持紫外线后,才能推出,
或与重新注册的瞬间.
移动已存在的工程
从 express-session / cookie-session(待定)
如果您的当前认证像 :
// before
app.use(session({ secret, store: new RedisStore({ ... }) }));
app.post('/login', async (req, res) => {
if (await checkPassword(req.body.email, req.body.password)) {
req.session.user = await db.users.findByEmail(req.body.email);
res.redirect('/');
} else {
res.status(401).send('bad-creds');
}
});
app.get('/me', (req, res) => res.json({ user: req.session.user || null }));
地图是:
| 专会 | @basenative/auth-webauthn |
|---|---|
req.session.user (由中件装入) | await adapter.currentUser(request) |
req.session.user = ... (登录) | verifyAuthentication 返回会话符号 |
req.session.destroy() (挂断) | adapter.destroySession(token) |
RedisStore 编号 : MemoryStore | 互换 stores.userSessions 对于一个重新支持的 |
secret (签名的饼干) | 不需要 — 会话符号不透明 + 随机 |
分步迁移:
- 入诸法相. 运行
migrations/0001_webauthn_schema.sql对你的DB。 如果你已经有一个users表格中,替换CREATE TABLE users与ALTER TABLE添加语句role, (中文(简体) ),role_changed_at, (中文(简体) ),role_changed_by。保留您现有的email, (中文(简体) ),password_hash等——仅用密码就不需要放下. - 选一柄地. 待办业务可能通过电子邮件进行认证;适配器根据
^[a-z0-9_-]{2,24}$。或者暴露一个单独的handle字段或覆盖validHandle写一个小的自定义存储层, 将邮件排出或规范化 。 (大多数应用程序在初始登录时会添加一次性"选择用户名"步骤;用户喜欢它比电子邮件更喜欢作为登录ID. ) - 平行运行两个认证 . 上载新
/api/auth/*与现有/login路线。 在账户设置页面上添加一个“ 设置密码” 按钮registerPasskey(currentUser.handle)。现有的基于密码的登录继续工作. - 移动会话读取 . 在你身边
req.session.user切换到await adapter.currentUser(req)。如果你在快递上,请写一个薄的中间软件:
适配器是运行时间不可知的——它只需要一个app.use(async (req, _res, next) => { req.user = await adapter.currentUser(req); next(); });Request- 形状对象.headers.get('cookie'). 包装快递req.headers如果需要的话. - 切过. 一旦每个活跃的用户至少有一个密码,就去掉密码端点. DB行可以留下来——
password_hash成为无用.
从下Auth / Auth.js 发送
这些库已经讲了提供者插件;这个软件包符合同样的模式。 在自定义的下一条Auth提供者内执行适配器呼叫 authorize 打回那个包裹 verifyAuthentication中的饼干名称 webauthnAdapter 如果您想要复用会话, 应当匹配您的下一张Auth cookie, 否则使用不同的名称并调用两者 currentUser 财务报告和审定财务报表 getSession 直到你切过来.
安全说明
- 挑战行为单用并被删除
consume(),无论过期. - TTL的挑战被锁定到 5分钟的服务器侧面 不要试图超越它更高;通过更大值的消费者代码会默默地被夹住.
- 饼干是
HttpOnly; Secure; SameSite=Lax禁用Secure只用于本地- HTTP 开发 . - 错误归来为
{ error: 'reason' }具有数值状态。 原因字符串是稳定的,可以安全地日志;没有包含PII或PII可验证值. seedRoles只有升级。 它永远不会移除admin从用户处,即使种子列表中缺少它们——手动处理角色去除.userVerification默认为'preferred'(紫外线请求,未执行)——见上文“构思”部分'required'权衡取舍.
测试
pnpm --filter @basenative/auth-webauthn test
单位测试注入了假 WebAuthn 原始对象(opts.lib因此,套房运行 没有 @simplewebauthn/server 已安装 。 生产代码从未通过 opts.lib;是内钩.
许可证
阿帕奇 - 2.0