feat: align with upstream 1.5.9.9
This commit is contained in:
@@ -0,0 +1,227 @@
|
||||
import {
|
||||
base64URLStringToBuffer,
|
||||
bufferToBase64URLString,
|
||||
startAuthentication
|
||||
} from '@simplewebauthn/browser'
|
||||
|
||||
const toCredentialDescriptor = ({ id, type }) => ({
|
||||
id: base64URLStringToBuffer(id),
|
||||
type
|
||||
})
|
||||
|
||||
const createWebAuthnError = (code, cause) => {
|
||||
const error = new Error(code)
|
||||
error.code = code
|
||||
error.cause = cause
|
||||
return error
|
||||
}
|
||||
|
||||
const normalizeAuthenticationError = (error) => {
|
||||
switch (error?.name) {
|
||||
case 'NotAllowedError':
|
||||
case 'AbortError':
|
||||
return createWebAuthnError('passkeyOperationUnavailable', error)
|
||||
case 'NotSupportedError':
|
||||
return createWebAuthnError('passkeyLoginUnsupported', error)
|
||||
case 'SecurityError':
|
||||
return createWebAuthnError('passkeySecurityInvalid', error)
|
||||
default:
|
||||
return createWebAuthnError('passkeyFailed', error)
|
||||
}
|
||||
}
|
||||
|
||||
const normalizeRegistrationError = (error) => {
|
||||
let code
|
||||
|
||||
switch (error?.name) {
|
||||
case 'NotAllowedError':
|
||||
case 'AbortError':
|
||||
code = 'passkeyOperationUnavailable'
|
||||
break
|
||||
case 'InvalidStateError':
|
||||
code = 'passkeyAlreadyRegistered'
|
||||
break
|
||||
case 'NotSupportedError':
|
||||
code = 'passkeyAlgorithmUnsupported'
|
||||
break
|
||||
case 'SecurityError':
|
||||
code = 'passkeySecurityInvalid'
|
||||
break
|
||||
default:
|
||||
code = 'addDeviceFailed'
|
||||
}
|
||||
|
||||
const normalizedError = createWebAuthnError(code, error)
|
||||
normalizedError.name = error.name
|
||||
return normalizedError
|
||||
}
|
||||
|
||||
export const getWebAuthnErrorMessage = (error, messages, fallback) => {
|
||||
const localizedMessage = error?.code ? messages?.[error.code] : ''
|
||||
return localizedMessage || error?.data?.message || error?.statusMessage || fallback
|
||||
}
|
||||
|
||||
const readClientExtensionResults = (credential) => {
|
||||
try {
|
||||
return credential.getClientExtensionResults?.() || {}
|
||||
} catch (error) {
|
||||
// 鸿蒙早期实现可能缺少完整扩展接口,扩展结果不参与服务端签名验证。
|
||||
console.warn('读取 WebAuthn 扩展结果失败:', error)
|
||||
return {}
|
||||
}
|
||||
}
|
||||
|
||||
const readOptionalResponseValue = (response, methodName) => {
|
||||
try {
|
||||
return response[methodName]?.()
|
||||
} catch (error) {
|
||||
// 部分系统凭据桥仅实现必需字段,可选元数据失败不应中断认证。
|
||||
console.warn(`读取 WebAuthn ${methodName} 失败:`, error)
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
export const signalUnknownWebAuthnCredential = async ({ credentialId, rpId }) => {
|
||||
if (typeof window === 'undefined') return false
|
||||
|
||||
const signalUnknownCredential = window.PublicKeyCredential?.signalUnknownCredential
|
||||
if (!credentialId || !rpId || typeof signalUnknownCredential !== 'function') {
|
||||
return false
|
||||
}
|
||||
|
||||
try {
|
||||
await signalUnknownCredential.call(window.PublicKeyCredential, {
|
||||
credentialId,
|
||||
rpId
|
||||
})
|
||||
return true
|
||||
} catch (error) {
|
||||
// 设备可能只实现基础 WebAuthn,清理失败不能影响服务端解绑结果。
|
||||
console.warn('通知系统清理失效 Passkey 失败:', error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
export const startWebAuthnAuthentication = async (optionsJSON, useBrowserAutofill = false) => {
|
||||
if (useBrowserAutofill) {
|
||||
return startAuthentication({ optionsJSON, useBrowserAutofill: true })
|
||||
}
|
||||
|
||||
if (!navigator.credentials?.get) {
|
||||
throw createWebAuthnError('passkeyLoginUnsupported')
|
||||
}
|
||||
|
||||
const { challenge, allowCredentials, ...remainingOptions } = optionsJSON
|
||||
const publicKey = {
|
||||
...remainingOptions,
|
||||
challenge: base64URLStringToBuffer(challenge)
|
||||
}
|
||||
|
||||
if (allowCredentials?.length) {
|
||||
publicKey.allowCredentials = allowCredentials.map(toCredentialDescriptor)
|
||||
}
|
||||
|
||||
// 部分鸿蒙版本的 WebAuthn 桥接会拒绝 AbortSignal,登录按钮本身已阻止重复提交。
|
||||
let credential
|
||||
try {
|
||||
credential = await navigator.credentials.get({ publicKey })
|
||||
} catch (error) {
|
||||
throw normalizeAuthenticationError(error)
|
||||
}
|
||||
if (!credential?.response) {
|
||||
throw createWebAuthnError('passkeyAuthenticationIncomplete')
|
||||
}
|
||||
|
||||
const response = credential.response
|
||||
|
||||
return {
|
||||
id: credential.id,
|
||||
rawId: bufferToBase64URLString(credential.rawId),
|
||||
response: {
|
||||
authenticatorData: bufferToBase64URLString(response.authenticatorData),
|
||||
clientDataJSON: bufferToBase64URLString(response.clientDataJSON),
|
||||
signature: bufferToBase64URLString(response.signature),
|
||||
userHandle: response.userHandle ? bufferToBase64URLString(response.userHandle) : undefined
|
||||
},
|
||||
type: credential.type,
|
||||
clientExtensionResults: readClientExtensionResults(credential),
|
||||
authenticatorAttachment: credential.authenticatorAttachment || undefined
|
||||
}
|
||||
}
|
||||
|
||||
export const startWebAuthnRegistration = async (optionsJSON) => {
|
||||
if (!navigator.credentials?.create) {
|
||||
throw createWebAuthnError('passkeyRegistrationUnsupported')
|
||||
}
|
||||
|
||||
const {
|
||||
challenge,
|
||||
user,
|
||||
excludeCredentials,
|
||||
hints,
|
||||
extensions,
|
||||
authenticatorSelection,
|
||||
...remainingOptions
|
||||
} = optionsJSON
|
||||
const compatibleExtensions = { ...extensions }
|
||||
const compatibleAuthenticatorSelection = { ...authenticatorSelection }
|
||||
|
||||
// 这些提示不参与服务端验证,部分鸿蒙凭据桥会因不认识它们而拒绝整个请求。
|
||||
delete compatibleExtensions.credProps
|
||||
delete compatibleAuthenticatorSelection.residentKey
|
||||
|
||||
const publicKey = {
|
||||
...remainingOptions,
|
||||
challenge: base64URLStringToBuffer(challenge),
|
||||
user: {
|
||||
...user,
|
||||
id: base64URLStringToBuffer(user.id)
|
||||
}
|
||||
}
|
||||
|
||||
if (Object.keys(compatibleExtensions).length) {
|
||||
publicKey.extensions = compatibleExtensions
|
||||
}
|
||||
if (Object.keys(compatibleAuthenticatorSelection).length) {
|
||||
publicKey.authenticatorSelection = compatibleAuthenticatorSelection
|
||||
}
|
||||
|
||||
if (excludeCredentials?.length) {
|
||||
publicKey.excludeCredentials = excludeCredentials.map(toCredentialDescriptor)
|
||||
}
|
||||
if (hints?.length) {
|
||||
publicKey.hints = hints
|
||||
}
|
||||
|
||||
// 部分鸿蒙版本的 WebAuthn 桥接会拒绝 AbortSignal,绑定按钮本身已阻止重复提交。
|
||||
let credential
|
||||
try {
|
||||
credential = await navigator.credentials.create({ publicKey })
|
||||
} catch (error) {
|
||||
throw normalizeRegistrationError(error)
|
||||
}
|
||||
if (!credential?.response) {
|
||||
throw createWebAuthnError('passkeyCreationIncomplete')
|
||||
}
|
||||
|
||||
const response = credential.response
|
||||
const publicKeyAlgorithm = readOptionalResponseValue(response, 'getPublicKeyAlgorithm')
|
||||
const credentialPublicKey = readOptionalResponseValue(response, 'getPublicKey')
|
||||
const authenticatorData = readOptionalResponseValue(response, 'getAuthenticatorData')
|
||||
|
||||
return {
|
||||
id: credential.id,
|
||||
rawId: bufferToBase64URLString(credential.rawId),
|
||||
response: {
|
||||
attestationObject: bufferToBase64URLString(response.attestationObject),
|
||||
clientDataJSON: bufferToBase64URLString(response.clientDataJSON),
|
||||
transports: readOptionalResponseValue(response, 'getTransports'),
|
||||
publicKeyAlgorithm,
|
||||
publicKey: credentialPublicKey ? bufferToBase64URLString(credentialPublicKey) : undefined,
|
||||
authenticatorData: authenticatorData ? bufferToBase64URLString(authenticatorData) : undefined
|
||||
},
|
||||
type: credential.type,
|
||||
clientExtensionResults: readClientExtensionResults(credential),
|
||||
authenticatorAttachment: credential.authenticatorAttachment || undefined
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user