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

对于特定的信用卡或银行账户,商家可以在强健的客户身份验证 (SCA) 流程中使用安全付款确认 (SPC)。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 调用的扩展程序。您无需自行指定“付款”扩展名。

请查看此示例代码:

// 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 可以告知商家交易成功。

后续步骤