w3c / payment-handler

Payment Handler API
https://w3c.github.io/payment-handler/
Other
74 stars 42 forks source link

Revisiting payment app filtering #96

Closed adamroach closed 7 years ago

adamroach commented 7 years ago

The current spec includes a function that a payment app can optionally register for itself (canHandle() in the existing setManifest() method) for the purpose of determining whether the payment app is applicable to the payment method and its filters. This is one of several mechanisms that have been discussed to date.

@marcoscaceres correctly points out that, as specified, the current mechanism is somewhat problematic, in that we're assuming basically a "pickling" of this function for use in future contexts in a way that's not obviously supported by ES.

In practice, when I proposed this approach (and, to be clear, it was my second-favorite choice out of options on the table), what I had in mind was something very similar to how PAC files operate, both from a persistence and execution environment perspective. While there are probably ways to specify canHandle() in a way that is more palatable (e.g., by having it point to an external file that is evaluated independently), there is some question as to whether doing so is worth the effort it is likely to take.

The alternate approach is to more concretely define a payment filtering algorithm based on a generic understanding of the filter parameter passed as part of the PaymentRequest.

I'm opening this issue to re-kindle the discussion regarding these two potential approaches, in light of the newly highlighted difficulties we are likely to encounter. Of course, alternatives approaches beyond the two I describe are welcome as well.

jakearchibald commented 7 years ago

If this cannot be determined from data the browser already has, and needs to execute code in the origin, a service worker event is the best fit.

tommythorsen commented 7 years ago

I think that changing this into an event might not be such a good idea privacy-wise. In a regular event handler, there's nothing stopping the service worker from logging all of the payment request information and passing it back to the back-end. That would let an information-greedy payment provider see all of the user's potential payments -- even the ones that don't ultimately end up in that particular payment app.

The intention of the currently specified mechanism is that the registered canHandle() function should be a "pickled", "pure" function ("pure" meaning that the function does not rely on anything other than the input parameters, and does not have any side-effects).

I'm not actually a great fan of the current canHandle() mechanism, but since I don't have a better suggestion, I think it's acceptable, as long as we can show that it's implementable. If not, then I would like to propose that we drop the whole canHandle() mechanism.

Since this has turned out to be very difficult to specify in a way that fulfills everyone's needs, maybe it would be better if we postponed this mechanism to version 2 of the specification? That would require that we revert some changes to the basic-card specification, and move back to having individual method IDs for the various types of cards, with all the problems that entails, but maybe those problems are easier to tackle.

marcoscaceres commented 7 years ago

But the only info the SW gets is "someone wants to know if you can handle this" - not who or what or where they are trying to buy. So the info is not useful beyond "lots of sites wonder if this particular payment method is supported by me".

marcoscaceres commented 7 years ago

In fact, there may be no intent to buy anything - because a site intimates a canMakeActivePayment() potential long before any actual payment happens (if it happens at all!). The browser knows the registered methods of the app, so it could strip out the ones that don't match.

jakearchibald commented 7 years ago

@tommythorsen can you give an example of a question that the browser cannot answer with data it has, but can be answered by executing code?

ianbjacobs commented 7 years ago

@marcoscaceres wrote "The browser knows the registered methods of the app, so it could strip out the ones that don't match."

We asked ourselves "who should do the filtering, the browser or the payment app?" There are pros and cons to each approach. If browsers do it, payment apps don't have to, which is a good thing.

However, we expect to see a number of payment methods in use, each with its own filtering mechanism, and if we rely on the browser, then we are not likely to see those implemented. Therefore, we have favored asking payment apps to implement the filtering.

Ian

adrianhopebailie commented 7 years ago

The browser knows the registered methods of the app, so it could strip out the ones that don't match.

@marcoscaceres This filtering of the Payment Request to a new object that represents a subset of the data is exactly what the current spec does and that you have insisted is a bad idea and should be taken to the TAG for review. I'm confused now as to what you're actually proposing?

ianbjacobs commented 7 years ago

@adrianhopebailie,

I think there is conflation of two topics:

