Authorized Payments using SPSP and STREAM

The current SPSP payment structure has no notion of authentication. This means anybody can push or pull value to or from an SPSP endpoint if the particular endpoint is known. While one can argue that this is a feature in the case of push payments (anonymous payments), it is definitely not acceptable in the case of pull payments from a security perspective (I’m happy to be proven wrong because what I’m about to propose is more complicated :wink:). I’ll recap the current flow using the wallet use case and outline the problems.

Current architecture

The wallet runs an SPSP server that keeps track of users’ balances. Each user has their own payment pointer to which they can receive money, e.g.

If Bob wants to send money to Alice, he queries their endpoint to receive a destination account and shared secret, connects to this account via STREAM and sends packets.

However, Alice does not know where those packets are coming from.

In the case of pull payments, Company requests a pull payment pointer from Alice which they would have to create using the wallet interface. It would look like this:

Company can query this endpoint, which is a special one for pull payments and this particular pull payment agreement, create a STREAM connection and receive value.

However, consider Devil to be a malicious user trying to pull money. They could just brute-force try different combinations of $wallet.com/username/uuid and would probably find a working endpoint they could exploit. While I agree that this is unlikely, $wallet.com/username/uuid could also be exposed to an ISP in case of a DNS leak.

I’m not an expert on wallet laws but I fear that these are risks that wallet providers are not willing to take.

Proposal: Authorized payments

Alice has one payment pointer for everything, it is their identity within wallet.com: $wallet.com/alice. Nobody can query, push or pull money without an access token. A GET request to $wallet.com/alice without a token (access or refresh) will start an oauth flow as depicted in the figure below. The GET request needs to contain the scope of the preferred action (query, push or pull, where push and pull include query).

Push flow

  • GET request from Company to $wallet.com/alice
    • Scope: push
    • Parameters: sending entity = Company
  • Wallet authenticates Company somehow (This part is still a bit unclear. It could be something wallet specific, i.e. Company needs to register with Wallet, or something broader. I am not an expert on this topic…)
  • (Alice may be asked to agree to push to her account)
  • Wallet returns access token including scope
  • GET request from Company to $wallet.com/alice including the access token
  • Returns destination account and shared secret
  • Company connects to Wallet using destination account and shared secret
  • Company opens a stream connection
  • Company sends the access token via data stream
  • After verifying token, the wallet sets receiveMax
  • Company streams money to Alice’s account

Alice can be sure that she is not receiving dirty money because there is a valid token sent with every request. In the case of querying the endpoint, which are steps 1-6, Alice can be sure that nobody receives her STREAM details. I don’t give my bank details to anyone either.

Pull flow

  • GET request from Company to $wallet.com/alice
    • Scope: push
    • Parameters: sending entity = Company, pull payment agreement
  • Wallet authenticates Company somehow again
  • Wallet returns access token including scope and agreement
  • GET query request from Company to $wallet.com/alice including the access token
  • Returns destination account and shared secret
  • Company connects to wallet using destination account and shared secret
  • Company opens a stream connection
  • Company sends the access token via data stream
  • After verifying token, Wallet starts streaming value to Company

Company needs to be authenticated by Wallet and especially pull payments need to be authorized by Alice. One could maybe even omit step (3) in my figure (authenticating Company) or make it optional if Alice needs to authorize incoming and outgoing payments. We can argue that Alice can make informed decisions on whether she wants to receive money from Bob or allow Company to pull. If she is shopping in a known online store, she can be quite certain that the request for pull pointer that is popping up on checkout is legit.

How could that work with Codius?

We wanted to enable pull payments in Codius such that pods are automatically extended. This requires a pull payment pointer when a pod is uploaded. Since there is no fully developed GUI for it (yet, at least not that I know of), there can not be a nice little popup to authorize the payment. However, Wallet could allow for pre-generation of access and refresh token on their platform that one has to copy. Let’s face it: At the moment, you have to know your stuff to upload to Codius anyway :wink:.

1 Like

Awesome work, Sabine!

In the pull payment flow, how do we guarantee that the correct amount is streamed from the sender (Alice’s Wallet) to the receiver (Company Wallet)? If Alice provides an auth token for insufficient funds, then what does that mean for user-experience? Does she have to confirm and send additional auth tokens? How are partial payments handled by the receiver – is this state logged? Can malicious actors simply initialize a series of partial payments in an attempt to overload the receiving server? Maybe auth tokens help with this? If Alice provides a pull payment pointer over the requested amount, the receiver will just “spend” the remaining funds, correct? I was taking a second look at the pull payment RFC; but it does not directly address these concerns.

