使用安全付款确认功能进行身份验证

商家可以将安全付款确认 (SPC) 作为针对给定信用卡或银行账户的增强型客户身份验证 (SCA) 流程的一部分。WebAuthn 执行身份验证(通常通过生物识别)。您必须预先注册 WebAuthn,如需了解详情,请参阅注册安全付款确认

典型实现的工作原理

SPC 最常见的用途是,客户在商家网站上购物时,信用卡发卡机构或银行要求进行付款人身份验证。

身份验证工作流。

我们来详细了解一下身份验证流程:

  1. 客户向商家提供其付款凭据(例如信用卡信息)。
  2. 如果付款人需要单独进行身份验证,商家会询问付款凭据的相应发卡机构或银行(依赖方或 RP)。例如,在 EMV® 3-D Secure 中就可能会发生这种交换。
    • 如果 RP 希望商家使用 SPC,并且用户之前已注册,则 RP 会在响应中提供付款人注册的凭据 ID 列表和质询。
    • 如果不需要进行身份验证,商家可以继续完成交易。
  3. 如果需要进行身份验证,商家会确定浏览器是否支持 SPC
    • 如果浏览器不支持 SPC,请继续执行现有身份验证流程。
  4. 商家调用 SPC。浏览器会显示一个确认对话框。
    • 如果 RP 未传递任何凭据 ID,则回退到现有身份验证流程。成功完成身份验证后,不妨考虑使用 SPC 注册来简化日后的身份验证
  5. 用户解锁设备,确认并验证付款金额和收款人。
  6. 商家会从身份验证中收到凭据。
  7. RP 从商家接收凭据并验证其真实性。
  8. RP 会将验证结果发送给商家。
  9. 商家会向用户显示一条消息,指明付款是成功还是失败。

功能检测

如需检测浏览器是否支持 SPC,您可以向 canMakePayment() 发送虚假调用。

复制并粘贴以下代码,以便在商家网站上检测 SPC 功能。

const isSecurePaymentConfirmationSupported = async () => {
  if (!'PaymentRequest' in window) {
    return [false, 'Payment Request API is not supported'];
  }

  try {
    // The data below is the minimum required to create the request and
    // check if a payment can be made.
    const supportedInstruments = [
      {
        supportedMethods: "secure-payment-confirmation",
        data: {
          // RP's hostname as its ID
          rpId: 'rp.example',
          // A dummy credential ID
          credentialIds: [new Uint8Array(1)],
          // A dummy challenge
          challenge: new Uint8Array(1),
          instrument: {
            // Non-empty display name string
            displayName: ' ',
            // Transparent-black pixel.
            icon: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+P+/HgAFhAJ/wlseKgAAAABJRU5ErkJggg==',
          },
          // A dummy merchant origin
          payeeOrigin: 'https://non-existent.example',
        }
      }
    ];

    const details = {
      // Dummy shopping details
      total: {label: 'Total', amount: {currency: 'USD', value: '0'}},
    };

    const request = new PaymentRequest(supportedInstruments, details);
    const canMakePayment = await request.canMakePayment();
    return [canMakePayment, canMakePayment ? '' : 'SPC is not available'];
  } catch (error) {
    console.error(error);
    return [false, error.message];
  }
};

isSecurePaymentConfirmationSupported().then(result => {
  const [isSecurePaymentConfirmationSupported, reason] = result;
  if (isSecurePaymentConfirmationSupported) {
    // Display the payment button that invokes SPC.
  } else {
    // Fallback to the legacy authentication method.
  }
});

对用户进行身份验证

如需对用户进行身份验证,请使用 secure-payment-confirmation 和 WebAuthn 参数调用 PaymentRequest.show() 方法:

以下是您应向付款方式的 data 属性 SecurePaymentConfirmationRequest 提供的参数。

参数 说明
rpId 将 RP 源的主机名用作 RP ID。
challenge 用于防范重放攻击的随机质询。
credentialIds 凭据 ID 的数组。在 WebAuthn 身份验证中,allowCredentials 属性接受 PublicKeyCredentialDescriptor 对象的数组,但在 SPC 中,您只需传递凭据 ID 列表。
payeeName(可选) 收款人的姓名。
payeeOrigin 收款人来源。在上述场景中,它是商家的来源。
instrument displayName 的字符串和指向图片资源的 icon 的网址。iconMustBeShown 的可选布尔值(默认为 true),用于指定必须成功提取并显示图标,请求才能成功。
timeout 签署交易的超时时间(以毫秒为单位)
extensions 向 WebAuthn 调用添加了扩展程序。您无需自行指定“payment”扩展程序。

