Secure Payment Confirmation

Editor’s Draft,

This version:
https://w3c.github.io/secure-payment-confirmation
Latest published version:
https://www.w3.org/TR/secure-payment-confirmation/
Issue Tracking:
GitHub
Editors:
(Google)
(Google)

Abstract

This specification describes data structures, formats, algorithms, and processing models to facilitate [webauthn-3] based payments on the Web.

Status of this document

1. Introduction

This section and its sub-sections are non-normative.

This specification defines an API that enables the use of strong authentication methods in payment flows on the web. It aims to provide the same authentication benefits and user privacy focus as [webauthn-3], whilst relaxing certain constraints to meet the needs of payment processing.

Similarly to [webauthn-3], this specification defines two related processes involving a user. The first is § 3 Enrollment, where a relationship is created between the user and the Relying Party. The second is § 4 Authentication, where a specific payment from the user to a recipient (possibly via an intermediary payment service provider) is authenticated for the Relying Party. An important concept in Secure Payment Confirmation is that with the permission of the Relying Party, the merchant or another entity may initiate an authentication ceremony on the Relying Party’s behalf.

Functionally, this specification defines a new payment method for the PaymentRequest API, and adds a WebAuthn Extension to extend [webauthn-3] with payment-specific datastructures and to relax assumptions to allow the API to be called in payment contexts.

1.1. Use Cases

The below use case scenarios illustrate the need for a payment-specific extension to [webauthn-3], rather than just asking web developers to build their own flows on-top of [webauthn-3] registration and authentication. We presume that the general use case of cryptographic-based authentication for online transactions is well established.

Note: These sections are still a WIP.

1.1.1. Verifying an authenticated payment

A user is performing an online transaction based on cryptographic authentication (i.e. either WebAuthn or this specification). The user completes the authentication and the issuing bank now wishes to verify the signed cryptogram that they received.

If the bank or merchant site is using WebAuthn, the payment-specific information must be placed in the WebAuthn challenge. This has several issues:

  1. It is a misuse of the challenge field (which is intended to defeat replay attacks).

  2. There is no specification for this, so each issuing bank may come up with their own format for how payment information should be formatted and encoded in the challenge - which causes fragmentation and confusion.

  3. Regulations may require that the user was shown the transaction amount. The challenge field cannot be used to verify that, as information included there has no behavior impact on WebAuthn.

Secure Payment Confirmation, on the other hand:

  1. Uses the challenge field only to defeat replay attacks, as with normal WebAuthn.

  2. Provides a specified format for transaction data, which will e.g. allow generic verification code and test suites to be developed and shared as appropriate.

  3. Guarantees that the user agent has presented the transaction information to the user, in a way that a malicious website (or maliciously introduced JavaScript code on a trusted website) cannot bypass.

    • The transaction information is included in the CollectedClientData dictionary, which is not influencable by JavaScript code.

    NOTE: This does assume the issuing bank trusts the user agent, but that is already required in payment flows today.

1.1.2. Registration in a bank iframe

1.1.3. Authentication on a merchant website

1.2. Sample API Usage Scenarios

In this section, we walk through some scenarios for Secure Payment Confirmation and the corresponding sample code for using this API. Note that these are example flows and do not limit the scope of how the API can be used.

1.2.1. Enrollment

This is the first-time flow, in which a new credential is created and stored by the issuing bank.

  1. The user visits merchant.com, selects an item to purchase, and proceeds to the checkout flow. They enter their payment instrument details, and indicate that they wish to pay (e.g. by pressing a "Pay" button).

  2. The merchant communicates with the issuing bank of the payment instrument over a backchannel. The issuing bank requests verification of the user, and provides a bank-owned URL for the merchant to open in an iframe.

  3. The merchant opens an iframe to bank.com, with the allow attribute set to "payment".

  4. In the iframe, the issuing bank confirms the user’s identity via a traditional means (e.g. SMS OTP). After confirming, they offer the user the ability to enroll in SPC authentication for future payments.

  5. The user consents (e.g. by clicking an "Enroll" button in the bank UX), and the bank runs the below example code in the iframe.

  6. The user goes through a WebAuthn registration flow. A new credential is created for the user and the public key is returned to the issuing bank who stores it in their server-side database associated with the user.

  7. The verification completes; the bank iframe closes and the merchant finishes the checkout process for the user.

The sample code for enrolling the user follows:

if (!window.PublicKeyCredential) { /* Client not capable. Handle error. */ }

