Description
For various applications it is desirable to be able to read or write user data. This proposal describes a format for tokens, a method for authenticating requests, and endpoints for authorizing tokens.
Token format
Each token is a JSON object with the following schema:
{
"session": String,
"expire": Int64?,
"scopes": Array(String)
"signature": String
}
Tokens will be created with a given session
, which allows them to be revoked by the user.
Tokens may be issued with an expire
timestamp after which the token will be considered invalid.
Tokens will have a list of one or more scopes
determining their permissions. The format for scopes
is described below.
Tokens will have a signature
to ensure integrity of the other fields.
Example token:
{
"session": "v1:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
"expires": 1554680038,
"scopes": [
":notifications",
":subscriptions/*",
"GET:tokens*",
],
"signature": "f//2hS20th8pALF305PJFK+D2aVtvefNnQheILHD2vU="
}
Authentication
All sub-routes of /api/v1/auth
must be authenticated.
Endpoints requiring authentication are authenticated with an Authentication: Bearer <token>
header.
Tokens are validated by taking all key-value pairs, minus signature
, and creating the following string:
key1=value1
key2=value1,value2,value3
Values are alpha-sorted. Keys are alpha-sorted and joined by newlines. There is no trailing newline.
The signature
is created by signing the string using SHA256-HMAC with the instance's HMAC_KEY
and Base64 encoded.
Using the above token as an example:
expires=1554680038
scopes=GET:tokens*,:notifications,:subscriptions/*
session=v1:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
Signed using SECRET_KEY
provides the signature f//2hS20th8pALF305PJFK+D2aVtvefNnQheILHD2vU=
.
Scoping
In order to provide a means of limiting permissions for an application, tokens must provide one or more scopes
.
Each scope
refers to a sub-route of /api/v1/auth
. Scopes are defined as zero or more HTTP methods (semicolon-separated), colon (:), followed by an endpoint and optional wildcard for matching sub-routes.
A scope X is said to be containing
scope Y if X can match Y, but Y cannot match X. For example, :subscriptions*
contains GET:subscriptions/subscribe
.
Example scope allowing access to /api/v1/auth/subscriptions
using any method:
{ "scopes": [":subscriptions"] }
Example scope allowing access to /api/v1/auth/subscriptions
and any sub-routes using any method:
{ "scopes": [":subscriptions*"] }
Example scope allowing access to any sub-routes of /api/v1/auth/subscriptions/
using only GET and POST:
{ "scopes": ["GET;POST:subscriptions/*"] }
Example scope allowing access to any endpoint using any method:
{ "scopes": [":*"] }
A SID cookie is equivalent to a token with "scopes": [":*"]
.
POST /api/v1/auth/tokens/register
The /api/v1/auth/tokens/register
endpoint would support the following body (Content-Type: application/json
):
{
"scopes": Array(String), // List of scopes (comma-separated)
"callbackUrl" : String?, // URL for redirecting after successful authorization, optional
"expire": Int64? // Int64, optional
}
Calls to /api/v1/auth/tokens/register
must already be authenticated in order to receive a new token. This can be accomplished by a signed in user or with a Bearer
token with a scope containing POST:tokens/register
.
Calls made using a Bearer
token can only create tokens with equivalent or subsets of their own scopes.
If calls are made using a SID cookie, the user will be presented with a page presenting the requested scopes, and must then authorize the request before being redirected to callbackUrl
. If no callbackUrl
is provided, then after authorizing the token the user will be redirected to a page on the instance containing the newly created token.
If a callbackUrl
is provided, then after successful authorization the instance will redirect to callbackUrl
with ?access_token=<token>
set as the query.
POST /api/v1/auth/tokens/unregister
The /api/v1/auth/tokens/unregister
endpoint would support the following body:
{
"session": String?
}
Tokens can be unregistered by calling /api/v1/auth/tokens/unregister
. A token must have a scope containing POST:tokens/unregister
in order to unregister itself. In order to unregister other tokens a token must have a scope containing GET:tokens
and POST:tokens/unregister
.
If the response body is empty, or session
is not provided the token is assumed to be unregistering itself.
Users would also be provided a /list_tokens
endpoint for viewing and degistering tokens they had authorized.
Example endpoints
All endpoints below are sub-routes of /api/v1/auth
.
- GET/POST
/preferences
- Would allow GET of current preferences, or POST with updated preferences - GET
/notifications
- Same as/api/v1/notifications
with support for?since=
, see [RFC] Push Notifications API #469 - GET
/subscriptions
- Shows list of currently subscribed channels by the user- POST
/subscriptions/:ucid
- Subscribe to givenucid
- DELETE
/subscriptions/:ucid
- Unsubscribe to givenucid
- POST
- GET
/tokens
- Lists tokens authorized by user- POST
/tokens/register
- POST
/tokens/unregister
- POST
Example of use
For an application with a shared pool of users, such as FreeTube or CloudTube, an example token could be:
{
"session": "v1:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
"scopes": [":notifications", "POST:subscriptions/*"],
"signature": "fNvXoT0MRAL9eE6lTE33CEg8HitYJDOL9a22rSN2Ihg="
}
This would allow clients to use ?since=TIMESTAMP
to receive any notifications missed when offline, and subscribe
to new channels. DELETE:subscriptions/*
is not included here to prevent unsubscribing to channels that still may need to be tracked by other clients.