As a counter-proposal to #15 here is a proposal that would require no Payment Handlers (i.e. they could potentially be deprecated entirely) but would require that instruments are stored by RPs in the browser using an ID that is both "routable" by a merchant and opaque enough that it is not sensitive to share.
This is a minor modification to the existing Secure Payment Confirmation (SPC) proposal.
Advantages:
- No need for Payment Handlers = very simple to implement for RPs
- Lower implementation and maintenance burden for browsers
- Much simpler Payment Request API (no need to support "payment method data" and can possibly exclude shipping details etc)
- Simple fallback to rendering an RP provided URL inside a modal dialogue
Disadvantages:
- Need a strategy to prevent instrument identifiers becoming global user identifiers
- No PH = no support for delegated shipping details etc
Assumptions:
- The RP (bank/issuer/wallet) registers payment instruments in the browser at some time when the user has authenticated themselves (e.g. login to Internet banking/wallet, complete a previous transaction)
- The instrument identifier is "routable" (i.e. can be used by a merchant to route an authorization request to the RP to get a challenge or fallback URL). E.g. A format-preserving tokenized card or URL
- The instrument identifier is opaque (does not reveal private information about the user) as it will be revealed to the merchant before SPC when the instrument is selected by the user from a browser-rendered instrument selector
Questions:
- Can the instrument identifier be easily rotated so it is not a global identifier shared by the browser with multiple merchants?
- How does an RP register payment instruments that will not be used with Secure Payment Confirmation but where the URL for the fallback flow is not same-origin as the RP? (e.g. ACS server for capturing SMS OTP is at different origin to issuer who is the RP)
- How will an RP manage the instruments installed in the user's browser? Will they be able to enumerate them, delete them etc?
- Do we need a flow that is equivalent to JIT installation of a Payment Handler?
Enrollment
RPs (issuers, wallets etc) will register payment instruments in a users browser during an enrolment flow which involves calling the WebAuthn create
API to create a new PaymentCredential
.
A PaymentCredential
is a new type of credential which MAY be linked to a PublicKeyCredential
(which may be implicitly created when registering the PaymentCredential
). It contains payment instrument meta-data (i.e. a label and icon) and is mapped to one or more payment methods.
It will be possible to create a new PaymentCredential
linked to an existing PublicKeyCredential
. In this case the browser will invoke the authenticator's authentication flow using the details of the existing credential (as opposed to the registration flow which creates a new credential on the authenticator).
It will be possible to create a PaymentCredential
that is not linked to a PublicKeyCredential
. This instrument will appear in the browser-rendered instrument selector when appropriate but can't be used to invoke Secure Payment Confirmation. If a user selects one of these instruments then the merchant can only invoke the fallback authN flow by passing in an RP provided URL to render.
Whenever an RP registers an instrument there will be a user interaction whereby the user confirms the registration and their intent to use this for payments in the future. If the credential is linked to a PublicKeyCredential
this will also involve a user interaction with their authenticator as with existing WebAuthn registration and authentication flows.
The installed PaymentCredentials
serve multiple functions:
- They are used by the browser to present the user with an instrument selection list when the merchant calls PR API without providing a list of instrument identifiers. The list is filtered based on the payment methods provided by the merchant and those mapped to the credential.
- The label and icon are used on the Secure Payment Confirmation screen.
- The label and icon are rendered by the browser as part of the modal dialogue that renders the fallback URL when Secure Payment Confirmation is skipped/fails.
- The fallback flow is only allowed if the provided URL is same-origin with the credential RP.
[SecureContext, Exposed=Window]
interface PaymentCredential : Credential {
};
partial dictionary CredentialCreationOptions {
PaymentCredentialCreationOptions securePayment;
};
dictionary PaymentCredentialCreationOptions {
// |instrumentId| is a caller provided ID for the payment instrument
// to which the new PaymentCredential should be bound. It should be
// an opaque string generated using a payment network specific algorithm
// that allows the network to identify the issuer of the instrument
// and the issuer to identify the account associated with this
// instrument. (e.g. form-preserving card token or URL)
required DOMString instrumentId;
required DOMString displayName;
required USVString icon;
required sequence<USVString> paymentMethods;
PaymentCredentialPublicKeyOptions createPublicKeyCredentialOptions
PublicKeyCredentialDescriptor existingPublicKeyCredential;
};
// Either |createPublicKeyCredentialOptions| or |existingPublicKeyCredential|
// or both must be provided.
// If both are provided then the browser should create the credential if
// the existing credential doesn't exist.
dictionary PaymentCredentialPublicKeyCreationOptions {
required PublicKeyCredentialRpEntity rp;
required BufferSource challenge;
required sequence<PublicKeyCredentialParameters> pubKeyCredParams;
unsigned long timeout;
// PublicKeyCredentialCreationOption attributes that are intentionally omitted:
// user: For a PaymentCredential, |instrument| is analogous to |user|.
// excludeCredentials: No payment use case has been proposed for this field.
// attestation: Authenticator attestation is considered an anti-pattern
// for adoption so will not be supported.
// extensions: No payment use case has been proposed for this field.
};
Example usage:
const securePaymentConfirmationCredentialCreationOptions = {
instrumentId: "Q1J4AwSWD4Dx6q1DTo0MB21XDAV76",
displayName: 'Mastercardยทยทยทยท4444',
icon: 'icon.png',
paymentMethods: ["https://mastercard.com/pay", "https://clicktopay.com"]
existingCredential: {
type: "public-key",
id: Uint8Array.from(credentialId, c => c.charCodeAt(0))
},
publicKeyCreationOptions: {
challenge,
rp,
pubKeyCredParams,
timeout
}
};
// Bind new credential to |credentialId|, or create a new credential
// if |credentialId| doesn't exist.
const credential = await navigator.credentials.create({
securePayment: securePaymentCredentialCreationOptions
});
Payment
When a user visits a merchant there are two possible flows before Secure Payment Confirmation (or the fallback flow) is invoked.
User Provided Instrument
- The merchant gets the instrument selection from the user via a form or other mechanism (e.g. selection of a card on file)
- The merchant sources a set of valid instrument identifiers, an optional challenge, and an optional fallback URL from the payment system.
- The merchant creates a Payment Request and passes the instrument identifiers into the constructor.
- The merchant calls
canMakePayment
and the browser returns a "truthy" response if one of the provided instrument identifiers identifies an installed PaymentCredential
that supports one of the merchant provided payment methods. If there is an instrument linked to a PublicKeyCredential
then the response also indicates that Secure Payment Confirmation is available - i.e. the response has a property canDoSecurePaymentConfirmation
equal to true
.
- The merchant calls
show()
. If the merchant provided multiple instrument identifiers and more than one PaymentCredential
is identified by one of the supplied identifiers (this shouldn't happen unless RPs have messed up) then the browser presents the user with an instrument selection list populated with all installed PaymentCredentials
that are identified by provided instrument identifiers. However, the browser will prefer to use a PaymentCredential
that is linked to a PublicKeyCredential
so that it can offer Secure Payment Confirmation. Therefor if only one PaymentCredential
that is identified by the supplied instrument identifiers is linked to a PublicKeyCredential
then that PaymentCredential
is auto-selected. If more than one PaymentCredential
linked to a PublicKeyCredential
is identified by the provided instrument identifiers then only those are listed for selection by the user.
- When the user selects an instrument (or when the browser auto-selects an instrument) the instrument identifier is passed to the merchant through an
instrumentSelected
event emitted by the Payment Request object.
Instrument Selection Through The Browser
- The merchant creates a Payment Request and DOES NOT pass in instrument identifiers.
- The merchant calls
canMakePayment
and the browser returns a "truthy" response if at least one of the installed PaymentCredentials
supports one of the provided payment methods. If there is an instrument linked to a PublicKeyCredential
then the response also indicates that Secure Payment Confirmation is available - i.e. the response has a property canDoSecurePaymentConfirmation
equal to true
- The merchant calls
show
and the browser presents the user with an instrument selection list populated with all installed PaymentCredential
s that support the payment methods the merchant supports.
- When the user selects an instrument the instrument identifier is passed to the merchant through an
instrumentSelected
event emitted by the Payment Request object.
- The merchant uses the instrument identifier from the event to route a request to the RP to get an optional challenge and optional fallback URL for the payment (the RP may also indicate to the merchant that no further authN is required - i.e. zero-friction - in which case the merchant exits the flow)
AuthN
At this point the User Provided Instrument and Instrument Selection Through The Browser flows converge.
-
The merchant passes the challenge and a fallback URL to the browser via a respondWith
method on the instrumentSelected
event.
-
If the merchant provided a challenge and the selected instrument is mapped to a PublicKeyCredential
the browser invokes Secure Payment Confirmation. The assertion generated is returned to the merchant in the PaymentResponse
.
-
If SPC fails or the merchant doesn't provide a challenge AND the merchant does provide a fallback URL then the browser shows a modal dialogue which renders the fallback URL and returns an instance of a Window
to the merchant in the PaymentResponse
. The fallback URL is only used if the origin is the same as the origin of the RP that enrolled the PaymentCredential
. The merchant should use postMessage
to communicate with the RP's window and can authenticate the user through some fallback mechanism such as SMS OTP.
Here's how a new simpler PR API could look with 3DS:
// Get instrument identifiers, challenge and fallback URL from network
const { instruments, challenge, fallbackUrl } = get3DSResponse(pan)
const request = new PaymentRequest({
supportedMethods: ["https://mastercard.com/pay", "http://clicktopay.com/"],
instruments // instruments = ["412938871562965", "41290981273767562"]
},{
currency: 'USD', value: '20.00'
})
const cmp = await request.canMakePayment()
if(cmp) {
request.addEventListener("instrumentSelected", e => {
e.respondWith({
challenge,
fallbackUrl
})
}
const {securePaymentConfirmation, fallbackWindow} = await request.show();
if(securePaymentConfirmation) {
do3DSStuff(securePaymentConfirmation)
return
}
if(fallbackWindow) {
fallbackWindow.addEventListener("message", e => {
//Handle messages from issuer window
})
}
} else {
//Legacy 3DS flow
}
Here's how a new simpler PR API could look WITH instrument selection:
const request = new PaymentRequest({
supportedMethods: ["https://mastercard.com/pay", "http://clicktopay.com/", "https://google.com/pay"],
},{
currency: 'USD', value: '20.00'
})
const cmp = await request.canMakePayment()
if(cmp) {
request.addEventListener("instrumentSelected", e => {
if(e.supportedMethods.includes("https://google.com/pay")) {
// Skip SPC and show Google Pay modal
e.respondWith({
fallbackUrl: "https://google.com/pay?instrument=" + e.instrumentId
})
return
}
const { challenge, fallbackUrl } = get3DSResponse(e.instrument)
e.respondWith({
challenge,
fallbackUrl
})
}
const {securePaymentConfirmation, fallbackWindow} = await request.show();
if(securePaymentConfirmation) {
do3DSStuff(securePaymentConfirmation)
return
}
if(fallbackWindow) {
fallbackWindow.addEventListener("message", e => {
if(e.origin == "https://google.com/")) {
//Message from Google Pay window
} else {
//Message from 3DS fallback window
}
})
}
} else {
//Legacy checkout flow
}