1) Whether some (as we currently anticipate) or all of the payment request data should be fed to a payment app. 2) Who should be evaluating whether a merchant filter for a given payment method matches what a given payment app's filter? Should it be the payment app (as we currently anticipate) or the browser?

Ian

adrianhopebailie commented 7 years ago

@ianbjacobs I disagree

@marcoscaceres said, in the context of whether the data passed to the SW is private or not:

The browser knows the registered methods of the app, so it could strip out the ones that don't match.

This implies he is talking about the data passed to the app which he is also advocating should be the exact same request object passed to the browser

adamroach commented 7 years ago

@jakearchibald

If this cannot be determined from data the browser already has, and needs to execute code in the origin, a service worker event is the best fit.

To be clear, this code must not execute in the origin. It needs to execute in its own unique origin.

jakearchibald commented 7 years ago

@adamroach can you give an example of a question that the browser cannot answer with data it has, but can be answered by executing code?

marcoscaceres commented 7 years ago

As I'm sucking at using natural language, I'm proposing this:

bank.com / index.html

const { paymentManager: pm } = navigator.serviceWorker;
pm.methods.set("visa-4756", {
  name: "Visa ending ****4756",
  methods: ["basic-card"],
  icons: [visaIcons],
});

merchant.com / checkout.html

const methodData = [{
    supportedMethods: ["basic-card"],
    data: {
      supportedNetworks: ['aFamousBrand', 'aDebitNetwork'],
      supportedTypes: ['debit']
    }
  }, {
    supportedMethods: ["bobpay.com"],
    data: {
      merchantIdentifier: "XXXX",
      bobPaySpecificField: true
    }
  }];
const request = new PaymentRequest(method, details, options);
const canDoIt = await request.canMakePayment();

bank.coms/service-worker.js

