Purchase links @readino.com: a suggestion #3031
Replies: 36 comments 4 replies
-
Hi Nikolay, how about regenerating a temporary session token (signed or not) for each "buy" link and replacing/invalidating it with a session cookie after the first request in the browser by the user ? Like this : https://en.wikipedia.org/wiki/Session_fixation#Regenerate_SID_on_each_request It seems to me enough secure and do not imply to edit the OPDS2 specification. What do you think ? |
Beta Was this translation helpful? Give feedback.
-
I’d like to describe the following scenario:
There are other scenarios where the last opened feed leaks — and anyone can use the link to gain access. This is why we prefer using short-lived URLs. If a link is only valid for one minute, the window of exposure is minimized and the risk is significantly reduced. As a side note, we're not suggesting changes to the OPDS spec itself. Our only proposal is to handle links with specific MIME types in a custom way. For example, in FBReader, we already support the non-standard MIME type |
Beta Was this translation helpful? Give feedback.
-
@HadrienGardeur any thoughts? |
Beta Was this translation helpful? Give feedback.
-
One more consideration. I’m not just concerned about the customer’s security — I’m also worried about the store’s reputation. If someone shares a book link with me that grants access to their account, I won’t misuse it. But I also won’t trust a store that allows such links to exist. |
Beta Was this translation helpful? Give feedback.
-
I don't think that an additional media type is necessary for what you're trying to achieve. OPDS clients can already handle authentication either by:
Let's say that I encounter a buy link then, it could look like this (I'm dropping the price and indirectAcquisition on purpose to make it shorter): "links": [
{
"rel": "http://opds-spec.org/acquisition/buy",
"type": "application/opds-publication+json",
"href": "https://example.com//api/v1/purchase/9781119701286/opds/",
"properties": {
"authenticate": "https://example.com/authentication.json"
}
}
] When following this link, clients would receive a 401 with an Authentication Document, but if they support the Once authenticated, this would return a link to an HTML page with a one-time use token: "links": [
{
"rel": "http://opds-spec.org/acquisition/buy",
"type": "application/opds-publication+json",
"href": "https://example.com//api/v1/purchase/9781119701286/html?token=abc123"
}
] The client could then redirect automatically the user to a browser at that URL. This is very similar to lending in OPDS where a borrow action returns an OPDS publication which either confirms:
Plus to make things extra smoother, you could also include this same |
Beta Was this translation helpful? Give feedback.
-
You can make your link both one-time use AND extremely short-lived to handle that concern. Since the HTML link is immediately consumed upon generation, you can make it work for only a minute or so. |
Beta Was this translation helpful? Give feedback.
-
Hi Hadrien, |
Beta Was this translation helpful? Give feedback.
-
The It'll be added to a future revision of the Authentication for OPDS spec along with updated OAuth grants and security best practices. |
Beta Was this translation helpful? Give feedback.
-
... but if you were focusing on expected behavior when buying an ebook in OPDS it's very much undefined because of 10+ years of Apple and Google making things impossible on mobile. Now that we can finally sell content in the US on iOS, it's opening up these discussions all over again. |
Beta Was this translation helpful? Give feedback.
-
It seems that your approach is essentially the same as ours, just using a different MIME type and response format. I'm not insisting on our variant—we’re still in the development phase, so switching is easy for us.
That's the key point. If Thorium implements that behaviour, our goal is achieved. Let me clarify a few points:
|
Beta Was this translation helpful? Give feedback.
-
Indirect acquisition through an OPDS publication is how we handle lending, so it makes sense to remain consistent and use it for purchasing as well.
It should be the entire publication. Some catalogs could skip HTML entirely if they already have a valid credit card saved for the user and directly return the publication that way. For example this could be a link to acquire a DRM free EPUB: {
"rel": "http://opds-spec.org/acquisition/buy",
"type": "application/opds-publication+json",
"href": "https://example.com/purchase",
"properties": {
"authenticate": "https://example.com/authentication.json",
"indirectAcquisiton": [
{
"type": "application/epub+zip"
}
]
}
} If they don't have a credit card saved or if it's no longer valid, then they could redirect the user to an HTML page as discussed above. Also, given the media type (an OPDS publication), it would be invalid to just return a link.
It's optional but streamlines things quite a bit. Instead of You can also optimize the process by including |
Beta Was this translation helpful? Give feedback.
-
If I can help in the debate : So what I suggest and already implemented in Thorium and should be implemented in either opds clients : it's to add the one time buy link only in the self link opds-publication, and remain a generic (authentication free) link in your opds-feed. So even if the opds-feed leaks and become public, there are no one time buy link available. On the opds client when the user choose a book, the opds-publication from the opds-feed is displayed first and then a update request is made to display the opds-publication self link info. 2 phases : the user click on the publication, then publication Info appears with only the opds-publication from the feed and then after the request made by thorium to the self link opds-publication, the new buy link with the ONE TIME authenticated token is updated to the href buy button. Finally the user can make his purchase on his web browser with his unique one time token info signed by the server with TTL. So there are very little chance, that this authentication token could be stollen. And moreover you can attribute limited right to this authentication token (and then cookie if needed) : for example only buy this specific book and not allow any viewing/editing settings parameter without new authentication requested. We are on the backend realm now, you can implement any secure measure what you want. Summary : opds-feed : {
"metadata": {
"title": "Readino",
},
"links": [
{
"rel": "self",
"href": "/api/v1/opds2",
"type": "application/opds+json"
}
],
"groups": [
{
"metadata": {
"title": "Bestsellers"
},
"links": [
{
"rel": "self",
"type": "application/opds+json",
"href": "/api/v1/opds2/bestsellers"
}
],
"publications": [
{
"links": [
{
"rel": "http://opds-spec.org/acquisition/buy",
"type": "application/opds-publication+json",
"href": "/api/v1/purchase/9781119794615/6/Partner",
}
},
{
"rel": "self",
"type": "application/opds-publication+json",
"href": "/api/v1/opds2/book/9781119794615"
}
],
"metadata": {
"@type": "http://schema.org/Book",
"title": "Advanced Accounting",
"identifier": "urn:isbn:9781119794615",
}
}
]
}]} The buy link from the opd-feed must be generic and potentially redirect to an opds-publication with the next authenticated buy link. The first buy link from an opds-feed will be in theory never requested by the user, because the opds-publication self link will be requested by thorium-reader before and automatically updated/displayed to the user.
{
"links": [
{
"rel": "http://opds-spec.org/acquisition/buy",
"type": "text/html",
"href": "/api/v1/purchase/9781119794615/6/Partner?token=userSignedTokenFromTheServerToDoOneTimeBuyAuthentication",
}
},
{
"rel": "self",
"type": "application/opds-publication+json",
"href": "/api/v1/opds2/book/9781119794615"
}
],
"metadata": {
"@type": "http://schema.org/Book",
"title": "Advanced Accounting",
"identifier": "urn:isbn:9781119794615",
}
} Steps: 1/ user choose bestsellers opds-feed I hope this is clear, I do my best to write spontaneously in english with my french thought :) I will try to make an opds test server if I have time, to prove/test my proposal. Edit: with this method like Hadrien suggest, you can lay down any opds-feed on CDN/Cache mechanism, It's become just a static file, I guess. It will be good for your saving :) |
Beta Was this translation helpful? Give feedback.
-
Agree.
Ok, we can update our dev catalogue shortly.
This case is much more clear; it does not require any action on the client side. This is how borrow links work in the libraries we support at the moment.
This seems to be the most interesting part of the process. As I understand it, the flow is as follows:
As a side note, the approach seems clear to me overall, although returning the entire entry just to convey a single URL feels a bit like a workaround. |
Beta Was this translation helpful? Give feedback.
-
We cannot do that, opds is state less, we are not implementing a finite state machine on the client side. I regret. |
Beta Was this translation helpful? Give feedback.
-
Thank you for this clarification. We are not concerned about the
Yes, we can certainly explore something along these lines. Thank you for the detailed explanation of the idea. Alternatively, we might simply provide an HTML link that requires authorization in the browser. This approach is less user-friendly but avoids compromising on security. However, it introduces a different issue: if the feed includes both an In our current setup, where we use a custom |
Beta Was this translation helpful? Give feedback.
-
So, according to the code, there is no way to make our custom type link "invisible" for Thorium? Any chance to add such a feature? |
Beta Was this translation helpful? Give feedback.
-
How FBReader solve the problem ?
we can update the codebase and make a PR to redirect to an external web browser, only content visible mime-type interpretable, like : |
Beta Was this translation helpful? Give feedback.
-
We have still not implemented two buy links, so FBReader doesn't solve the problem at the moment. Most likely, when a book contains two buy links—one with our custom MIME type and one with "text/html"—we’ll implement logic to always prefer the custom type and ignore the "text/html" link.
That would be great for us, but I suspect that might break look of some third-party OPDS in Thorium. As for FBReader, it ignores links with unknown/unsupported types. |
Beta Was this translation helpful? Give feedback.
-
I'm not following you at all on that statement @panaC because what I'm describing is very much RESTful:
Let's take another example: lending.
I don't think it's accurate to say that a client is not stateless if it:
|
Beta Was this translation helpful? Give feedback.
-
For the We want to move beyond having just a simple OPDS parser in Readium Mobile, which means natively supporting authentication and interactions to make it easier for implementers. While A good example of that would be a customized homepage:
You can't rely on a |
Beta Was this translation helpful? Give feedback.
-
Links with unknown media types should be ignored by clients, I don't think it's a good idea to still display an action for them. For example, you could have:
In this case, it wouldn't make any sense to display two download links if you only support LCP. |
Beta Was this translation helpful? Give feedback.
-
What about cookie like any web browser ? We do support cookies in Thorium since some (old) opds feed require them. |
Beta Was this translation helpful? Give feedback.
-
What you said, if I follow you, is in fact totally "stateless" indeed and respect HATEOAS principle. What we cannot do (as the previous example discussion) is to automatically simulate a click to the download button from a previous action that returns an opds-publication. I'm really sorry if my writting is not clear, but i think we are on the line together, many thanks for your response Hadrien 🙏 EDIT: We can borrow it, but we should not force the user to download it, until he asks for it. Just we cannot "automatically follows that download link" do this |
Beta Was this translation helpful? Give feedback.
-
Let's improve that !
This is already the case for thorium-reader, this discussion is mainly about the "buy" button, I think. EDIT: borrow link always display a button on unknown content-type, except for Adobe or the button is disabled |
Beta Was this translation helpful? Give feedback.
-
thorium-reader/src/main/converter/opds.ts Lines 326 to 348 in 27a3ce9 Buy is not filtered by their types thorium-reader/src/main/converter/opds.ts Lines 340 to 342 in 27a3ce9 We have to think about badly implemented opds-feed, so we cannot filter on unknown content-type but only on unsupported one, I guess. What is the safe content-type list for you :
What else ? |
Beta Was this translation helpful? Give feedback.
-
I don't think there's much of a difference between these examples. Let's go back to my example for borrowing:
You can't say that client A is not stateless because it follows a link in a response, that's not any different from following a redirect in a 3xx response. The situation is exactly the same here with the buy use case that we're talking about. Upon receiving a response that contains a link to HTML:
The server can even set these expectations from the very beginning through the use of {
"rel": "http://opds-spec.org/acquisition/buy",
"type": "application/opds-publication+json",
"href": "https://example.com/purchase",
"properties": {
"authenticate": "https://example.com/authentication.json",
"indirectAcquisition": [
{
"type": "text/html",
"indirectAcquisition": [{"type": "application/epub+zip"}]
}
]
}
} This tells the clients in advance that to complete the transaction, they'll first get an OPDS publication, then go through a webpage before they can finally obtain an EPUB. |
Beta Was this translation helpful? Give feedback.
-
You could also receive the publication directly in the response, so you'll need to allow the media type for an LCP license along with every supported publication format. |
Beta Was this translation helpful? Give feedback.
-
This would require the server to set cookies (which we don't need/want for OAuth based workflows) and wouldn't solve this use case either. The presence of |
Beta Was this translation helpful? Give feedback.
-
This is a great discussion but at this point this is not an actionable issue in the tracker ... transferring. |
Beta Was this translation helpful? Give feedback.
-
There was one actionable issue inside, I think: to stop processing unknown mimes in buy links as text/html (i.e., not open them in a browser, just ignore them). @danielweck, do you think it should be made a separate ticket? As for other ideas, I'm currently on holiday. When I return (in mid-July), I will write a list of the "extension" we use in our catalogue that we want to discuss and maybe suggest for the standard and/or for Thorium. For the discussion, suppose we want to add some processing for new mime types. If we are ready to contribute to Thorium, does it make sense? Will such patches be considered, or do you have a strong rule "not to process anything out of the standard"? |
Beta Was this translation helpful? Give feedback.
Uh oh!
There was an error while loading. Please reload this page.
-
Hi,
In our OPDS 2.0 catalog, we've implemented
http://opds-spec.org/acquisition/buy
links to support purchases. However, these links currently do not work correctly in Thorium, due to our use of a non-standard step in the purchase flow.Our goal is to allow users to initiate purchases by clicking the "Buy" button in clients like Thorium or FBReader. The purchase page is hosted on our website but redirects users to a Stripe checkout form. To make this seamless, the user must be authenticated on our site.
Initially, we considered embedding sign-in tokens directly in the feed links. However, this approach is insecure—if the feed content leaks, unauthorized parties could gain access to the user’s account.
Instead, we introduced an intermediate step:
application/x-short-lived-link+json
).This approach provides both:
We’re open to modifying our approach if there's a better way to meet these goals.
The feed with working links (Stripe integration is currently in test mode) is at https://dev.readino.com/api/v1/opds2
Best,
-- Nikolay
Beta Was this translation helpful? Give feedback.
All reactions