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

Eiji Kitamura
Eiji Kitamura

对于特定的信用卡或银行账户,商家可以在强健的客户身份验证 (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 可以告知商家交易成功。

后续步骤