// We only get ["basic card"] methods, not bobpay.com!
addEventListener("canmakepayment",  ev => {
  ev.canMakePayment(new Promise(resolve => {
    let canDoIt = false;
    for(const {supportedNetworks, supportedTypes} of ev.methods){
      if(supportedNetworks.includes("aFamousBrand") && supportedTypes.includes("debit")){
        canDoIt = true;
      }
    }
    // Could do other checks async...
    resolve(canDoIt);
  });
});
marcoscaceres commented 7 years ago

(the above should actually destructure "method.data", but you get the idea.... the point is that bank.com can do that checks it needs need against the matching payment methods sent from merchant.com - without spying on merchant.com's bobpay's stuff)

ianbjacobs commented 7 years ago

Hi @marcoscaceres,

Thanks for the code!

First, bank.com is doing the evaluation of the merchant filter. That is consistent with what we've sought.

Second, it seems that the browser is giving bank.com's service worker only the data associated with the payment method(s) supported by bank.com. That's also consistent with what we've sought. However, it suggests that a subset of methodData is given to the payment app, and I had the impression from another discussion you did not want to do that. Perhaps I have misunderstood one suggestion or the other. But our current preference is for the browser to give the service worker only that information relevant for the payment methods it supports.

When we first started talking about service worker handling of filters, people observed the following:

Thus, your proposal reveals information about the transaction to all payment apps with matching payment methods, before any one of those payment apps has been selected by the user.

People expressed privacy concerns with this approach. That's why we pursued a design where the payment app registers a function that the browser can evaluate outside of the payment app's knowledge.

I hope that sheds light on how we ended up with what we have.

I look forward to more discussion to work through this,

Ian

adrianhopebailie commented 7 years ago

I like the approach of using an event (I think I have heard consensus that the group i happy with this change).

What we need to maintain though is the requirement that the event handler is executed in a "sandbox" of some sort that prevents the data passed in being stored or sent over the network.

As @ianbjacobs points out, this is partly based on privacy concerns, but also because this needs to return quickly as it likely impacting UI (the set of apps being presented to the user)

tommythorsen commented 7 years ago

can you give an example of a question that the browser cannot answer with data it has, but can be answered by executing code?

The browser is not able to answer questions regarding the contents of the data fields in PaymentMethodData and PaymentDetailsModifier dictionaries. The structure of the contents of these fields are proprietary, and only known by the payee (the merchant) and the payment provider (the one that owns the payment app).

An example of what the contents of the data field might be, can be found in the Basic Card Payment specification. Here, information about the supported networks ("visa", "amex", "mastercard", etc) and the type of card ("credit", "debit", etc) is supplied in the data field.

Is the canHandle() mechanism an absolute and fundamental requirement that we can't do without? As I've said above, I'm not a great fan of this, and I feel that it shouldn't be necessary for the merchant to ask this question, as long as he can supply a fallback option in the form of a recommended payment app to ensure that the user isn't presented with a dead end in the payment request UI.

marcoscaceres commented 7 years ago

On 25 Jan 2017, at 6:46 pm, Adrian Hope-Bailie notifications@github.com wrote:

I like the approach of using an event (I think I have heard consensus that the group i happy with this change).

What we need to maintain though is the requirement that the event handler is executed in a "sandbox" of some sort that prevents the data passed in being stored or sent over the network.

We can't do that, unfortunately. That would break service workers. It would also be super easy to work around (just put everything in IDB or make a text file with the Cache API, and then send it later).

So, if there is data that the event should not expose, we should talk about that: the event is the sandbox.

My proposal only exposes filtered payment methods and whatever is on the DOM Event interface, from which it inherits.

As @ianbjacobs points out, this is partly based on privacy concerns, but also because this needs to return quickly as it likely impacting UI (the set of apps being presented to the user)

That's the ideal, but not what can happen in reality. For example, the service worker may need to connect to a watch or other device over Bluetooth (actually bought something today like this with my Apple Watch) or might need to verify that the payment method has balance over the network or whatever - it's up to the application, we shouldn't restrict how it checks. If it is taking too long (or the SW shuts down), then the browser can intervene appropriately. — You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub, or mute the thread.

tommythorsen commented 7 years ago

@adrianhopebailie

What we need to maintain though is the requirement that the event handler is executed in a "sandbox" of some sort that prevents the data passed in being stored or sent over the network.

While this would conceptually take care of the privacy concerns, I feel that this might be too hard to implement in a secure way. Are there any examples of similar things in existence today, or are we treading new ground here? If it's the latter, then I am apprehensive about this.

@marcoscaceres

But the only info the SW gets is "someone wants to know if you can handle this" - not who or what or where they are trying to buy. So the info is not useful beyond "lots of sites wonder if this particular payment method is supported by me".

The SW is installed into a browser by a web site that might have the user logged in. It also regularly opens client windows which presumably will prompt for user login. I think that the SW does know "who".

As for "what" and "where", these depend on the contents of the opaque data fields in the payment request. Although this is out of our control, I think it is likely that most of these will contain something that lets the payment provider identify the merchant. My gut feeling tells me that the SW can know "where". If the merchant is narrow enough in scope, the payment provider can probably also get some ideas about "what", just by knowing "where".

@marcoscaceres

In fact, there may be no intent to buy anything - because a site intimates a canMakeActivePayment() potential long before any actual payment happens (if it happens at all!). The browser knows the registered methods of the app, so it could strip out the ones that don't match.

It is likely that canMakeActivePayment() will be called while loading the web page, in order to construct a "Buy" button. If my assumptions above, about the payment provider's ability to infer "who" and "where" are correct, then this does not make things any better at all. In fact, this might make a payment provider able to track every visit a user makes to any page that uses payment requests.

marcoscaceres commented 7 years ago

On 25 Jan 2017, at 8:01 pm, Tommy Thorsen notifications@github.com wrote:

can you give an example of a question that the browser cannot answer with data it has, but can be answered by executing code?

The browser is not able to answer questions regarding the contents of the data fields in PaymentMethodData and PaymentDetailsModifier dictionaries. The structure of the contents of these fields are proprietary, and only known by the payee (the merchant) and the payment provider (the one that owns the payment app).

An example of what the contents of the data field might be, can be found in the Basic Card Payment specification. Here, information about the supported networks ("visa", "amex", "mastercard", etc) and the type of card ("credit", "debit", etc) is supplied in the data field.

Is the canHandle() mechanism an absolute and fundamental requirement that we can't do without?

I believe it is. The Service Worker or object that the Service Worker gets representing the PaymentRequest must be able to handle all the method calls: .canMakePayment(), .abort(), .updateWith(), etc.

As I've said above, I'm not a great fan of this, and I feel that it shouldn't be necessary for the merchant to ask this question, as long as he

Or she:) can supply a fallback option in the form of a recommended payment app to ensure that the user isn't presented with a dead end in the payment request UI.

