commit b2b70ba0c929df432379d23022880e8f001a579f Author: Shadowfacts Date: Wed Mar 27 10:18:52 2019 -0400 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e43b0f9 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.DS_Store diff --git a/README.md b/README.md new file mode 100644 index 0000000..1bc695a --- /dev/null +++ b/README.md @@ -0,0 +1,34 @@ +# Fervor API +Fervor is a open standard for a self-hosted RSS aggregator API. It's primary goal is to replace the current de-facto standard, the [Fever API](https://feedafever.com/api). + +The Fever API has a number of issues, including: + +1. Using MD5 for authentication. +2. Poor design (e.g. `group` and `feeds_group` being separate entities.) +3. Poor documentation (the difference between `group` and `feeds_group` is never articulated.) +4. No longer being maintained. + +The Fervor API is intended to be a common foundation for self-hosted RSS aggregators, not a catch-all solution. Different services will have their own specific needs from an API, so Fervor is designed to be only the component that is common to all of RSS aggregator implementations. + +## Specification +The specification is divided into the following sections: + +- [Authentication](https://github.com/shadowfacts/fervor/blob/master/authentication.md) +- [Endpoints](https://github.com/shadowfacts/fervor/blob/master/endpoints.md) +- [Entities](https://github.com/shadowfacts/fervor/blob/master/entities.md) +- [Extensions](https://github.com/shadowfacts/fervor/blob/master/extensions.md) +- [Pagination](https://github.com/shadowfacts/fervor/blob/master/pagination.md) + +## Versioning +The Fervor API uses [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +Minor fixes increment the patch number, minor additions increment the minor number, and any removals increment the major number. + +The current Fervor version is 0.1.0. The major version 0 will remain until the specification is finished and no longer a draft. + +## Known Implementations +### Clients +- + +### Servers +- \ No newline at end of file diff --git a/authentication.md b/authentication.md new file mode 100644 index 0000000..ed29c06 --- /dev/null +++ b/authentication.md @@ -0,0 +1,242 @@ +# Authentication +Fervor uses [OAuth 2](https://oauth.net/2/) for authentication so that clients don't need to store user credentials. + +Basic steps for using OAuth 2 with Fervor: + +1. Get the instance domain from the user. +2. Register your client with the Fervor API on that domain ([Client Registration](#client-registration)) to get the client ID and secret. +3. Navigate to the authorize endpoint to allow the user to log in ([Authorization Code Request](#authorization-code-request)) to generate the authorization code. +4. Request the access token from the server ([Access Token Request](#access-token-request)). + +See the [Example](#example) below for a concrete example of the Fervor authentication flow. + +**Notes**: + +Servers may implement token expiration and refreshing, so clients should be capable of handling either possibility. See [Refreshing Access Tokens](#refreshing-access-tokens) for details. + +Servers may implement password authentication for obtaining an access token, however, this should **only** be used for testing/in development. See [Password Authentication](#password-authentication) for details. + +## Client Registration +To register your client with the server, make a POST request to `/api/v1/register`. + +#### Parameters +Parameters should be sent as `application/x-www-form-urlencoded` in the POST body. + +| Key | Description | Required | +| -------------- | ---------------------------------------------------------------------- | -------- | +| `client_name` | String. The name of your client. May be presented to the user. | Yes | +| `website` | URL. The URL of your client or homepage. May be presented to the user. | No | +| `redirect_uri` | URI. The URI the redirected to after a successful login. | Yes | + +The redirect URI must not include a fragment, and may include query parameters. + +#### Response +An object with the client ID and secret to be used when retrieving an authorization code. + +| Key | Description | Required | +| --------------- | -------------------------- | -------- | +| `client_id` | String. The client ID. | Yes | +| `client_secret` | String. The client secret. | Yes | + +## Authorization Code Request +Now that you have a client ID and secret, the user needs to log in to the server to allow the client to access their account and a authorization code to be generated. + +Navigate to `/oauth/authorize` in a web browser (either a browser embedded in a native application, or the user's web browser itself). The following query parameters should be included: + +| Key | Description | Required | +| --------------- | ------------------------------------------------------------------------------------------------------------------- | -------- | +| `response_type` | String. Must be `code`. | Yes | +| `client_id` | String. The client ID received from the previous step. | Yes | +| `redirect_uri` | URI. The URI the user will be redirected to after a successful login. **Must** be the same as in the previous step. | Yes | +| `state` | Any. Session state/ID may be included here. If provided, the redirect will include the same query parameter value. | No | + +The user will then be prompted to log in and approve your client. After this has happened, the user will be redirected (using a `302 Found` response with a `Location` header) back to the redirect URI you specified with the additional query parameter `code` containing the authorization code. If a `state` parameter was given to the request, the same value will be provided in the redirect's `state` query parameter. + +## Access Token Request +### `POST /oauth/token` +Uses the client ID and secret to get an authorization code and + +#### Parameters +Parameters should be sent as `application/x-www-form-urlencoded` in the POST body. + +| Key | Description | Required | +| -------------------- | ------------------------------------------------------------------------------- | -------- | +| `grant_type` | String. The grant type being used. See below for supported options. | Yes | +| `redirect_uri` | URI. **Must** be the same as in the previous steps. Only used for verification. | Yes | +| `client_id` | String. The client ID received from the Client Registration step. | Yes | +| `client_secret` | String. The client secret received from the Client Registration step. | Yes | + +##### Supported Grant Types +1. `authorization_code`: Used to obtain an access token from an authorization code. The following parameters should also be included: + - `authorization_code`: String. The authorization code received from the [Authorization Code Request](#authorization-code-request) step. +2. `refresh_token`: Used to refresh an existing access token that has expired. The following parameters should also be included: + - `refresh_token`: String. The refresh token that was received from the previous Access Token response. +3. `password`: Used to obtain an access token from a users credentials. **Password grants should _never_ be used in production; they are available for development/testing purposes only.** The following parameters should also be included. + - `username`: String. The user's username. + - `password`: String. The user's password. + +#### Successful Response +An object with information about the result of the access token request. + +**Note for server implementers:** This response must include the `Cache-Control: no-store` and `Pragma: no-cache` headers to ensure that the response is not cached by the client. + +| Key | Description | Required | +| --------------- | ------------------------------------------------------------------------------------------------------------------------------- | ----------- | +| `access_token` | String. The access token issued by the server. | Yes | +| `token_type` | String. The type of the issued token. Must be `bearer`. | Yes | +| `expires_in` | Integer. The number of seconds the access token is valid for. If specified, `refresh_token` must also also be. | Recommended | +| `refresh_token` | String. The token that may be used to obtain a new access token when this one expires. If specified, `expires_in` must also be. | Recommended | + +#### Unsuccessful Responses +Returned if the request is unsuccessful. See below for HTTP response codes and error types. + +| Key | Description | Required | +| ------------------- | ------------------------------------------------------------- | -------- | +| `error` | String. The type of the error. See below for possible values. | Yes | +| `error_description` | String. A more detailed description of the error. | Yes | + +##### 400 `invalid_request` +The request is missing parameters, has invalid values, or otherwise can't be processed. + +##### 401 `invalid_client` +Authentication failed to an invalid client ID. + +##### 400 `invalid_grant` +The given authorization code is invalid/expired or the `redirect_uri` did not match the originally specified. + +##### 400 `unsupported_grant_type` +The `grant_type` used is not supported. + +## Access Token Usage +All Fervor access tokens are Bearer tokens. To use them, include the `Authorization: Bearer ` header on all your requests (where `` is your access token). + +## Refreshing Access Tokens +Implementing access token expiration is optional, so clients must be able to handle either possibility. + +If expiration/refreshing is implemented, the Access Token Response (see above) must include the `expires_in` and `refresh_token` parameters. After an access token expires, the refresh token will be used to obtain a new one. + +To obtain a new token, the [Access Token Request](#access-token-request) should be performed with the `refresh_token` grant type and parameter. A new access token will be received, and the new refresh token should be stored for future use. + +## Password Authentication +Using password authentication is vastly less secure and defeats the purpose of OAuth. As such, it should **never** be used in production. Clients are **not** required to implement, however, they may do so for testing and development purposes. + +To obtain an access token from user credentials, the client must first be registered as usual (see [Client Registration](#client-registration)) and then the [Access Token Request](#access-token-request) may be made with the `password` grant type and the `username`/`password` parameters. + +## Example +This is an example of the complete authentication flow from beginning to end. + +### Step 1: Get the server domain from the user +For this example, we'll use `fervor.example.com` + +### Step 2: Register your client with the server. +For this example, we'll pretend we're building a native application which is already registered to handle the `fervorclient://` URL scheme. If you're building a web application, you'd use a normal URL for your application. + +We'll send the following request: + +``` +POST /api/v1/register +Host: fervor.example.com +Content-Type: application/x-www-form-urlencoded + +client_name=Example%20Client&redirect_uri=fervorclient://oauth +``` + +and get back the following response: + +``` +HTTP/1.1 200 OK +Content-Type: application/json + +{ + "client_id": "rH58aObIri1OTCSw2q0L", + "client_secret": "5LfDsOyrDSmq6f4EHe3v" +} +``` + +### Step 3: Prompt the User to Log In +Next, we'll need to allow the user to log in to their account so that we can receive an authorization code. + +To do this, we'll open the following URL in a web view inside our imaginary app: + +``` +https://fervor.example.com/oauth/authorize?response_type=code&client_id=rH58aObIri1OTCSw2q0L&redirect_uri=fervorclient://oauth +``` + +Once the user logs in and agrees to allow our app to interact with their account, the web view will be redirected to the following URL containing our authorization code. + +``` +fervorclient://oauth?code=ypqBbDdsOXUeYJFnlbT0 +``` + +We can detect when we're redirected to `fervorclient://oauth` and close our web view, storing the authorization code from the `code` query parameter. + +### Step 4: Exchange the Authorization Code for an Access Token +Now that we've got an authorization code, we can obtain an access token that can be used to interact with the Fervor API. + +We'll make another request, this time to the token endpoint: + +``` +POST /oauth/token +Host: fervor.example.com +Content-Type: application/x-www-form-urlencoded + +grant_type=authorization_code&redirect_uri=fervorclient://oauth&client_id=rH58aObIri1OTCSw2q0L&client_secret=5LfDsOyrDSmq6f4EHe3v&authorization_code=ypqBbDdsOXUeYJFnlbT0 +``` + +and we'll get back a response with an access token we can use: + +``` +HTTP/1.1 200 OK +Content-Type: application/json +Cache-Control: no-store +Pragma: no-cache + +{ + "access_token": "Bsltr6EAUIiAKCtw3ieg", + "token_type": "bearer", + "expires_in": 3600, + "refresh_token": "Tr6xsiZN3dFKygaZQNlb" +} +``` + +We'll store the access token for use with API calls, and the refresh token for use in an hour when the current access token expires. + +### Step 5: Making API Calls +We can now use our access token to make API calls: + +``` +GET /api/v1/instance +Host: fervor.example.com +Authorization: Bearer Bsltr6EAUIiAKCtw3ieg +``` + +and we'll get back an Instance object. + +### Step 6: Refreshing the Token +After an hour has passed, our access token will have expired, so we'll need to generate a new one using the refresh token we received. + +We'll once again make a request to the token endpoint, this time with the `refresh_token` grant type: + +``` +POST /oauth/token +Host: fervor.example.com +Content-Type: application/x-www-form-urlencoded + +grant_type=refresh_token&redirect_uri=fervorclient://oauth&client_id=rH58aObIri1OTCSw2q0L&client_secret=5LfDsOyrDSmq6f4EHe3v&refresh_token=Tr6xsiZN3dFKygaZQNlb +``` + +and we'll get back a new access token response: + +``` +HTTP/1.1 200 OK +Content-Type: application/json +Cache-Control: no-store +Pragma: no-cache + +{ + "access_token": "PbBBXlPNONhxhdkG6gUu", + "token_type": "bearer", + "expires_in": 3600, + "refresh_token": "DyEC8hLOazgwLS7cUbBb" +} +``` diff --git a/endpoints.md b/endpoints.md new file mode 100644 index 0000000..60d9279 --- /dev/null +++ b/endpoints.md @@ -0,0 +1,56 @@ +# API Endpoints + +## Server Instance +### `GET /api/v1/instance` +Returns an Instance object containing information about this instance and this Fervor implementation. + +**This endpoint does not require authentication.** It should be used by clients prior to completing the login/authentication flow to ensure that the server implementation is compatible with the client. + +## Groups +### `GET /api/v1/groups/` +Returns an array of all available Group objects. + +### `GET /api/v1/groups/:id` +Returns the Group object for the given ID. + +### `GET /api/v1/groups/:id/feeds` +Returns an array of all Feed objects belonging to the group with the given ID. + +Equivalent to retrieving the Group object and then looking up all the feeds specified by its `feed_ids`. + +### `GET /api/v1/groups/:id/items` +[Paginated](./pagination.md). + +Returns an array of the most recent Items (read or unread) from all the feeds in this group. + +#### Parameters +| Key | Description | Required | +| ------ | ------------------------------------------------------------------------------------------------------------ | -------- | +| `only` | String. One of `read` or `unread`. Only the given type of items will be returned, or both types, if omitted. | No | + +## Feeds +### `GET /api/v1/feeds/` +Returns an array of all available Feed objects. + +### `GET /api/v1/feeds/:id` +Returns the Feed object for the given ID. + +### `GET /api/v1/feeds/:id/items` +[Paginated](./pagination.md). + +Returns an array of the most recent Items (read or unread) from this feed. + +#### Parameters +| Key | Description | Required | +| ------ | ------------------------------------------------------------------------------------------------------------ | -------- | +| `only` | String. One of `read` or `unread`. Only the given type of items will be returned, or both types, if omitted. | No | + +## Items +### `GET /api/v1/items/:id` +Returns the Item object for the given ID. + +### `POST /api/v1/items/:id/read` +Marks the Item with the given ID as read. + +### `POST /api/v1/items/:id/unread` +Marks the Item with the given ID as unread. \ No newline at end of file diff --git a/entities.md b/entities.md new file mode 100644 index 0000000..55f1462 --- /dev/null +++ b/entities.md @@ -0,0 +1,68 @@ +# Entities +All entities are UTF-8 encoded JSON objects. + +## Notes +Object IDs are positive integers. + +Date and DateTime fields are encoded as [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) strings. + +## Instance +An Instance object provides information about the instance intself and the specific implementation of Fervor that is being used. + +| Key | Description | Required | +| ------------------------ | --------------------------------------------------------- | -------- | +| `name` | String. The user-friendly name of this instance. | Yes | +| `url` | URL. The user-friendly URL of this instance. | Yes | +| `version` | String. The Fervor version this software uses. | Yes | +| `implementation_name` | String. The name of the softwre this instance is running. | Yes | +| `implementation_version` | String. The version of software this instance is running. | Yes | +| `extensions` | [Extensions object](#extensions). | No | + +### Extensions +The extensions object provides additional information about extension to the Fervor API this instance provides. + +There should be a field in the object for each supported extension. The key should be a unique identifier for the extension (reverse domain name style is recommended) and the value should be a URL for the extension specification or API documentation. + +See the [Extensions](./extensions.md) page for more information. + +## Feed +An RSS feed. + +| Key | Description | Required | +| -------------- | ------------------------------------------------------------- | -------- | +| `id` | Positive Integer. The ID of this feed. | Yes | +| `title` | String. The title/name of this feed. | Yes | +| `url` | URL. The URL of the website this feed belongs to. | Yes | +| `service_url` | URL. The URL of this feed on the aggregation service. | No | +| `feed_url` | URL. The URL of the RSS document itself. | Yes | +| `last_updated` | DateTime. The last time this feed was updated by the service. | Yes | +| `group_ids` | Array of IDs. The IDs of the groups that contain this feed. | Yes | + + +## Item +An item is an individual item from an RSS feed. + +| Key | Description | Required | +| ------------- | ------------------------------------------------------------------ | -------- | +| `id` | ID. The ID of this item. | Yes | +| `feed_id` | ID. The ID of the feed to which this item belongs. | Yes | +| `title` | String. The title of this item. | Yes | +| `author` | String. The author of this item. | No | +| `published` | DateTime. The date this item was published. | No | +| `created_at` | DateTime. The date this item was added to the aggregation service. | No | +| `content` | HTML String. The content of this item. | No | +| `summary` | HTML String. The summar of this item. | No | +| `url` | URL. The original URL of this item. | Yes | +| `service_url` | URL. The URL of this item on the aggregation service. | No | +| `read` | Boolean. Whether this item has been marked as read.. | No | + +## Group +A group contains multiple feeds. + +Feeds may or may not belong to multiple groups, depending on the service implementation. + +| Key | Description | Required | +| -------------- | ------------------------------------------------------------- | -------- | +| `id` | ID. The ID of this group. | Yes | +| `title` | String. The title/name of this group. | Yes | +| `feed_ids` | Array of IDs. The IDs of the feeds that this group contains. | Yes | \ No newline at end of file diff --git a/extensions.md b/extensions.md new file mode 100644 index 0000000..ce2155b --- /dev/null +++ b/extensions.md @@ -0,0 +1,35 @@ +# Extensions +Since the Fervor API is designed to be a common foundation, it doesn't include all of the features that implementors might want. As such, it's designed to be extensible. + +When extending the API, several factors should be taken into account to prevent naming conflicts. + +## Extension Identifiers +Each extension should have its own unique identifier. It's recommended that these be reverse domain names (e.g. `com.example.fervor.tags`). + +## Announcing Extensions +Implementations make their extensions known by including them in the [Extensions](./entities.md#extensions) sub-object in the [Instance](./entities.md#instance) object. + +For each extension provided, there should be a field in the Extensions object with their identifier as the key and the URL of their specification/documentation as the value. + +## Extended Entities +Extensions may include additional data in any of the [Entities](./entities.md). To avoid name conflicts, all extension data should be contained in a sub-object of the entity whose key is the extension identifier. + +For example, an extended item object: + +```json +{ + "id": 1, + "feed_id": 1, + "title": "Example", + "url": "https://example.com", + "com.example.fervor.tags": { + "favorite": true, + "tags": [1, 3] + } +} +``` + +## Extended Endpoints +Extensions may also provide additional API endpoints that provide access to their specific data. To avoid name conflicts, all extension endpoints should be under `/api//`. + +For example, the `com.example.fervor.tags` extension may provide a GET endpoint at `/api/com.example.fervor.tags/v1/tags` that returns an array of all tag objects. \ No newline at end of file diff --git a/pagination.md b/pagination.md new file mode 100644 index 0000000..c3b8d32 --- /dev/null +++ b/pagination.md @@ -0,0 +1,45 @@ +# Pagination +Some API endpoints which are resource-intensive to handle are paginated. The following options are supported for paginated endpoints. + +The `limit` query parameter may also be used with any of the following options (except Specific IDs) to specify the maximum number of resources to return. Default is 20. + +## Max ID +The `max_id` query parameter may be used to request resources immediately older than the given ID. + +## Min ID +The `min_id` query parameter may be used to request resources immediately newer than the given ID. + +## Since ID +The `since_id` query parameter may be used to request resources newer than the given ID. + +**Note:** The `since_id` parameter does not return resources _immediately_ newer, so using this may result in a gap. See the examples below for a comparison with the `min_id` parameter. + +## Specific IDs +Clients may request paginated resources with specific IDs. + +The `ids` query parameter must be a comma-delimited string containing the numerical IDs for all the desired resources. + +Server implementations may limit this endpoint to a maximum number of resources that may be requested at once. As such, clients should check that they received objects for all the IDs requested and perform another request if they haven't. + +## Examples +Given resources with IDs 1 through 50, all of which are accessible by the requesting user, these options will have the following results: + +### `max_id=20` +Resources with IDs 21 through 40 will be returned. + +### `max_id=50` +No resources will be returned. + +### `min_id=30` +Resources with IDs 10 through 29 will be returned. + +### `min_id=1` +No resources wil be returned. + +### `since_id=30` +Resources with IDs 1 through 19 will be returned. + +Note that with this response, there is a gap between the ID requested in the parameter and the maximum ID returned by the server. Compare this with the result of the `min_id=30` request above. + +### `since_id=1` +No resources will be returned. \ No newline at end of file