-
Notifications
You must be signed in to change notification settings - Fork 4
OPDSForDistributors
You're a publisher or distributor. You have some books you've licensed to one or more libraries. You want to get these books into SimplyE.
You want to host the books yourself, but make them available to patrons of that library on demand. You don't want to have to integrate with the library's ILS to check their patron credentials; you trust the library to determine who should have access to the books.
Here's the solution:
- Put up an OPDS feed that lists the metadata for the books, and includes links to their download URLs. Protect the download URLs so that you must present a token to actually download a copy of a book.
- Put up a service that hands out tokens by implementing the OAuth 2.0 Client Credentials flow.
- Create an access key and secret key for the Client Credentials flow. Give these keys to the library that has licensed your books. (Create a different set of keys for every library.)
- Put up an Authentication For OPDS document that links to your token service. Link to this document from your root OPDS feed.
- When a patron requests one of your books from the library, the library will use its access key and secret key to generate a token from your token service. The library will pass this token to the patron, who will use it to download a copy of that book.
Here's how it works:
- When a patron of a library wants to download one of your books, they will contact a server operated by that library.
- The library server will contact your server, using its access key and secret key, and ask for an OAuth bearer token good for 60 seconds.
- When you return the OAuth bearer token, the library server will send this bearer token to the patron's device.
- The patron's device will use the bearer token to download the book from your server.
- After 60 seconds, the bearer token expires, preventing a patron from using it to download every book in your system.
Here's a more detailed look at the software you need to set up for this system to work.
Your OPDS feed won't be shown directly to library patrons; it will be consumed by a piece of software. It's important to include all the metadata you would show to a patron (title, author, categories, and so on), because that information will eventually be shown to the patrons, but there's no need to arrange the books in an order that humans will find attractive. The library's server will be combining your books with books from other sources, and presenting a unified feed (using the metadata you provided) to its patrons.
You should create what's called a Complete Acquisition Feed. This is "a single, consolidated Acquisition Feed that includes the complete representation of every unique OPDS Catalog Entry Document in an OPDS Catalog... to facilitate crawling and aggregation."
Here are best practices for your Complete Acquisition Feed:
-
List all the books, in reverse chronological order (newest first). If something about a book changes (its metadata, for instance), bump it to the top of the list and update its
<atom:updated>
tag. -
Don't put everything on one page; keep it to 50 or 100 books per page. Use the "next" link relation to link from one page to the next.
-
Make sure each book has a link with the relation
http://opds-spec.org/acquisition
. This is where the patron will go to download the book. It should look something like this:
<entry>
<title>A Great Book</title>
...
<link href="https://my-opds-server/the-book.epub"
rel="http://opds-spec.org/acquisition" type="application/epub+zip"/>
</entry>
Next, create a piece of software that implements the OAuth 2.0 Client Credentials flow.
Basically, you need to implement request-response interactions like the following.
Request:
POST /get-a-bearer-token
Host: my-opds-server
Content-Type: application/x-www-form-urlencoded
Authorization: Basic base64({client_id}:{client_secret})
grant_type=client_credentials
Response:
200 OK HTTP/1.1
Content-Type: application/json
{
"expires_in": 60,
"token_type": "Bearer",
"access_token": "zKBkFyWYTYmrRGuER2SmpMc9y3qd8T"
}
The client_id
and client_secret
are the two pieces of information
you'll give to a library to allow them to access your books.
Your expires_in
value should be at least 60 seconds. You need to
allow time for the library's server to propagate the access token to
the mobile client, and then for the mobile client to use it in a
request to your server.
You can issue a token that can only be used to download the specific book, or a token that can be used to download any book on the site. Whatever's easiest for you. The SimplyE mobile client will only use the token it receives to download the specific book it requested.
A library patron with an access_token
can use it to download a book
they would otherwise be unable to download:
GET /the-book.epub
Host: my-opds-server
Authorization: Bearer zKBkFyWYTYmrRGuER2SmpMc9y3qd8T
Now you have an OPDS server that allows an authenticated user to download a book, and you have a service that allows a library to make any patron an "authenticated user" for a limited time.
But there's no connection between these two pieces. There's no
indication that this is the kind of OPDS server that also implements
the Client Credentials flow. Someone looking at the OPDS server can
try to get /the-book.epub
, but they'll just get a 401
error. It's not clear what to do about it.
The Authentication For OPDS document connects these two pieces.
Your root OPDS feed should include a link to your Authentication For
OPDS document on the <feed>
level (not inside any particular <entry>
).
<feed>
<link href="https://my-opds-server/authentication-doc"
rel="http://opds-spec.org/auth/document"
type="application/vnd.opds.authentication.v1.0+json"/>
...
</feed>
You should also serve your Authentication For OPDS document along with
any 401 response code. Be sure to include its media type
(application/vnd.opds.authentication.v1.0+json
) in the
Content-Type
header.
Here's the simplest Authentication For OPDS document that will work in this scenario:
{
"id" : "https://my-opds-server/",
"title": "My OPDS Server",
"authentication": [
{
"type": "http://opds-spec.org/auth/oauth/client_credentials",
"links" : [
{ "rel": "authenticate",
"href" : "https://my-opds-server/get-a-bearer-token"
}
]
}
]
}
This document defines an id
, which establishes which OPDS server
it's talking about.
This document also defines a single authentication flow, which
explains how to get past a 401 error. All you have to do is present an
OAuth Bearer Token that you got through the OAuth Client Credentials
flow. But which URL are you supposed to POST to, to trigger the Client
Credentials flow? That information is in the authenticate
link.
So, if you try to download a book, but you get a 401 error, you're
served this document. You send a POST request to
https://my-opds-server/get-a-bearer-token
and get a bearer
token. Then you repeat the request that got you the 401 error, but
this time you include the bearer token. Now you can download the book.
From an implementation perspective, that's all you need to know. But
you may have noticed some sleight-of-hand in the narrative just
above. The "you" who sends a POST request to
https://my-opds-server/get-a-bearer-token
is different from the
"you" who downloads the book. The "you" who sends the POST is a
library; the "you" who downloads the book is one of the library's
patrons.
The library can get a bearer token with no problem, but it's not the one who needs a copy of the book. How does the book actually get to the patron?
There are two solutions. The first is simple: the library can download the book and send it on to the patron. In this case, the patron never even knows that a token was involved -- it looks like the book came directly from the library. The upside is that this works with every OPDS client. The downside is that it's inefficient (the book is transferred over the network twice) and introduces a new point of failure.
The other solution is to send the token to the client, and have the client redeem it for a copy of the book. We call this "bearer token propagation". You don't need to know how this works unless you are writing an OPDS server for a library, but here's how it works:
There are three parties here: the patron (using a mobile device), the library (a web server), and the distributor (another web server).
The patron is in the middle of an HTTP request. They have just asked
the library to send them a copy of a specific book. The library has
just obtained a bearer token abcdefg
from the distributor. Anyone
who presents that token to the distributor can download that book.
The patron is waiting for an HTTP response. The library doesn't have the book and doesn't want to download it just to pass it in to the patron. Instead, the library sends a document containing the two pieces of information necessary to download the book from the distributor:
- The book's URL is
https://my-opds-server/the-book.epub
. (Obtained from the distributor's OPDS feed.) - You can download the book by providing the bearer token
abcdefg
. (Obtained through the OAuth Client Credentials flow.)
Here's the response the library will send to the patron in this
case. The document below is based on the access token
format defined by
OAuth 2.0. In fact, it's almost exactly the same as the access token
the distributor just sent to the library! The only differences are
that this document has a special media type (it's not
application/json
), and it includes a new location
attribute.
200 OK
Content-Type: application/vnd.librarysimplified.bearer-token+json
{
"expires_in": 60,
"token_type": "Bearer",
"access_token": "zKBkFyWYTYmrRGuER2SmpMc9y3qd8T"
"location": "https://my-opds-server/the-book.epub"
}
These fields come directly from the distributor:
-
access_token
: The access token to use when downloading the book. -
token_type
: This will always be the literal stringBearer
. -
expires_in
: The token is good for this number of seconds.
This field is set by the library to tell the client which book it should be downloading:
-
location
: The URL of the book for which the client requested a copy.
Since not every OPDS client can handle this authentication flow, the
library OPDS server needs to advertise it (using
<opds:indirectAcquisition>
whenever it offers books from this
distributor. Here's what the link to borrow a book from this
distributor might look like:
<entry>
<title>A Great Book</title>
...
<link href="https://my-library.org/borrow/book1"
rel="http://opds-spec.org/acquisition/borrow"
type="application/atom+xml;type=entry;profile=opds-catalog">
<opds:indirectAcquisition type="application/vnd.librarysimplified.bearer-token+json">
<opds:indirectAcquisition type="application/epub+zip">
</opds>
</link>
</entry>
This is saying:
- You can borrow this book by making a request to https://my-library.org/borrow/book1.
- But you're not going to get back an EPUB!
- You're going to get a document of type
application/atom+xml;type=entry;profile=opds-catalog
... - Which will tell you how to get a document of type
application/vnd.librarysimplified.bearer-token+json
... - ...which you can exchange for an EPUB.
The application/vnd.librarysimplified.bearer-token+json
step is the tricky one. If you're an OPDS client, and you can't handle a document of type
application/vnd.librarysimplified.bearer-token+json
, then you shouldn't
give your user the impression they can get this book through you. They
can borrow the book, but you won't be able to go through the steps
necessary to download it.