I don't think we should do the recommended payment app thing - the merchant can recommend whatever they like (or fall back to basic card). — You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub, or mute the thread.

tommythorsen commented 7 years ago

Is the canHandle() mechanism an absolute and fundamental requirement that we can't do without?

I believe it is. The Service Worker or object that the Service Worker gets representing the PaymentRequest must be able to handle all the method calls: .canMakePayment(), .abort(), .updateWith(), etc.

Well, we could remove .canMakePayment() too...

I don't think we should do the recommended payment app thing - the merchant can recommend whatever they like (or fall back to basic card).

I'm not sure I follow your argumentation, but falling back to basic card is certainly also an option. I think that for the sake of kickstarting the payment app ecosystem, though, recommended payment apps has the potential to work very well.

jakearchibald commented 7 years ago

@tommythorsen

The browser is not able to answer questions regarding the contents of the data fields in PaymentMethodData and PaymentDetailsModifier dictionaries. The structure of the contents of these fields are proprietary, and only known by the payee (the merchant) and the payment provider (the one that owns the payment app).

I think I get it, but could you provide a quick example (with code)?

I understand the thing about the proprietary structures, but what isn't clear to me is how the other origin would answer that without any access to origin data or communication.

It kinda feels like the recommended apps issue - too much time is being spent discussing the mechanism rather than the bigger picture.

Are there any examples of similar [sandboxing] in existence today, or are we treading new ground here? If it's the latter, then I am apprehensive about this.

I think it's new ground. There are worklets from the houdini project, which may be similar. However, are we sure this solves the problem, if the code has no access to network or storage?

tommythorsen commented 7 years ago

@jakearchibald

I think I get it, but could you provide a quick example (with code)?

Ok, I'll give it a shot. But instead of using the boring basic card payment method, I'm going to use a hypothetical visa checkout payment app. I've taken some of the property names and values from the visa checkout specification, so this is not entirely unrealistic. Here's the code which the merchant uses to create a payment request:

var methodData = [
    {
        supportedMethods: [ "https://secure.checkout.visa.com/pay" ],
        data: {
            apikey: "9898df897df87df987df98d7f987df",
            xPayToken: "xv2:" + timestamp + ":" + hashString,
            settings: {
                payment: {
                    cardBrands: [ "VISA", "AMEX" ],
                    acceptCanadianVisaDebit: false
                }
            }
        }
    }
]
var details = {
    total: {
        label: "Total",
        amount: { currency: "USD", value: "100.00" }
    },
    displayItems: [
        {
            label: "Blue suede shoes",
            amount: { currency: "USD", value: "100.00" }
        }
    ],
    shippingOptions: [
        {
            id: "free",
            label: "Free shipping!",
            amount: { currency: "USD", value: "0.00" }
        }
    ],
    modifiers: []
}

var request = new PaymentRequest(methodData, details);
request.show()

Everything inside methodData.data is proprietary visa checkout stuff, which the browser knows nothing about. A hypothetical canHandle() method provided by the visa payment app, which the user has installed in his browser, could look at the supported cardBrands and the other information in methodData.data and compare this with the cardBrands that the user has previously registered with visa checkout and see if there is a match. If not, then maybe the function should return false.

adamroach commented 7 years ago

@jakearchibald --

I think it's new ground.

From a spec perspective, that's probably true. As I mention above, though, the execution environment I'm proposing is identical to the execution environment PAC files run in: no network access and a unique origin. This has been implemented in browsers for decades.

