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

Eiji Kitamura
北村英二

商家可以使用安全付款确认 (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 调用。您无需自行指定“付款”扩展程序。

请查看此示例代码:

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

后续步骤