API V2 Oauth
Bookshare Authentication and Authorization
1. Overview
Bookshare has many resources that are publicly available, from book listings to book content, that don’t require any user authentication. For resources that are secured, such as copyrighted material or user account data, the Bookshare API v2 requires an OAuth 2 token. The process of obtaining a token for a user, and then using that token follow the general steps defined by the OAuth 2 standard. We will go over what the steps look like for Bookshare here to make it more concrete.
In the end, you will have a particular token string that is associated with a particular user, representing their authorization to allow your application to act on their behalf. That token should be presented on each call that is made on their behalf, as a "Bearer" token in an "Authorization" request header.
Using curl, a call to get a list of a user’s reading lists, then, would look like this:
curl -H "Authorization: Bearer <token string>" https://api.bookshare.org/v2/lists?api_key=<your API key>
2. Obtaining an OAuth 2 token
The OAuth 2 spec describes different ways to get a token, each with different characteristics. The Bookshare API supports two of these, which have a similar flow:
-
Your app: Post a request to the Bookshare Auth site, requesting a token for a certain user, for your app.
-
User: Is presented with a Bookshare page to authorize or deny your app to perform certain operations on their behalf (download books, update account information, etc).
-
The Bookshare Auth site may first challenge the user for their username and password if it can’t authenticate them from the initial request.
-
-
Your app: If the user approved the request, the Bookshare Auth site will redirect to a URI that you provided for your app, with a code or token included on the response. The two flows differ at this point, but in the end, your app receives an access token to be used for calls on behalf of that user.
This means a couple of important things:
-
Your app never sees a user’s Bookshare username and password. All you need is the user’s token, which identifies them to Bookshare.
-
Your app needs to be able to be addressed by a URI so that the Bookshare Auth site can deliver the token to you. The mechanics of implementing that are beyond the scope of this documentation, but it could be a custom URI scheme registered with a mobile OS, or an HTTP URL handled by your app or site.
The Bookshare API v1 uses a different, custom authentication scheme. To help smooth the migration to the v2 API, OAuth 2 tokens can be used for access to both v1 and v2 resources. |
3. OAuth 2 Grant Types
The Bookshare API supports the "authorization code" and "implicit" grant types.[1] You are free to use either one, but should be aware of the differences.
-
Authorization code: This requires an extra call from your client to the Bookshare Auth site in order to exchange the initial authorization code that you receive for an access token; it is the access token that carries the user’s authorization information, and gets sent in along with each request. The benefit of this flow is that you also receive a refresh token, which allows you to request a new access token when it expires, without having to involve the user in reauthorizing your app.
-
Implicit: This flow provides you with the access token directly when the Bookshare Auth server contacts you after the user’s authorization. There is no need to exchange it for another token. The downside is that you will not receive a refresh token, so you will have to involve the user to reauthorize your app when the initial token expires.
In brief, the choice of flow has security implications, but may partly be determined by your client. The implicit flow was designed primarily for browser-based clients, where the client may not be able to provide an HTTPS redirect URI. Being within the browser, the access token is vulnerable to exposure, so its lifespan is typically shorter, and no automatic refresh is allowed. The authorization code flow allows for a more secure transmission of the token, since the final token request can be forced to be on a secure channel, so it’s trusted lifespan can be allowed to be longer. You might say that the tradeoff is between simplicity and security: the implicit flow is simpler, while the authorization code flow is more secure.
There are many more discussions of the tradeoffs of these flows on the web, so feel free to research it more for your situation.
4. Sample OAuth 2 authorization code grant flow
Let’s suppose that the application Flubber wants to get an OAuth 2 token for the person using their application, and wants to use the authorization code grant type.
To be able to handle the callback that will notify them that a user has approved them, the Flubber developers have registered a URI scheme of "flubber" with their mobile app platform, and have implemented the ability to recognize calls to the URI "flubber://authorize".
They have also registered their application with Bookshare, and were given an API key of "abcdefg". In the OAuth 2 flows, this will be used for the client_id
parameter whenever it is called for.
They start by offering the app user a choice to "Login with Bookshare", which results in a POST to the Bookshare authorization server with form data. They also choose to send along a state
parameter of "something", which they can compare against the state
value that comes back on the redirect. The POST would look like this:
POST /oauth/authorize HTTP/1.1
Host: auth.bookshare.org
Content-Type: application/x-www-form-urlencoded
response_type=code&client_id=abcdefg&redirect_uri=flubber://authorize&scope=basic&state=something
The result of this will be a redirect to a Bookshare web page, where the user will enter their credentials, and then be asked to approve or deny Flubber’s access to Bookshare on their behalf. The Flubber app isn’t involved in any of this interaction, so it is irrelevant to them how it happens.
Once the user has authenticated themselves and approved access, the Bookshare service will send a redirect to the Flubber user-agent, giving it the redirect URI that was passed in, and attaching the authorization code.
flubber://authorize?code=4hMt0A&state=something
That authorization code can then be exchanged for an access token by calling the API again.
POST /oauth/token HTTP/1.1
Host: auth.bookshare.org
Authorization: Basic abcdefghijklmnop (1)
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code&code=4hMt0A&redirect_uri=flubber://authorize&scope=basic (2)
1 | The basic auth credentials are the client ID and an empty password. |
2 | The original redirect URI is needed as well, to ensure that the request is coming from the same client who requested the code. |
This will send a JSON response that will include the access token, expiration time in seconds, and a refresh token.
HTTP/1.1 200 OK
Content-Type: application/json;charset=UTF-8
Content-Length: 169
{
"access_token":"1234567",
"token_type":"bearer",
"refresh_token":"9876543",
"expires_in":1209599,
"scope":"basic"
}
The Flubber app should store this token value and use it in subsequent calls for this user. In curl
, a call to get the user’s reading lists would look like this:
curl -H "Authorization: Bearer 1234567" https://api.bookshare.org/v2/mylists?api_key=abcdefg
Finally, if the user instead had denied the access request, the Flubber app would still get a response at the redirect URI, but with an error code and reason in it.
flubber://authorize?error=access_denied&error_description=User denied access
Regardless of the reason, without an access token, the Flubber app would be limited to making requests for unsecured resources.
5. Sample OAuth 2 implicit grant flow
The implicit grant flow differs from the authorization code flow by returning the access token directly from the first request, rather than having the client make a second call to exchange an authorization code for an access token. At an API level, the difference is that the client specifies a different response_type
parameter:
POST /oauth/authorize HTTP/1.1
Host: auth.bookshare.org
Content-Type: application/x-www-form-urlencoded
response_type=token&client_id=abcdefg&redirect_uri=flubber://authorize&scope=basic&state=something
The response, then, has the access token as a hash fragment on the callback URI:
flubber://authorize#access_token=1234567&token_type=bearer&expires_in=2592000
Note that these are not request parameters, but are part of a URI fragment. The values will have to be parsed from the URI string by the client, typically Javascript.
6. Token lifespan
A token will be valid until it either expires, or is revoked by the user. The expiration time is given, in seconds, in the callback URI or token response, but you will also be told when it expires by an error response if an outdated token is used.
{"error":"unauthorized","error_description":"Access token expired: 1234567"}
If you have a refresh token, you can simply make a call without involving the user, and receive a new access token.
POST /oauth/token HTTP/1.1
Host: auth.bookshare.org
Authorization: Basic abcdefghijklmnop
Content-Type: application/x-www-form-urlencoded
grant_type=refresh_token&refresh_token=9876544&scope=basic
7. Resources
7.1. OAuth 2
There are only two endpoints needed to use OAuth 2: requesting an authorization (a Bookshare user’s approval to act on their behalf), and request an access token (a representation of that approval). Which one, or ones, that you use will depend on the grant type flow.
7.1.1. Request client authorization for a user
POST /oauth/authorize
Description
The Bookshare API supports the implicit and authorization code grant types, where the end user is asked to authorize access after they have identified themselves correctly to Bookshare. The client ends up with an access token without needing to know the end users Bookshare credentials. See the OAuth 2 spec for more information.
Parameters
Type | Name | Description | Schema |
---|---|---|---|
FormData |
response_type |
OAuth 2 grant type, should be 'code' for authorization code flow, or 'token' for implicit flow |
enum (code, token) |
FormData |
client_id |
Your client identifier |
string |
FormData |
redirect_uri |
URI for redirect to your client after the user has been authorized |
string |
FormData |
scope |
The requested scope of the access request. Should be 'basic' |
enum (basic) |
FormData |
state |
An opaque value that will be passed back with the redirect URI to allow the client to ensure the integrity of the response |
string |
Responses
HTTP Code | Description | Schema |
---|---|---|
200 |
OAuth 2 token response |
|
default |
unexpected error |
Consumes
-
application/x-www-form-urlencoded
Example HTTP request
See the Sample OAuth 2 authorization code grant flow and Sample OAuth 2 implicit grant flow for examples of using this endpoint.
7.1.2. Request an OAuth 2 access token
POST /oauth/token
Description
This endpoint allows you to exchange either an authorization code or a refresh token for an access token.
Parameters
Type | Name | Description | Schema | Default |
---|---|---|---|---|
FormData |
grant_type |
OAuth 2 grant type, should be 'authorization_code', 'refresh_token'. The 'password' type is limited to trusted clients only. |
enum (authorization_code, refresh_token, password) |
|
FormData |
code |
Authorization code from authorization request (only for 'authorization_code' type) |
string |
|
FormData |
refresh_token |
Refresh token from original token request (only for 'refresh_token' type) |
string |
|
FormData |
username |
Bookshare username (only for 'password' type) |
string |
|
FormData |
password |
Bookshare password (only for 'password' type) |
string |
|
FormData |
scope |
The requested scope of the authorization (optional) |
enum (basic) |
|
Responses
HTTP Code | Description | Schema |
---|---|---|
200 |
OAuth 2 token response |
|
default |
unexpected error |
Consumes
-
application/x-www-form-urlencoded
Security
Type | Name |
---|---|
basic |
Example HTTP request
POST /oauth/token HTTP/1.1
Host: auth.bookshare.org
Authorization: Basic abcdefghijklmnop
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code&code=12345&scope=basic
Example HTTP response
HTTP/1.1 200 OK
Content-Type: application/json;charset=UTF-8
Content-Length: 169
{
"access_token":"ad13cb0b-1f5d-44e3-95e4-a072c5200c4f",
"token_type":"bearer",
"refresh_token":"3303fab2-2823-4560-a710-eb1f11e544e7",
"expires_in":1209599,
"scope":"basic"
}
Example Curl request
$ curl -u "abcdefg:" -d "grant_type=authorization_code&code=12345&scope=basic" https://auth.bookshare.org/oauth/token
8. Definitions
8.1. token
Name | Description | Schema |
---|---|---|
access_token |
An OAuth 2 bearer token |
string |
expires_in |
Number of seconds that this token will be valid |
string |
refresh_token |
Only for the 'code' grant type, this token can be redeemed for another access token without requiring the user to reauthorize the application. |
string |
scope |
A name representing the set of capabilities for which this token is authorized |
string |
token_type |
Will always be 'bearer' |
string |
8.2. token_error
Name | Description | Schema |
---|---|---|
error |
One of a set of standard error names |
enum (invalid_request, unauthorized_client, access_denied, unsupported_response_type, invalid_scope, server_error, temporarily_unavailable) |
error_description |
A human readable description of the error condition |
string |
9. Security
9.1. basic_auth
Use your client identifier (API key) as the username, with no password.
Type : basic
Appendix A: OAuth2 Password Grant Type
For trusted clients, the Bookshare API supports the OAuth2 Password Grant Type, offering a simplified way of getting authorization tokens for users. We limit its use because it is a flow that allows the client to see a user’s password before it is transmitted to Bookshare. Before we allow a client to use the password grant type, we ask the following:
-
To demonstrate that their system does not store the plaintext password, or forward to any third party.
-
To describe how the authorization code or implicit grant types are not possible with their technology.
Our goal is to provide as strong of assurances as is reasonable to the Bookshare users that their credentials are being managed responsibly. The password grant type requires a significant amount of trust between Bookshare and a client, so the stronger the demonstration of your credentials management, the more likely we will be to consider allowing this grant type.
A.1. Password Grant Flow
The password grant type is similar to the HTTP Basic Auth flow, in that the client collects and transmits a user’s credentials, and gets an authorization answer directly. In this case, the client gets the OAuth2 token back after this first call, without the redirection required in the authorization code or implicit grant types.
For example, a client will collect the username and password of a user through a form or some other means, and then call the token endpoint. Along with this information, the client should pass a HTTP Basic Auth header, with the API key as the username and a blank string as the password. The way this data is formed and presented will vary by programming language and tool, but an example with curl
would look like this:
curl -u "abcdefg:" -d "grant_type=password&username=john.smith@somewhere.org&password=mysecret" https://auth.bookshare.org/oauth/token
The HTTP request itself will look something like this:
POST /oauth/token HTTP/1.1
Host: auth.bookshare.org
Authorization: Basic eXR2czlwenNkNjJidjdyemFtd2RrdGhlOg==
Content-Type: application/x-www-form-urlencoded
grant_type=password&username=john.smith@somewhere.org&password=abcdefg
The Bookshare server will authenticate the user, and if their credentials match, will return a token. This will be the same kind of access token you would get from the other grant types, with an expiration interval and a refresh token.
HTTP/1.1 200 OK
Content-Type: application/json;charset=UTF-8
Content-Length: 169
{
"access_token":"1234567",
"token_type":"bearer",
"refresh_token":"9876543",
"expires_in":1209599,
"scope":"basic"
}
If the credentials don’t match what the server knows about the user, you will get a 400 status code and an error message.
HTTP/1.1 400 Bad Request
Content-Type: application/json;charset=UTF-8
{
"error":"invalid_grant",
"error_description":"Bad credentials"
}
Once you have an access token, you can store it for this user, and pass it along with each subsequent request to identify the user to the system. In curl
, a call to get the user’s reading lists, for example, would look like this:
curl -H "Authorization: Bearer 1234567" https://api.bookshare.org/v2/mylists?api_key=abcdefg