请查看以下示例代码:

// After confirming SPC is available on this browser via a feature detection,
// fetch the request options cross-origin from the RP server.
const options = fetchFromServer('https://rp.example/spc-auth-request');
const { credentialIds, challenge } = options;

const request = new PaymentRequest([{
  // Specify `secure-payment-confirmation` as payment method.
  supportedMethods: "secure-payment-confirmation",
  data: {
    // The RP ID
    rpId: 'rp.example',

    // List of credential IDs obtained from the RP server.
    credentialIds,

    // The challenge is also obtained from the RP server.
    challenge,

    // A display name and an icon that represent the payment instrument.
    instrument: {
      displayName: "Fancy Card ****1234",
      icon: "https://rp.example/card-art.png",
      iconMustBeShown: false
    },

    // The origin of the payee (merchant)
    payeeOrigin: "https://merchant.example",

    // The number of milliseconds to timeout.
    timeout: 360000,  // 6 minutes
  }
}], {
  // Payment details.
  total: {
    label: "Total",
    amount: {
      currency: "USD",
      value: "5.00",
    },
  },
});

try {
  const response = await request.show();

  // response.details is a PublicKeyCredential, with a clientDataJSON that
  // contains the transaction data for verification by the issuing bank.
  // Make sure to serialize the binary part of the credential before
  // transferring to the server.
  const result = fetchFromServer('https://rp.example/spc-auth-response', response.details);
  if (result.success) {
    await response.complete('success');
  } else {
    await response.complete('fail');
  }
} catch (err) {
  // SPC cannot be used; merchant should fallback to traditional flows
  console.error(err);
}

.show() 函数会生成 PaymentResponse 对象,但 details 包含一个包含公钥凭据的 clientDataJSON,其中包含要由 RP 进行验证的事务数据 (payment)。

生成的凭据必须跨源传输到 RP 并进行验证。

RP 如何验证交易

在 RP 服务器上验证交易数据是付款流程中最重要的一步。

如需验证交易数据,RP 可以遵循 WebAuthn 的身份验证断言验证流程。此外,他们还需要验证 payment

clientDataJSON 的示例载荷:

{
  "type":"payment.get",
  "challenge":"SAxYy64IvwWpoqpr8JV1CVLHDNLKXlxbtPv4Xg3cnoc",
  "origin":"https://spc-merchant.glitch.me",
  "crossOrigin":false,
  "payment":{
    "rp":"spc-rp.glitch.me",
    "topOrigin":"https://spc-merchant.glitch.me",
    "payeeOrigin":"https://spc-merchant.glitch.me",
    "total":{
      "value":"15.00",
      "currency":"USD"
    },
    "instrument":{
      "icon":"https://cdn.glitch.me/94838ffe-241b-4a67-a9e0-290bfe34c351%2Fbank.png?v=1639111444422",
      "displayName":"Fancy Card 825809751248"
    }
  }
}
  • rp 与 RP 的来源相匹配。
  • topOrigin 与 RP 预期的顶级源(上例中的商家源)匹配。
  • payeeOrigin 与应向用户显示的收款人来源一致。
  • total 与应向用户显示的交易金额一致。
  • instrument 与应向用户显示的付款工具详细信息相匹配。
const clientData = base64url.decode(response.clientDataJSON);
const clientDataJSON = JSON.parse(clientData);

if (!clientDataJSON.payment) {
  throw 'The credential does not contain payment payload.';
}

const payment = clientDataJSON.payment;
if (payment.rp !== expectedRPID ||
    payment.topOrigin !== expectedOrigin ||
    payment.payeeOrigin !== expectedOrigin ||
    payment.total.value !== '15.00' ||
    payment.total.currency !== 'USD') {
  throw 'Malformed payment information.';
}

通过所有验证条件后,RP 可以告知商家交易已成功。

后续步骤