const publicKey = {
  // The challenge should be created by the bank server and sent to the iframe.
  challenge: new Uint8Array([21,31,105 /* 29 more random bytes generated by the server */]),

  // Relying Party:
  rp: {
    name: "Fancy Bank",
  },

  // User:
  user: {
    // An id that the bank server can use to identify this user in future interactions.
    id: Uint8Array.from(window.atob("MIIBkzCCATigAwIBAjCCAZMwggE4oAMCAQIwggGTMII="), c=>c.charCodeAt(0)),
    name: "jane.doe@example.com",
    displayName: "Jane Doe",
  },

  // This Relying Party will accept either an ES256 or RS256 credential, but
  // prefers an ES256 credential.
  pubKeyCredParams: [
    {
      type: "public-key",
      alg: -7 // "ES256"
    },
    {
      type: "public-key",
      alg: -257 // "RS256"
    }
  ],

  // This Relying Party requires user verification.
  authenticatorSelection: {
    userVerification: "required"
  },

  timeout: 360000,  // 6 minutes

  // Indicate that this is an SPC credential. This is currently required to
  // allow credential creation in an iframe, and so that the browser knows this
  // credential relates to SPC.
  //
  // It is expected that a future version of the spec may remove the need for
  // this extension.
  extensions: {
    "payment": {
      isPayment: true,
    }
  }
};

// Note: The following call will cause the authenticator to display UI.
navigator.credentials.create({ publicKey })
  .then(function (newCredentialInfo) {
    // Send new credential info to server for verification and registration.
  }).catch(function (err) {
    // No acceptable authenticator or user refused consent. Handle appropriately.
  });

1.2.2. Authentication

This is the flow when a user with an already registered credential is performing a transaction and the issuing bank and merchant wish to use Secure Payment Confirmation.

  1. The user visits merchant.com, selects an item to purchase, and proceeds to the checkout flow. They enter their payment instrument details, and indicate that they wish to pay (e.g. by pressing a "Pay" button).

    Note: This may or may not be the merchant website the user was originally on when they registered this device; it is irrelevant here.

  2. The merchant communicates with the issuing bank of the payment instrument over a backchannel. The issuing bank requests verification of the user, but notes that it accepts SPC and provides a list of known credential IDs for this payment instrument.

  3. The merchant runs the below example code.

  4. The user accepts the transaction in the SPC UX, and performs a subsequent WebAuthn authentication ceremony. The signed cryptogram is returned to the merchant.

  5. The merchant sends the signed cryptogram to the issuing bank via the backchannel. The issuing bank verifies the cryptogram, and knows that the user is both valid and has consented to the transaction. The issuing bank authorizes the transaction and the merchant finishes the checkout process for the user.

The sample code for authenticating the user follows. Note that the example code presumes access to await/async, for easier to read promise handling.

if (!window.PaymentRequest) { /* PaymentRequest not available; merchant should fallback to traditional flows */ }

