跳转到内容

能力 · API

WebAuthn 与通行密钥(passkeys)

发布于

Web Authentication API 通过 navigator.credentials.create({ publicKey }) 注册一个绑定到认证器的公钥凭据,之后用 navigator.credentials.get({ publicKey }) 证明持有它,于是 PWA 可以让用户用指纹、人脸或设备 PIN 登录,而不是密码。通行密钥(passkey)就是以可发现方式(residentKey: "required")创建的这类凭据,在多数平台上由 iCloud 钥匙串或 Google 密码管理器等凭据管理器同步;签名数据包含源与一次性 challenge,所以在仿冒域名上钓到的凭据无法使用。

PublicKeyCredential 在 Chrome 67、Edge 18、Firefox 60 与 Safari 13 发布;条件式中介(通行密钥自动填充)在 Chrome 108、Safari 16 与 Firefox 119 可用;PublicKeyCredential.getClientCapabilities() 在 Chrome 133、Safari 17.4 与 Firefox 135 可用(BCD api.PublicKeyCredential)。Android WebView 131 暴露 isConditionalMediationAvailable(),但由于缺少自动填充集成,它兑现为 false。各平台的通行密钥同步覆盖情况见 passkeys.dev。

navigator.credentials.create({ publicKey: creationOptions, signal })
navigator.credentials.get({ publicKey: requestOptions, mediation, signal })
PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable()
PublicKeyCredential.isConditionalMediationAvailable()
PublicKeyCredential.getClientCapabilities()
PublicKeyCredential.parseCreationOptionsFromJSON(json)
PublicKeyCredential.parseRequestOptionsFromJSON(json)
credential.toJSON()

create() 以一个 PublicKeyCredential 兑现,其 response 是 AuthenticatorAttestationResponse(clientDataJSON、attestationObject、getPublicKey()、getTransports());get() 兑现的凭据其 response 是 AuthenticatorAssertionResponse(clientDataJSON、authenticatorData、signature、userHandle)。静态的 is…Available() 方法以布尔值兑现,不需要手势。两个仪式都要求安全上下文;跨源 iframe 还需要 publickey-credentials-create 或 publickey-credentials-get Permissions Policy 特性。

create() 在 publicKey 下接受 PublicKeyCredentialCreationOptions;get() 接受 PublicKeyCredentialRequestOptions,再加上外层 CredentialRequestOptions 的 mediation 成员。二进制成员的类型是 BufferSource;JSON 变体接受 base64url 字符串。

字典 成员 类型 必填 说明
创建 rp PublicKeyCredentialRpEntity 是 name(必填)与 id(默认为源的有效域名,必须是它的可注册后缀)。
创建 user PublicKeyCredentialUserEntity 是 id(1 到 64 字节,不透明)、name、displayName。
创建 challenge BufferSource 是 服务端生成的随机字节,回显在 clientDataJSON 中。
创建 pubKeyCredParams sequence<PublicKeyCredentialParameters> 是 { type: "public-key", alg } 条目;-7(ES256)与 -257(RS256)覆盖当前的认证器。
创建 timeout unsigned long 否 毫秒提示值,客户端可能裁剪。
创建 excludeCredentials sequence<PublicKeyCredentialDescriptor> 否 用户已有的凭据 ID;匹配到的认证器以 InvalidStateError 拒绝。
创建 authenticatorSelection AuthenticatorSelectionCriteria 否 authenticatorAttachment("platform" 或 "cross-platform")、residentKey("discouraged"、"preferred"、"required")、requireResidentKey(遗留布尔值)、userVerification("required"、默认 "preferred"、"discouraged")。
创建 hints sequence<DOMString> 否 "security-key"、"client-device"、"hybrid",按偏好排序。
创建 attestation DOMString 否,默认 "none" "none"、"indirect"、"direct"、"enterprise"。
创建 extensions AuthenticationExtensionsClientInputs 否 例如 credProps,用于得知凭据是否成为可发现凭据。
请求 challenge BufferSource 是 新的服务端 challenge。
请求 rpId DOMString 否 必须与创建时使用的 rp.id 一致。
请求 allowCredentials sequence<PublicKeyCredentialDescriptor> 否 留空让认证器自行选择可发现凭据(通行密钥流程)。
请求 userVerification DOMString 否,默认 "preferred" 同上。
请求 timeout、hints、extensions 否 与创建时相同。
外层 mediation CredentialMediationRequirement 否,默认 "optional" "conditional" 在表单自动填充中展示通行密钥并静默等待;"required" 强制弹窗;"silent" 对 publicKey 会被拒绝。
外层 signal AbortSignal 否 中止后,进行中的仪式以 AbortError 拒绝。
异常 条件
NotAllowedError 调用方的源是不透明源;文档与某个祖先跨源且缺少对应的 Permissions Policy 特性;客户端要求瞬时激活而调用时没有;用户拒绝、超时,或找不到可用凭据且用户关闭了对话框;或(create())用户代理近期没有中介过认证且不授予同意。
SecurityError 有效域名不是合法域名,或 rp.id / rpId 既不等于它也不是它的可注册后缀,且关联源检查(/.well-known/webauthn)失败。
TypeError user.id 不在 1 到 64 字节之间。
NotSupportedError pubKeyCredParams 中没有任何一项的类型与算法被客户端支持。
InvalidStateError 某个认证器持有 excludeCredentials 中列出的凭据且用户已同意(这是客户端唯一原样上抛的认证器错误)。
AbortError signal 已中止。
EncodingError parseCreationOptionsFromJSON() 或 parseRequestOptionsFromJSON() 遇到无法解码的值。