jakearchibald commented 7 years ago

@tommythorsen

and compare this with the cardBrands that the user has previously registered with visa checkout and see if there is a match

This sounds like it'd require access to origin storage, no? Could you show the code for that bit too?

tommythorsen commented 7 years ago

@jakearchibald

This sounds like it'd require access to origin storage, no? Could you show the code for that bit too?

Yeah, either it needs access to storage, or the function needs to be individually constructed for the user. I have not made any attempts to explore this subject any further than this, so your attempt to implement would be as good as mine. Maybe one of the actual proponents of this mechanism would be more interested in writing some example code for this?

tommythorsen commented 7 years ago

@adamroach

[...] the execution environment I'm proposing is identical to the execution environment PAC files run in: no network access and a unique origin. This has been implemented in browsers for decades.

While it has been implemented in browsers for decades, a quick google search for "pac file attack" makes me doubt that this is proof that it works, security-wise. On the first page, I get the following hits:

Further reading on the subject suggests that the way to stay secure w.r.t. PAC files boils down to "for god's sake, don't let PAC files from a non-trusted source onto your system", rather than "don't worry, the browser's sandbox will keep you safe". Do we think we can make our function more secure than this? My gut feeling says that it's not possible to do this in a way that's absolutely and demonstrably bulletproof.

jakearchibald commented 7 years ago