Using SPSP and STREAM, I don’t think we can guarantee that. The wallet could include a mechanism to check that when the pointer is created, there are sufficient funds available for at least the first pull. I don’t think that should be in the spec, though. This can be a wallet specific check.

Company would always check whether the token itself allows for enough value to be pulled. The agreement should therefore be stored within the token. The amount should already include all fees that Company may incur (just like paypal, where Company pays the fees). This becomes interesting if Alice just does not have enough funds in her account. We may want a refund mechanism, i.e. Company doesn’t receive enough, reports an error in the UI and refunds whatever it has received minus fees. Alice would then be asked to create a new token to try the pull again. Similar like when a credit card is declined.

If the agreement allows for 10 XRP to be pulled within 1 day and Company decides to pull 3 in the morning, 4 at noon, and 3 at night, the states are stored within the SPSP server on the sender’s side. The receiver would have to implement its own accounting tool on the client side. There is already @wilsonianb’s pull manager that was implemented to allow a codius host to deal with pull payments.

By receiving server you mean the wallet? I.e. the server that is receiving the pull requests? I guess that could happen now in the current implementation as well as in the auth-token based one. However, the auth token allows the server to identify the malicious actor and to ban them.

I’m afraid I don’t understand this question. What do you mean by “spend” the remaining funds?

1 Like