InvalidStateError 以外的认证器级状态(认证器没有可发现凭据存储以满足 residentKey: "required"、或不具备用户验证能力时的 ConstraintError,内部失败时的 UnknownError)不会被规范的客户端算法上抛;仪式继续尝试其他认证器,最终在超时时以 NotAllowedError 结束。Chrome 与此不同,会直接报告这些状态(见实测)。

每个示例都先检测 PublicKeyCredential,并给出在没有通行密钥的环境里保留密码登录的分支。challenge、用户 ID 与凭据 ID 由服务端以 base64url 字符串下发,浏览器有 JSON 辅助方法时直接交给它们处理。

注册通行密钥,缺少支持时回退到密码表单

Section titled “注册通行密钥,缺少支持时回退到密码表单”

向服务端请求创建选项,确认存在平台认证器,再把证明(attestation)回传。Chrome 129、Firefox 119 与 Safari 18.4 之前的浏览器没有 parseCreationOptionsFromJSON(),在那里手动解码 base64url。

const fromB64url = (s) => Uint8Array.from(atob(s.replace(/-/g, '+').replace(/_/g, '/')), (c) => c.charCodeAt(0));
async function registerPasskey() {
if (!window.PublicKeyCredential) return showPasswordForm('当前浏览器不支持通行密钥。');
if (!(await PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable())) {
return showPasswordForm('此设备没有平台认证器。');
}
const json = await (await fetch('/webauthn/register/options', { method: 'POST' })).json();
const publicKey = PublicKeyCredential.parseCreationOptionsFromJSON
? PublicKeyCredential.parseCreationOptionsFromJSON(json)
: {
...json,
challenge: fromB64url(json.challenge),
user: { ...json.user, id: fromB64url(json.user.id) },
excludeCredentials: (json.excludeCredentials ?? []).map((c) => ({ ...c, id: fromB64url(c.id) })),
};
try {
const credential = await navigator.credentials.create({ publicKey });
await fetch('/webauthn/register/verify', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify(credential.toJSON ? credential.toJSON() : serialize(credential)),
});
} catch (err) {
if (err.name === 'InvalidStateError') return notify('此设备上已有该账号的通行密钥。');
if (err.name === 'NotAllowedError') return notify('已取消创建通行密钥。');
throw err;
}
}

服务端的选项应设置 authenticatorSelection: { residentKey: "required", userVerification: "preferred" },凭据才是可发现的;否则用户必须先输入用户名,get() 才找得到它。

用通行密钥自动填充登录,并保留弹窗回退

Section titled “用通行密钥自动填充登录,并保留弹窗回退”

mediation: "conditional" 让浏览器把通行密钥列在用户名输入框的自动填充里。请求会一直等到用户选中,所以在页面加载时就发起,并用一个 AbortSignal 配合可见的「用通行密钥登录」按钮:按钮先取消它,再发起弹窗请求。

async function startConditionalSignIn(input, button) {
if (!window.PublicKeyCredential?.isConditionalMediationAvailable) return;
if (!(await PublicKeyCredential.isConditionalMediationAvailable())) return;
input.setAttribute('autocomplete', 'username webauthn');
const controller = new AbortController();
const options = await (await fetch('/webauthn/login/options')).json();
const publicKey = PublicKeyCredential.parseRequestOptionsFromJSON(options);
button.addEventListener('click', async () => {
controller.abort();
const assertion = await navigator.credentials.get({ publicKey, mediation: 'required' });
await verify(assertion);
});
try {
const assertion = await navigator.credentials.get({ publicKey, mediation: 'conditional', signal: controller.signal });
await verify(assertion);
} catch (err) {
if (err.name !== 'AbortError') console.error(`${err.name}: ${err.message}`);
}
}

把 allowCredentials 留空,这样为 rpId 保存的任何通行密钥都能出现;没有通行密钥的用户看到的是普通自动填充,密码路径不受影响。

在平台通行密钥与安全密钥之间取舍

Section titled “在平台通行密钥与安全密钥之间取舍”

hints 告诉浏览器优先展示哪种界面。给员工配发硬件密钥的企业应用,以 "security-key" 开头并要求用户验证;面向消费者的应用用 "client-device",让设备自带的生物识别排在最前。

async function createFor(audience, publicKey) {
if (!window.PublicKeyCredential) throw new Error('WebAuthn 不可用');
const options = {
...publicKey,
hints: audience === 'workforce' ? ['security-key', 'hybrid'] : ['client-device', 'hybrid'],
authenticatorSelection: {
residentKey: 'required',
userVerification: audience === 'workforce' ? 'required' : 'preferred',
},
};
return navigator.credentials.create({ publicKey: options });
}

不支持 hints 的浏览器按 WebIDL 字典规则丢弃该成员,回退是自动的:显示默认选择器。

规范

规范状态
Web Authentication Level 3: create a new credentialW3C
Web Authentication Level 3: get an assertionW3C
Web Authentication Level 3: AuthenticatorSelectionCriteriaW3C