The design of this (if it's necessary) depends heavily on the kind of access it needs to other things.

@adamroach can you pick up where @tommythorsen left off? Can you write some code that shows what a developer may need to write in a canHandle callback? Something that shows a decision being made that the browser couldn't make itself. Just use the current as-specced API - it'll be enough to get an idea of what's needed.

I imagine there are a few ways to solve this, and some are defined by specs already, but we shouldn't be diving into the solution until we fully understand the problem.

adamroach commented 7 years ago

@tommythorsen -- Those attacks are uniformly due to the role that PAC files play (controlling where all web traffic goes), rather than the environment they run in.

jakearchibald commented 7 years ago

Discussing PAC files is redundant. There are existing specs that could be used for originless computation. There's also the possibility of creating something new.

Sorry to sound like a stuck record but… clarifying the problem and its requirements should be done before searching for a solution.

Can we do that first?

adamroach commented 7 years ago

@jakearchibald --

Sure. There is context here that has probably been lost, since a lot of this arose from discussions between this document, the Payment Request document, and the Basic Card document. Even worse, some of the context is in PR and/or issues rather than being in any current version of the specs, and a lot of it is difficult to keep up with unless you're on the calls.

The driving case for this is the ability to filter payment apps in several dimensions. Using "basic-card" as the example (because it's one that people understand the most simply due to their role as credit card holders); payment apps will want to take into account things such as:

I've heard other attributes floated (e.g., payment providers that are tied to region, or merchants that can't accept corporate cards), but those seem a bit exotic. What's important, though, is to keep in mind that the list of attributes is not fixed at these two, and may well grow over time.

Syntactically, the way this would be indicated by a merchant is as is shown in example 2 of the Payment Request spec:

const methodData = [{
  supportedMethods: ["basic-card"],
  data: {
    supportedNetworks: ['visa', 'mastercard'],
    supportedTypes: ['credit']
    // Any other payment-method-specific fields go here.
  }
}];

...although there has been some discussion of having a separate field for the criteria that will be used to filter payment providers; e.g.:

const methodData = [{
  supportedMethods: ["basic-card"],
  filters: {
    supportedNetworks: ['visa', 'mastercard'],
    supportedTypes: ['credit']
  },
  data: {
    // Any payment-method-specific fields go here.
  }
}];

So basically, what we're trying to avoid here is having a merchant say "I can take 'basic-card', but only Mastercard and Visa, and I don't accept debit cards" and then have payment providers that only provide basic-card American Express and/or basic-card debit show up in the list.

The discussions we had basically laid out two options:

  1. Come up with a general algorithm in which the payment app indicates, at registration time, the precise combination of payment method filters it matches; or
  2. Allow the payment app to register a pure function that evaluates the filter.

I personally prefer the first option, and think it can be made to work; but there was overall consensus from others to go in the second direction. The rationale, as I can best explain it, is that coming up with a usable syntax for payment apps to indicate which filters they should match is going to be incredibly complex for anything beyond the most trivial cases, while registering a pure function to evaluate filters is much easier for app developers to understand.

The question that is the subject of this issue is whether we have erred in this evaluation, and should instead attempt to pursue option 1.

adamroach commented 7 years ago

@jakearchibald -- That's the problem statement, at least. As for requirements, the top-level, all-caps, blinking, red, bold, 96-point headline one is that -- for a variety of both business intelligence and privacy reasons -- when a user is making a purchase, no payment app other than the one that user ultimately selects can know anything about the transaction, including knowing that a transaction has occurred.

We also have to know, on a per-payment-option basis, which options should be presented to a user if the payment app matches (e.g., if a payment app has a credit card and a debit card, and the merchant says they can only take credit cards, then the debit card option should not be shown to the user for selection).

jakearchibald commented 7 years ago

Generally I'm a fan of code rather than config, as it's easier to debug & follow, but the complexity in making this work with the restrictions could remove a lot of those benefits.

We need a series of use-cases, both common & exotic.

The champions of "config" need to show (with code) that these use-cases can be met, or justify why it isn't a big deal that particular cases can't be met.

The champions of "execution" (which is a great name for a wrestling tag-team) need to do the same, including detailing when the code executes. Ignore the sandboxing for now.

This should reveal:

After this, if "execution" looks like the best solution, we can look at the examples and pick a sandboxing method. Which is best depends on what kind of event loop and API access it needs. But we need use-cases and code examples before we tackle that.

So, who's going to provide the use-cases (sounds like there are some already)? Who are the champions?

adamroach commented 7 years ago

@jakearchibald -- agree with the approach. One comment:

If fixing the security issues of "execution" also prevent use-cases. As in, if some use-cases need network access, then that requirement clashes with the security requirements.

If it is, from an information theory standpoint, even possible to craft a "config" option (which by definition cannot access the network at evaluation time) to satisfy a use case, then it must be possible to craft an "execution" option that does not need to access the network for that same use case. This third bullet is a red herring.

adrianhopebailie commented 7 years ago

For some context, this is a requirement that has migrated from the payment request discussions to here because the editors/implementors felt a "generic" filtering algorithm was a bad idea.

I stand to be corrected but I think @zkoch made the statement that the security team at Google were not happy with the idea of processing filters that were not from a predefined set.

As such, browsers will natively support SOME filtering for well-known payment methods (like basic-card) but there was no mechanism for developers of new payment methods (like https://bobpay.com) to define their own method-specific filters.

@ianbjacobs , @mattsaxon and I spent a lot of time proposing ideas on how this might work. It went from query string params in the identifier to stand-alone filter data in the PaymentMethodData as @adamroach describes above.

I would agree that if we can come up with a config only way for this to work then that would be first prize but it would be useful to hear from the implementors if this is something they would support.

marcoscaceres commented 7 years ago

Was web crypto considered? I've not done the work yet to see how it would work to encrypt ".data", but might save me some time exploring that rabbit hole if someone else has already evaluated it.

On 28 Jan 2017, at 12:17 am, Adrian Hope-Bailie notifications@github.com wrote:

For some context, this is a requirement that has migrated from the payment request discussions to here because the editors/implementors felt a "generic" filtering algorithm was a bad idea.

I stand to be corrected but I think @zkoch made the statement that the security team at Google were not happy with the idea of processing filters that were not from a predefined set.

As such, browsers will natively support SOME filtering for well-known payment methods (like basic-card) but there was no mechanism for developers of new payment methods (like https://bobpay.com) to define their own method-specific filters.

@ianbjacobs , @mattsaxon and I spent a lot of time proposing ideas on how this might work. It went from query string params in the identifier to stand-alone filter data in the PaymentMethodData as @adamroach describes above.

I would agree that if we can come up with a config only way for this to work then that would be first prize but it would be useful to hear from the implementors if this is something they would support.

— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub, or mute the thread.

marcoscaceres commented 7 years ago

I did the exploration work of using Web Crypto here: https://github.com/w3c/webpayments-payment-apps-api/issues/99#issuecomment-276264871 and had someone from the WG check it. It seems doable, but has a network request penalty (decryption needs to be done on server).

adrianhopebailie commented 7 years ago

I don't think crypto will work here for a few reasons.

  1. PaymentMethodData:PaymentMethodIdentifer is a 1:Many relationship

Each PaymentMethodData object in a payment request is has a sequence of payment method identifiers. This is by design, because it's possible that a merchant may wish to group the data they provide under a set of payment methods for which the data is common.

  1. Not all payment methods will easily support PKI

What would the public key for the basic-card payment method be, for example? We have reluctantly agreed that for "proprietary" payment methods we require a payment method manifest that defines some details (such as allowed payment apps) to be hosted by the publisher of the payment method. To expect that entity to also host public key infrastructure seems like a tall order.

adamroach commented 7 years ago

@jakearchibald -- It came up on today's call that the algorithmic means of describing this was discussed this past summer; see https://github.com/w3c/webpayments-method-identifiers/issues/5#issuecomment-225665121

adamroach commented 7 years ago

So, as a thumbnail sketch of how we can do this in a configuration way (instead of an executable way), I propose concretely that we:

(a) Move those attributes that can be used to distinguish among payment apps into a "filter" property:

const methodData = [{
  supportedMethods: ["basic-card"],
  filters: {
    supportedNetworks: ['visa', 'mastercard'],
    supportedTypes: ['credit']
  },
  data: {
    // Any payment-method-specific fields go here.
  }
}];

On the payment app side, payment options include capabilities that are matched against these filters:

paymentMethodOptions.set("option-id-1", {
  supportedMethods: ["basic-card"],
  capabilities: {
    supportedNetworks: ['visa'],
    supportedTypes: ['credit']
  },
  // ... Other information about the payment option, such as "Visa ending in 1111"
});

Then, to determine matching (pseudocode):

set matchingOptions to an empty set

for requestMethod in paymentRequestMethodData:
  for app in paymentApps:
    for option in app.options:
      set optionMatch to true
      for filterName in keys(requestMethod.filters):
        if option.capabilities does not have key filterName, set optionMatch to false
        else if the intersection of requestMethod.filters[filterName] and option.capabilites[filtername]
          is empty, set optionMatch to false
      if optionMatch is true, add option to matchingOptions

Notably, this means that if the payment request asks for a filter (e.g., "country: 'gb'"), but the payment app does not include a matching capability (e.g., has not included "country" in their list of capabilities), then that payment app will not match the request. I believe the country example I give highlights why this is the correct way to handle things. The implication here is that payment apps will want to register will all of the capabilities associated with their payment method.

rsolomakhin commented 7 years ago

@adamroach proposed:

const methodData = [{ supportedMethods: ["basic-card"], filters: { supportedNetworks: ['visa', 'mastercard'], supportedTypes: ['credit'] }, data: { // Any payment-method-specific fields go here. } }];

I like this approach to filtering, but would prefer to avoid breaking changes to the PaymentMethodData structure. So, let's not add filters, but define an algorithm for the browser to filter payment apps based on subset of data. In particular, let's consider all lists of strings to be filters. First, a payment app should tell the browser how it should be matched by filters:

hypothetical.api.register(
    "basic-card",
    "MyBank's Debit Card",
    data: {
        supportedNetworks: "visa",
        supportedTypes: "debit"
    });

Second, the merchant website requests a payment:

new PaymentRequest([{
        supportedMethods: ["basic-card"],
        data: {
            supportedNetworks: ["visa", "amex"],
            supportedTypes: ["debit", "prepaid"],
        }
    }], details);

Third, the browser runs the matching algorithm for PaymentRequest.show(). This matching algorithm returns the list of payment methods that the user should be able to select:

rsolomakhin commented 7 years ago

@adamroach wrote:

payment apps will want to register will all of the capabilities associated with their payment method.

+1

jakearchibald commented 7 years ago

@adamroach that's way less DSLy than I expected. If it covers all the use-cases, it seems like a pretty good pattern. Thanks for putting it together!

adrianhopebailie commented 7 years ago

@rsolomakhin, I think we may need to discuss this step:

  • If matchingFiltersNumber is equal to len(dataKeys), i.e., all filters match:
    • Add method to matchingMethods.

A note from previous discussions on this topic for context, an app that wishes to "farm" merchant data or always be presented to the user is simply going to claim support for as many payment methods as it knows about with the bare minimum filters. That way it increases its odds of being selected to handle a payment and can try to enroll the user for a requested method once it is invoked. (i.e. It fakes support to get selected and then tries to "wing it" once it is, by supporting basic card as a fallback). Worst case scenario it gathers data about the merchant and it's accepted payment methods.

One way to counter this is to only allow the request to specify a "wildcard". i.e. A merchant can submit a request with the method bobpay.com and no filters and this will match a payment app that is registered to handle the bobpay.com payment method but does have additional filters specified.

Another possibility is that browsers intelligently order options presented to the user based on how often that option has resulted in a failed request in the past?

rsolomakhin commented 7 years ago

An app that wishes to "farm" merchant data or always be presented to the user is simply going to claim support for as many payment methods as it knows about with the bare minimum filters.

There's no 100% guaranteed way to avoid this, but the user agent can use own signals to detect such apps and penalize them, somehow. I don't see how this affects the spec, though.

Another possibility is that browsers intelligently order options presented to the user based on how often that option has resulted in a failed request in the past?

We plan to do this.

adamroach commented 7 years ago

@adrianhopebailie --

One way to counter this is to only allow the request to specify a "wildcard". i.e. A merchant can submit a request with the method bobpay.com and no filters and this will match a payment app that is registered to handle the bobpay.com payment method but does have additional filters specified.

I'll note that the algorithm I sketch out has this exact property: if a merchant specifies a filter, then the payment app must have a matching capability. If the payment app has a capability that the merchant hasn't requested, that makes no difference.

Also, I think it muddies the conversation to think of these as "filters" on the app side -- they're capabilities. The merchant is specifying a filter that demands certain capabilities of payment apps in order for those apps to match. If you think of them in that way, it makes it much easier to see how they should interact.

adrianhopebailie commented 7 years ago

Also, I think it muddies the conversation to think of these as "filters" on the app side -- they're capabilities. The merchant is specifying a filter that demands certain capabilities of payment apps in order for those apps to match. If you think of them in that way, it makes it much easier to see how they should interact.

+1

marcoscaceres commented 7 years ago

It's still not clear to me if "native" payment handlers will have more capabilities to determine if .canMakePayment() can respond with "true" than the filtering here allows. That is, if a native (e.g., Android Pay, Apple Pay, Microsoft Wallet, or a native app installed from an app store) payment handler can do its .canMakePayment() support by:

Then we are doing the web a disservice by not providing an analogous set of capabilities (via service workers).

However, if the answer to .canMakePayment() is that native payment solutions only do the string matching, then filtering is fine.

rsolomakhin commented 7 years ago

Apple Pay has canMakePayments and canMakePaymentsWithActiveCard. Android Pay has isReadyToPay. These methods precede our enlightened conversations, however, and therefore should not be taken as gospel, IMHO.

marcoscaceres commented 7 years ago

canMakePaymentsWithActiveCard... This method asynchronously contacts the Apple Pay servers as part of the verification process.

I rest my case.

adamroach commented 7 years ago

I'm a bit confused. Every time Nick Shearer has spoken on this topic, he's indicated that these Apple Pay methods are very limited, and effectively provide the application hints (not promises) about whether there is some kind of payment instrument registered. It doesn't even allow the requestor to indicate which kind of card they're asking about -- it's just "is Apple Pay activated on this device in a way that it could, in theory, for some card, and I don't know which one, possibly but not certainly, make a payment?"

And I'm pretty confident that doing so does not involve any network interaction whatsoever.