Here are some comments DJ made that I thought I should share with everybody interested in this topic:

  • DNS would only leak wallet.com. The full URI is sent only over https, so that part shouldn’t be an issue.
  • Isn’t Alice technically receiving money from her (trusted) connector, and Bob is paying a connector earlier in the chain? i.e. Bob isn’t really paying Alice – any regulatory responsibilities would belong to the intermediate connectors, not Alice/Bob. Maybe from a legal POV that isn’t actually good enough, but it seems worth considering :stuck_out_tongue:
  • In your diagram, why doesn’t Alice use her wallet to generate a payment pointer $wallet.com/alice/1234-asdf-, give that to Company, and assume that any payments to that pointer are from Company (Company is responsible for keeping the pointer secret).
  • Lastly, you’re probably aware of it already but XRP ledger has “Deposit Authorization” which is similar in objective (https://xrpl.org/depositauth.html)

And here is my thoughts about that:

DNS would only leak wallet.com

Very good point that I was not considering.

Isn’t Alice technically receiving money from her (trusted) connector, and Bob is paying a connector earlier in the chain?

Let’s say there is Bob - ConnectorA - ConnectorB - Alice. Alice trusts ConnectorB and ConnectorB trusts ConnectorA. ConnectorA, however, I a huge one that does not KYC its peers. ConnectorA allows anybody to connect to it. Hence, Bob could send dirty money to Alice without her consent. She may not even notice it because she doesn’t check her wallet every day. The money is not staying at ConnectorA or ConnectorB so they are not in trouble, or only slightly because they did business that included dirty money. By including an auth flow, we can make sure that the money flow doesn’t even happen if Alice doesn’t want to.
Actually, I have no idea how the legal consequences of such a transaction are and I do see that this is not a very strong argument. In general, I’m more worried about the pull part than the push part.

why doesn’t Alice use her wallet to generate a payment pointer $wallet.com/alice/1234-asdf- , give that to Company, and assume that any payments to that pointer are from Company (Company is responsible for keeping the pointer secret)

This is exactly what I assumed so far. When discussing with @matdehaast and @don, they were very certain that the security is too low for a wallet provider. Access tokens only have a short live span whereas the unique pull pointer is active as long as the pull agreement is valid. This is probably something that we should discuss with wallet providers.

Can you elaborate on how oauth fits in this payment flow? Who is acting as the identity provider?

Could we solve all these problems by saying:

  1. A pull pointer can include an expiry
  2. There’s a well-defined OAuth based flow to get an auth token that lets you request pull pointers

We’d have to define the OAuth flow, and I think this proposal lays a lot of groundwork for that, but it would make it so that all this complexity isn’t present in the core pull payment spec.

1 Like

If we use the roles defined in the awesome OAuth 2 Simplied then I’d map them as follows for a pull payment:

OAuth 2 ILP
The Third-Party Application: “Client” Payee
The API: “Resource Server” The ILP endpoint of the wallet
The Authorization Server Wallet
The User: “Resource Owner” Payer

A Payment Pointer is just a URL. The resource behind that URL is a JSON document that currently is expected to contain at least an ILP Address and secret used to open a STREAM connection.

The question is, how sensitive is this data? If it can be used to pull payments then I’d suggest it’s very sensitive and should not be accessible without some form of authentication.

If it’s just for push then that’s less risky.

As a general rule it is probably best if Payment Pointers that return an SPSP response do expire. We should add to the spec that it is good practice for SPSP Servers to return a 410 Gone response after an elapsed expiry period.

This is along the lines of what we have been exploring. Almost all online auth today is built on OAuth 2 (which is just a framework). We should do the same. We don’t need to redefine OAuth, just be specific about HOW it is used.

Looking at how this is being done with other financial APIs @matdehaast , @don and I were discussing this flow:

  1. The user gives the client (merchant) their Payment Pointer which is hosted by their wallet. (E.g. $wallet.example/alice)
  2. Client does a GET to the URL resolved and get’s back info about the OAuth 2 endpoint for the user AND the URL where it can request a new Pull Agreement (the payment industry calls these mandates or abstractly a payment intent) or we define a standard URL path for the Pull Agreement API.
  3. Client makes a POST to the Pull Agreement endpoint with the details of the requested agreement and gets back the URL of the newly created mandate (which could also be a Payment Pointer) that has not yet been authorized by the user (e.g. $wallet.example/27c0a7b7-c037-49aa-a862-0c15c1b0d524). The request is validated against standard business rules of the wallet such as a minimum expiry, supported asset code etc but not yet authorized by the user.
  4. Client initiates an OAuth 2 flow via the OAuth 2 endpoint providing the URL (Payment Pointer) of the mandate as the resource it is requesting access to.
  5. User gives consent for client to get access to the resource (i.e. ability to use Payment Pointer)
  6. Client hits resource URL using access token gained via OAuth as bearer token
    6.1. and gets back STREAM connection credentials, or
    6.2. and uses ILP-over-HTTP to push payments to itself.

I know we don’t all agree on which is better (6.1 or 6.2) but I think everything up to that point is a solid flow that aligns with current best practise and leaves very little for us to define outside of existing OAuth 2 (and possibly the extensions of OAuth 2 that have already been defined by the Financial API WG at Open ID Foundation).

Doesn’t the client have to have pre-registered its oauth redirect uri with the wallet beforehand?

I can’t see any actual difference between authorization (via some kind of header) and no authorization (where we just use the path in the URL).

The pull pointers are generated for each pull payment so they’re already not public.

Headers and the URL path both go into the body of the HTTP request so they’re both protected by SSL.

And if they’re being fetched using OAuth credentials rather than being hand entered then arguments about how users are used to handling URLs don’t really apply either.

So maybe we’re talking past each other or I’m thinking about pull pointers differently, because I don’t really understand the argument around authentication.

My proposal would be:

  1. Client makes a POST and gets back a pull pointer
    6.1. Client uses the pull pointer using standard SPSP specification to pull money
2 Likes

No, OAuth supports dynamic client registration, see RFC 7591.

By the way, IndieAuth is a good example of an OAuth-based protocol that does many of the things we’d want to do:

https://indieauth.net/

You’re overloading here. Is the path in the URL an identifier for the mandate or is it the auth token?

By making it both you’re limiting how we can use Payment Pointers because if I want to have a mandate that lives longer than a few minutes I’d be reluctant to issue it as Payment Pointer with no additional security around how it’s used. How can I trust that it will be stored securely?

What we’re proposing is that the Payment Pointer represents a mandate. To execute the mandate you access the URL resolved from the Payment Pointer.

A separate issue is whether you have permission to execute the mandate. This is determined by the wallet using an auth token that must accompany the request. Auth tokens can have their own life-cycle and can be refreshed or revoked without needing to change the mandate itself.

As a wallet this is important because I would want to give my users the ability to manage their mandates (which may have long life-cycles, possibly months, years or even infinite) but not need to deal with managing the life-cycle of the client auth tokens.

So far, there is no explicit authorization. The path in the URL is an identifier for the mandate. Since the user (Alice) has to create the pull pointer themselves (either within their own SPSP server or on the wallet’s SPSP server somehow), it is believed that they authorize this payment implicitly.
@sharafian correct me if I’m wrong.

Can we trust that the refresh tokens are stored securely? (This is somebody asking that is not too familiar with the specs around OAuth)

To me, this is the greatest value add here.