const request = new PaymentRequest([{
  supportedMethods: "secure-payment-confirmation",
  data: {
    // List of credential IDs obtained from the bank.
    credentialIds,

    // The challenge is also obtained from the bank.
    challenge: new Uint8Array([21,31,105 /* 29 more random bytes generated by the bank */]),

    instrument: {
      displayName: "Fancy Card ****1234",
      icon: "https://fancybank.com/card-art.png",
    }, 

    timeout: 360000,  // 6 minutes
  }], {
    total: {
      label: "Total",
      amount: {
        currency: "USD",
        value: "5.00",
      },
    },
  });

try {
  const canMakePayment = await request.canMakePayment();
  if (!canMakePayment) { throw new Error('Cannot make payment'); }

  const response = await request.show();
  await response.complete('success');

  // response.data is a PublicKeyCredential, with a clientDataJSON that
  // contains the transaction data for verification by the issuing bank.

  /* send response.data to the issuing bank for verification */
} catch (err) {
  /* SPC cannot be used; merchant should fallback to traditional flows */
}

2. Dependencies

This specification relies on several other underlying specifications, listed below and in Terms defined by reference.

WebAuthn Conditional UI

Secure Payment Confirmation only shows the transaction UX if one of the passed credentials is valid for the current device, without requiring a user interaction. This concept is not currently part of [webauthn-3], but is on the roadmap for a future enhancement.

Until this is available, user agents can consider either storing a local cache of WebAuthn credentials created for this device, or always showing the transaction UX even if the user may be unable to complete the authentication.

TODO: Bikeshed the name.

3. Enrollment

To enroll a user for Secure Payment Confirmation, relying parties should call navigator.credentials.create(), with the payment WebAuthn Extension specified.

Note: We currently require an extension in order to allow credential creation in an iframe and to allow the browser to cache SPC credentials in the absence of WebAuthn Conditional UI. Future versions of this specification may remove the requirement for the extension.

4. Authentication

To authenticate a payment via Secure Payment Confirmation, this specification defines a new payment method, "secure-payment-confirmation". This payment method confirms the transaction with the user and then performs an authentication ceremony to authenticate the user and create a signed blob representing the transaction.

At a high level, authentication for Secure Payment Confirmation is similar to [webauthn-3], with one major conceptual shift. Secure Payment Confirmation allows a third-party (e.g. the merchant) to trigger an authentication ceremony on behalf of the Relying Party, passing in credentials that it has obtained from the Relying Party on some other unspecified channel. See § 1.1.3 Authentication on a merchant website.

4.1. Payment Method: Secure Payment Confirmation

TODO: This specification also needs to monkey-patch step 12 of PaymentRequest’s constructor somehow, to enforce that when "secure-payment-confirmation" is used, exactly one method is given.

4.1.1. Payment Method Identifier

The standardized payment method identifier for this specification is "secure-payment-confirmation".

4.1.2. SecurePaymentConfirmationRequest Dictionary

dictionary SecurePaymentConfirmationRequest {
    required BufferSource challenge;
    required FrozenArray<BufferSource> credentialIds;
    required PaymentCredentialInstrument instrument;
    unsigned long timeout;
    AuthenticationExtensionsClientInputs extensions;
};

The SecurePaymentConfirmationRequest dictionary contains the following members:

challenge member, of type BufferSource

A random one-time challenge that the relying party generates on the server side to prevent replay attacks.

credentialIds member, of type FrozenArray<BufferSource>

The list of credential identifiers for the given instrument.

instrument member, of type PaymentCredentialInstrument

The description of the instrument name and icon to display during enrollment and to be signed along with the transaction details.

timeout member, of type unsigned long

The number of milliseconds before the request to sign the transaction details times out. At most 1 hour.

extensions member, of type AuthenticationExtensionsClientInputs

Any WebAuthn extensions that should be used for the passed credential(s). The caller does not need to specify the payment extension; it is added automatically.

4.1.3. Steps to check if a payment can be made

The steps to check if a payment can be made for this payment method, for an input SecurePaymentConfirmationRequest request, are:

  1. If request.credentialIds is empty, return false.

  2. If request.instrument.displayName is empty, return false.

  3. Download the image specified in request.instrument.icon. If this fails, return false.

    Note: Performing this step here mitigates a privacy leak. TODO: Document + link-to privacy section.

  4. Optionally, the user agent may elect to return false.

    Note: This covers the current Chrome behavior of checking whether the passed credentials match those on the system, and early-exit if so. This is a potential privacy concern, and may be removed.

  5. Return true.

4.1.4. Displaying a transaction confirmation UX

TODO: We need to determine how best this can be specified. We tend to avoid requiring user agents to show specific UX, but in the case of SPC we do want to ensure that the appropriate transaction details are communicated to the user (either via browser UX or via the authenticator device itself, if it has an output).

4.1.5. Steps to respond to a payment request

The steps to respond to a payment request for this payment method, for an input SecurePaymentConfirmationRequest request, are:

  1. Create a AuthenticationExtensionsPaymentInputs dictionary, payment, with:

    1. isPayment set to true.

    2. rpOrigin set to TODO.

      TODO: We don’t have the rp origin here; maybe this should just go in the processing steps.
    3. topOrigin set to the origin of the top-level frame.

      TODO: This is part of the PaymentRequestEvent, but it’s unclear how to access that in an inbuilt PaymentHandler.
    4. total set to the total from the PaymentRequest.

      TODO: This is part of the PaymentRequestEvent, but it’s unclear how to access that in an inbuilt PaymentHandler.
    5. instrument set to request.instrument.

  2. Create a PublicKeyCredentialRequestOptions, publicKeyOpts, with:

    1. challenge set to request.challenge.

    2. timeout set to request.timeout.

    3. userVerification set to required.

    4. extensions set to a AuthenticationExtensionsClientInputs dictionary whose payment member is set to payment, and whose other members are set from request.extensions.

  3. For each id in request.credentialIds:

    1. Create a PublicKeyCredentialDescriptor, descriptor, with:

      1. type set to public-key

      2. id set to id

      3. transports set to a sequence of length 1 whose only member is internal.

    2. Push descriptor onto publicKeyOpts.allowCredentials.

  4. Let outputCredential be the result of calling navigator.credentials.get({publicKey}).

    Note: This triggers [webauthn-3]'s Get behavior

  5. Return outputCredential.

5. WebAuthn Extension - "payment"

This client registration extension and authentication extension indicates that a credential is either being created for or used for Secure Payment Confirmation, respectively.

For registration, this extension relaxes the WebAuthn requirements to allow credential creation in a cross-origin iframe, and also allows the browser to identify and cache Secure Payment Confirmation credentials. For authentication, this extension allows a third-party to perform an authentication ceremony on behalf of the Relying Party, and also adds transaction information to the signed cryptogram.

Notably, a website should not call navigator.credentials.get() with this extension directly; for authentication the extension can only be accessed via PaymentRequest with a "secure-payment-confirmation" payment method.

Extension identifier

payment

Operation applicability

Registration and authentication

Client extension input
partial dictionary AuthenticationExtensionsClientInputs {
  AuthenticationExtensionsPaymentInputs payment;
};

dictionary AuthenticationExtensionsPaymentInputs {
  boolean isPayment;

  // Only used for authentication.
  USVString rpOrigin;
  USVString topOrigin;
  PaymentCurrencyAmount total;
  PaymentCredentialInstrument instrument;
};
isPayment member, of type boolean

Indicates that the extension is active.

TODO: Find a better way to do this. Needed currently because other members are auth-time only.
rpOrigin member, of type USVString

The Relying Party origin of the credential(s) being used. Only valid at authentication time.

topOrigin member, of type USVString

The origin of the top-level frame. Only valid at authentication time.

total member, of type PaymentCurrencyAmount

The total amount the user is paying to the payee. Only valid at authentication time.

instrument member, of type PaymentCredentialInstrument

The instrument details to be displayed to the user. Only valid at authentication time.

Client extension processing (registration)

Note: Reading [webauthn-3] literally, these steps don’t work; extensions are injected at step 12 of [[Create]] and cannot really modify anything. However other extensions ignore that entirely and assume they can modify any part of any WebAuthn algorithm!

When creating a new credential:

  1. Remove the check for sameOriginWithAncestors in step 2.

    Note: This allows for creating SPC credentials in a cross-origin iframe, as long as the correct permission policy is set (see § 7 Permissions Policy integration). We could additionally require and consume a transient activation here, if we felt the permission policy is not sufficient.

  2. In step 13, set the type to "payment.create".

    TODO: Can we and/or should we rely on getClientExtensionResults() instead?
Client extension processing (authentication)

When making an assertion:

  1. If not in a "secure-payment-confirmation" payment handler, return a "NotAllowedError" DOMException.

    Note: This guards against websites trying to access the extended powers of SPC without going through the browser UX.

  2. During [[DiscoverFromExternalSource]](origin, options, sameOriginWithAncestors):

    1. Skip step 6.1, which compares options.rpId to effectiveDomain

      Note: This enables cross-domain authentication ceremonies; see § 1.1.3 Authentication on a merchant website.

    2. In step 9, instead of creating a CollectedClientData, instead create a CollectedClientPaymentData with:

      1. type set to "payment.get"

      2. payment set to a new CollectedClientAdditionalPaymentData with:

        1. rpOrigin set to rpOrigin.

        2. topOrigin set to topOrigin.

        3. total set to total.

        4. instrument set instrument.

      3. All other members set as per the original step 9.

Client extension output

None

Authenticator extension processing

None

5.1. CollectedClientPaymentData Dictionary

dictionary CollectedClientPaymentData : CollectedClientData {
    required CollectedClientAdditionalPaymentData payment;
};

The CollectedClientPaymentData dictionary inherits from CollectedClientData. It contains the following additional field:

payment member, of type CollectedClientAdditionalPaymentData

The additional payment information to sign.

5.2. CollectedClientAdditionalPaymentData Dictionary

dictionary CollectedClientAdditionalPaymentData {
    required USVString rp;
    required USVString topOrigin;
    required PaymentCurrencyAmount total;
    required PaymentCredentialInstrument instrument;
};

The CollectedClientAdditionalPaymentData dictionary contains the following fields:

rp member, of type USVString

The relying party that created the credential.

topOrigin member, of type USVString

The origin of the top level context that requested to sign the transaction details. Typically this would be called a merchant.

total member, of type PaymentCurrencyAmount

The PaymentCurrencyAmount of the [payment-request] total field.

instrument member, of type PaymentCredentialInstrument

The instrument information that was displayed to the user.

Note that there is no paymentRequestOrigin field in CollectedClientAdditionalPaymentData, because the origin of the calling frame is already included in CollectedClientData of [webauthn-3].

6. Common Data Structures

The following data structures are shared between enrollment and authentication.

6.1. PaymentCredentialInstrument Dictionary

dictionary PaymentCredentialInstrument {
    required DOMString displayName;
    required USVString icon;
};

The PaymentCredentialInstrument dictionary contains the information to be displayed to the user and signed together with the transaction details. It contains the following members:

displayName member, of type DOMString

The name of the payment instrument to be displayed to the user.

icon member, of type USVString

The URL of the icon of the payment instrument.

7. Permissions Policy integration

This specification uses the "payment" policy-identifier string from [payment-request] to control access to both enrollment and authentication. This extends the WebAuthn Permission Policy.

Note: Algorithms specified in [CREDENTIAL-MANAGEMENT-1] perform the actual permissions policy evaluation. This is because such policy evaluation needs to occur when there is access to the current settings object. The [[Create]](origin, options, sameOriginWithAncestors) and [[DiscoverFromExternalSource]](origin, options, sameOriginWithAncestors) internal methods do not have such access since they are invoked in parallel (by algorithms specified in [CREDENTIAL-MANAGEMENT-1]).

8. Security Considerations

Note: This section is still very much a draft.

Main considerations (on top of WebAuthn itself):

9. Privacy Considerations

Note: This section is still very much a draft.

SPC primarily points to WebAuthn for its privacy model, but it does relax that model in significant ways that should be considered.

10. Accessibility Considerations

User Agents implementing this specification should follow both WebAuthn’s Accessibility Considerations and PaymentRequest’s Accessibility Considerations.

Index

Terms defined by this specification

Terms defined by reference

References

Normative References

[CREDENTIAL-MANAGEMENT-1]
Mike West. Credential Management Level 1. 17 January 2019. WD. URL: https://www.w3.org/TR/credential-management-1/
[PAYMENT-METHOD-ID]
Marcos Caceres; et al. Payment Method Identifiers. 5 September 2019. CR. URL: https://www.w3.org/TR/payment-method-id/
[PAYMENT-REQUEST]
Marcos Caceres; et al. Payment Request API. 29 June 2021. CR. URL: https://www.w3.org/TR/payment-request/
[WEBAUTHN-1]
Dirk Balfanz; et al. Web Authentication:An API for accessing Public Key Credentials Level 1. 4 March 2019. REC. URL: https://www.w3.org/TR/webauthn-1/
[WebIDL]
Boris Zbarsky. Web IDL. 15 December 2016. ED. URL: https://heycam.github.io/webidl/

Informative References

[ECMASCRIPT]
ECMAScript Language Specification. URL: https://tc39.es/ecma262/multipage/
[HTML]
Anne van Kesteren; et al. HTML Standard. Living Standard. URL: https://html.spec.whatwg.org/multipage/
[WEBAUTHN-3]
Jeff Hodges; et al. Web Authentication: An API for accessing Public Key Credentials - Level 3. 27 April 2021. WD. URL: https://www.w3.org/TR/webauthn-3/

IDL Index

dictionary SecurePaymentConfirmationRequest {
    required BufferSource challenge;
    required FrozenArray<BufferSource> credentialIds;
    required PaymentCredentialInstrument instrument;
    unsigned long timeout;
    AuthenticationExtensionsClientInputs extensions;
};

partial dictionary AuthenticationExtensionsClientInputs {
  AuthenticationExtensionsPaymentInputs payment;
};

dictionary AuthenticationExtensionsPaymentInputs {
  boolean isPayment;

  // Only used for authentication.
  USVString rpOrigin;
  USVString topOrigin;
  PaymentCurrencyAmount total;
  PaymentCredentialInstrument instrument;
};

dictionary CollectedClientPaymentData : CollectedClientData {
    required CollectedClientAdditionalPaymentData payment;
};

dictionary CollectedClientAdditionalPaymentData {
    required USVString rp;
    required USVString topOrigin;
    required PaymentCurrencyAmount total;
    required PaymentCredentialInstrument instrument;
};

dictionary PaymentCredentialInstrument {
    required DOMString displayName;
    required USVString icon;
};