JSON Web Tokens for Sanic applications. This project was originally inspired by Flask JWT and Django Rest Framework JWT, but some departing decisions have been made.
- Getting Started
- Authenticate
- Other initialization parameters
- Endpoints
- Protecting routes
- Scopes
- Settings
Install from pypi using:
pip install sanic-jwt
In order to add Sanic JWT, all you need to do is initialize it by passing the sanic_jwt.initialize
method the Sanic()
instance, and an authentication function.
from sanic_jwt import initialize
async def authenticate(request):
return dict(user_id='some_id')
app = Sanic()
initialize(app, authenticate)
Because Sanic (and this package) are agnostic towards whatever user management system you use, you need to tell Sanic JWT how it should authenticate a user.
You MUST define this method. It should take a request
argument, and return a subscriptable object with user_id
key or or object with user_id
attribute (can be customized by SANIC_JWT_USER_ID
, see Settings for details).
async def authenticate(request):
return dict(user_id='some_id')
A very basic user management system might be as follows, with its corresponding authenticate
method:
from sanic_jwt import exceptions
class User(object):
def __init__(self, id, username, password):
self.user_id = id
self.username = username
self.password = password
def __str__(self):
return "User(id='%s')" % self.id
users = [
User(1, 'user1', 'abcxyz'),
User(2, 'user2', 'abcxyz'),
]
username_table = {u.username: u for u in users}
userid_table = {u.user_id: u for u in users}
async def authenticate(request, *args, **kwargs):
username = request.json.get('username', None)
password = request.json.get('password', None)
if not username or not password:
raise exceptions.AuthenticationFailed("Missing username or password.")
user = username_table.get(username, None)
if user is None:
raise exceptions.AuthenticationFailed("User not found.")
if password != user.password:
raise exceptions.AuthenticationFailed("Password is incorrect.")
return user
Default: None
Purpose: If you would like to add additional views to the authentication module, you can add them here. They must be class based views. Side note, your CBV will probably also need to handle preflight requests, so do not forget to add an options
response.
Example: The below example could be used in creating a "magic" passwordless login authentication.
class MagicLoginHandler(HTTPMethodView):
async def options(self, request):
return response.text('', status=204)
async def post(self, request):
# create a magic login token and email it to the user
response = {
'magic-token': token
}
return json(response)
initialize(
app,
authenticate=lambda: True,
class_views=[
('/magic-login', MagicLoginHandler) # The path will be relative to the url prefix (which defaults to /auth)
]
)
Default: None
Purpose: Required if SANIC_JWT_REFRESH_TOKEN_ENABLED
is set to True
in the config. It is a handler to store a refresh token. If you do not set it up, and you have enabled refresh tokens, then the application will raise a RefreshTokenNotImplemented
exception.
Example:
def store_refresh_token(user_id, refresh_token, *args, **kwargs):
key = 'refresh_token_{user_id}'.format(user_id=user_id)
async def store(key):
await aredis.set(key, refresh_token)
app.add_task(store(key))
initialize(
app,
authenticate=lambda: True,
store_refresh_token=store_refresh_token
)
Default: None
Purpose: Required if SANIC_JWT_REFRESH_TOKEN_ENABLED
is set to True
in the config. It is a handler to retrieve a refresh token. If you do not set it up, and you have enabled refresh tokens, then the application will raise a RefreshTokenNotImplemented
exception.
Example:
def retrieve_refresh_token(user_id, *args, **kwargs):
key = 'refresh_token_{user_id}'.format(user_id=user_id)
async def retrieve(key):
return await aredis.get(key)
app.add_task(retrieve(key))
initialize(
app,
authenticate=lambda: True,
retrieve_refresh_token=retrieve_refresh_token
)
Default: None
Purpose: Given a request
and a payload
, this is a handler to retrieve a user object from your application to be used, for example in the /me
endpoint. It should return either a dict
or an instance of an object that either has a to_dict
method, or __dict__
method.
Example:
class User(object):
...
def to_dict(self):
properties = ['user_id', 'username', 'email', 'verified']
return {prop: getattr(self, prop, None) for prop in properties}
def retrieve_user(request, payload, *args, **kwargs):
if payload:
user_id = payload.get('user_id', None)
user = User.get(user_id=user_id)
return user
else:
return None
initialize(
app,
authenticate=lambda: True,
retrieve_user=retrieve_user
)
Methods: POST
Generates an access token if the authenticate
method is True
. Using the example above, pass it a username
and password
and return an access token.
curl -X POST -H "Content-Type: application/json" -d '{"username": "<USERNAME>", "password": "<PASSWORD>"}' http://localhost:8000/auth
The response, if the user credentials are valid:
{
"access_token": "<JWT>"
}
Methods: GET
Returns with whether or not a given access token is valid.
curl -X GET -H "Authorization: Bearer <JWT>" http://localhost:8000/auth/verify
Assuming that it is valid, the response:
200 Response
{
"valid": true
}
If it is not valid, you will also be given a reason.
400 Response
{
"valid": false,
"reason": "Signature has expired"
}
Methods: GET
Returns information about the currently authenticated user.
curl -X GET -H "Authorization: Bearer <JWT>" http://localhost:8000/auth/me
Assuming that it is valid, the response:
200 Response
{
"user_id": 123456
}
As discussed, because the application is agnostic about your user management decisions, you need to have a user object that either is a dict
or a object instance with a to_dict
or __dict__
method. The output of these methods will be used to generate the /me
response.
Methods: POST
Validates the refresh token, and provides back a new access token.
curl -X POST -H "Content-Type: application/json" -H "Authorization: Bearer <JWT>" -d '{"refresh_token": "<REFRESH TOKEN>"}' http://localhost:8000/auth/refresh
The response, if the refresh token is valid.
{
"access_token": "<JWT>"
}
Note: Right now, you are required to send the access token (aka JWT
) and the refresh token. Why? Well, it seems like a good idea to facilitate the lookup of refresh tokens by knowing against which user you are trying to look up. The alternative is to lookup the user by refresh token alone. But, with this method, we are explicitly sending the user information in the JWT
. While there is NO verification of the JWT
at this stage, it is used to pass the payload. This decision may be subject to change in the future.
A route can be protected behind authentication simply by applying the @protected()
decorator.
from sanic_jwt.decorators import protected
@app.route("/")
async def open_route(request):
return json({"protected": False})
@app.route("/protected")
@protected()
async def protected_route(request):
return json({"protected": True})
In addition to protecting routes to authenticated users, they can be scoped to require one or more scopes by applying the @scoped()
decorator.
NOTE: If you are using the @scoped
decorator, you do NOT also need the @protected
decorator. It is assumed that if you are scoping the endpoint, that it is also meant to be protected.
A scope is a string that consists of two parts: namespace, and action(s). For example, it might look like this: user:read
.
namespace - A scope can have either one namespace, or no namespaces. action - A scope can have either no actions, or many actions.
scope: user
namespace: user
action: --
scope: user:read
namespace: user
action: read
scope: user:read:write
namespace: user
action: [read, write]
scope: :read
namespace: --
action: read
In defining a scoped route, you define one or more scopes that will be acceptable. A scope is accepted if the payload contains a scope that is equal to or higher than what is required. For sake of clarity in the below explanation, required_scope
means the scope that is required for access, and user_scope
is the scope the payload has.
A scope is acceptable ...
- If the
required_scope
namespace and theuser_scope
namespace are equal
# True
required_scope = 'user'
user_scope = 'user'
- If the
required_scope
has actions, then theuser_scope
must be top level (no defined actions), or also has the same actions
# True
required_scope = 'user:read'
user_scope = 'user'
# True
required_scope = 'user:read'
user_scope = 'user:read'
# True
required_scope = 'user:read'
user_scope = 'user:read:write'
# True
required_scope = ':read'
user_scope = ':read'
Here is a list of example scopes and whether they pass or not:
required scope user scope(s) outcome
============== ============= =======
'user' ['something'] False
'user' ['user'] True
'user:read' ['user'] True
'user:read' ['user:read'] True
'user:read' ['user:write'] False
'user:read' ['user:read:write'] True
'user' ['user:read'] False
'user:read:write' ['user:read'] False
'user:read:write' ['user:read:write'] True
'user:read:write' ['user:write:read'] True
'user' ['something', 'else'] False
'user' ['something', 'else', 'user'] True
'user:read' ['something:else', 'user:read'] True
'user:read' ['user:read', 'something:else'] True
':read' [':read'] True
':read' ['admin'] True
In order to protect a route from being accessed by tokens without the appropriate scope(s), pass in one or more scopes:
@app.route("/protected/scoped/1")
@protected()
@scoped('user')
async def protected_route1(request):
return json({"protected": True, "scoped": True})
In the above example, only an access token with a payload containing a scope for user
will be accepted (such as the payload below).
{
"user_id": 1,
"scopes: ["user"]
}
You can also define multiple scopes:
@scoped(['user', 'admin'])
In the above example, a payload MUST have both the user
and admin
scopes defined.
But, what if we only want to require one of the scopes, and not both user
AND admin
? Easy:
@scoped(['user', 'admin'], False)
Now, having a scope of either user
OR admin
will be acceptable.
The @scoped()
decorator takes three parameters:
scoped(scopes, requires_all, require_all_actions)
scopes
Either a single string
, or a list
of strings that are the defined scopes for the route.
@scoped('user')
...
# Or
@scoped(['user', 'admin'])
...
require_all
A boolean
that determines whether all of the defined scopes, or just one must be satisfied. Defaults to True
.
@scoped(['user', 'admin'])
...
# A payload MUST have both 'user' and 'admin' scopes
@scoped(['user', 'admin'], require_all=False)
...
# A payload can have either 'user' or 'admin' scope
require_all_actions
A boolean
that determines whether all of the actions on a defined scope, or just one must be satisfied. Defaults to True
.
@scoped(':read:write')
...
# A payload MUST have both the `:read` and `:write` actions in scope
@scoped(':read:write', require_all_actions=False)
...
# A payload can have either the `:read` or `:write` action in scope
See example/scopes.py
for a full working example with various scopes and users.
SANIC_JWT_ACCESS_TOKEN_NAME
Default: 'access_token'
Purpose: The key to be used in the payload to identify the access token.
SANIC_JWT_ALGORITHM
Default: 'HS256'
Purpose: The hashing algorithm used to generate the tokens. Your available options are:
- HS256 - HMAC using SHA-256 hash algorithm (default)
- HS384 - HMAC using SHA-384 hash algorithm
- HS512 - HMAC using SHA-512 hash algorithm
- ES256 - ECDSA signature algorithm using SHA-256 hash algorithm
- ES384 - ECDSA signature algorithm using SHA-384 hash algorithm
- ES512 - ECDSA signature algorithm using SHA-512 hash algorithm
- RS256 - RSASSA-PKCS1-v1_5 signature algorithm using SHA-256 hash algorithm
- RS384 - RSASSA-PKCS1-v1_5 signature algorithm using SHA-384 hash algorithm
- RS512 - RSASSA-PKCS1-v1_5 signature algorithm using SHA-512 hash algorithm
- PS256 - RSASSA-PSS signature using SHA-256 and MGF1 padding with SHA-256
- PS384 - RSASSA-PSS signature using SHA-384 and MGF1 padding with SHA-384
- PS512 - RSASSA-PSS signature using SHA-512 and MGF1 padding with SHA-512
SANIC_JWT_AUTHORIZATION_HEADER
Default: 'authorization'
Purpose: The HTTP request header used to identify the token. See also SANIC_JWT_AUTHORIZATION_HEADER_PREFIX
.
Example:
Authorization: Bearer <JWT HERE>
SANIC_JWT_AUTHORIZATION_HEADER_PREFIX
Default: 'Bearer'
Purpose: The prefix for the JWT in the HTTP request header used to identify the token. See also SANIC_JWT_AUTHORIZATION_HEADER
.
Example:
Authorization: Bearer <JWT HERE>
SANIC_JWT_AUTHORIZATION_HEADER_REFRESH_PREFIX
Default: 'Refresh'
Purpose: Not currently in use.
SANIC_JWT_CLAIM_AUD
Default: None
Purpose: The aud (audience) claim identifies the recipients that the JWT is intended for. Each principal intended to process the JWT MUST identify itself with a value in the audience claim. If the principal processing the claim does not identify itself with a value in the aud claim when this claim is present, then the JWT MUST be rejected. In the general case, the aud value is an array of case-sensitive strings, each commonly containing a string or URI value. In the special case when the JWT has one audience, the aud value MAY be a single case-sensitive string containing a string or URI value. Use of this claim is OPTIONAL. If you assign a str
value, then the aud claim will be generated for all requests, and will be required to verify a token.
SANIC_JWT_CLAIM_IAT
Default: None
, requires a bool
value
Purpose: The iat (issued at) claim identifies the time at which the JWT was issued. This claim can be used to determine the age of the JWT. Its value will be a numeric timestamp. Use of this claim is OPTIONAL. If you assign a True
value, then the iat claim will be generated for all requests.
SANIC_JWT_CLAIM_ISS
Default: None
, requires a str
value
Purpose: The iss (issuer) claim identifies the principal that issued the JWT. The iss value is a case-sensitive string usually containing a string or URI value. Use of this claim is OPTIONAL. If you assign a str
value, then the iss claim will be generated for all requests, and will be required to verify a token.
SANIC_JWT_CLAIM_NBF
Default: None
Purpose: The nbf (not before) claim identifies the time before which the JWT MUST NOT be accepted for processing. The processing of the nbf claim requires that the current date/time MUST be after or equal to the not-before date/time listed in the nbf claim. Implementers MAY provide for some small leeway, usually no more than a few minutes, to account for clock skew. Its value will be a numeric timestamp. Use of this claim is OPTIONAL. If you assign a True
value, then the nbg claim will be generated for all requests, and will be required to verify a token. If True
, the nbf claim will be set to the current time of the generation of the token. You can modify this with two additional settings: SANIC_JWT_CLAIM_NBF_DELTA
(the number of seconds to add to the timestamp) and SANIC_JWT_LEEWAY
(the number of seconds of leeway you want to allow for).
SANIC_JWT_CLAIM_NBF_DELTA
Default: 0
Purpose: The offset in seconds between the moment of token generation and the moment when you would like the token to be valid in the future. See SANIC_JWT_CLAIM_NBF
for more details.
SANIC_JWT_COOKIE_DOMAIN
Default: ''
Purpose: Used when SANIC_JWT_COOKIE_SET
is set to True
. When generating the cookie, it will associate it with this domain.
SANIC_JWT_COOKIE_HTTPONLY
Default: True
Purpose: Used when SANIC_JWT_COOKIE_SET
is set to True
. It enables HTTP only cookies. HIGHLY recommended that you do not turn this off, unless you know what you are doing.
SANIC_JWT_COOKIE_SET
Default: False
Purpose: By default, the application will lookie for access tokens in the HTTP request headers. If you would instead prefer to send them through cookies, enable this to True
.
SANIC_JWT_COOKIE_TOKEN_NAME
Default: SANIC_JWT_ACCESS_TOKEN_NAME
, will take whatever value is set there
Purpose: The name of the cookie to be set for storing the access token if using cookie based authentication.
SANIC_JWT_COOKIE_REFRESH_TOKEN_NAME
Default: SANIC_JWT_REFRESH_TOKEN_NAME
, will take whatever value is set there
Purpose: The name of the cookie to be set for storing the refresh token if using cookie based authentication.
SANIC_JWT_EXPIRATION_DELTA
Default: 60 * 5 * 6
Purpose: The length of time that the access token should be valid. Since there is NO way to revoke an access token, it is recommended to keep this time period short, and to enable refresh tokens (which can be revoked) to retrieve new access tokens.
SANIC_JWT_PAYLOAD_HANDLER
Default: 'sanic_jwt.handlers.build_payload'
Purpose: A handler method used to generate a payload. If you override this method, then you must return a dict
with a key to the user id. See SANIC_JWT_USER_ID
. In MOST cases, you should not need to override this method. If you would like to add additional information into a payload, the recommended method is to use SANIC_JWT_HANDLER_PAYLOAD_EXTEND
.
SANIC_JWT_HANDLER_PAYLOAD_EXTEND
Default: 'sanic_jwt.handlers.extend_payload'
Purpose: A handler method used to add additional information into a payload. It takes a payload
as an input, and returns the payload with the additional information. If you have any of the registered claims enabled (see SANIC_JWT_CLAIM_ISS
, SANIC_JWT_CLAIM_IAT
, SANIC_JWT_CLAIM_NBF
, SANIC_JWT_CLAIM_AUD
), then you must return them with this handler. Therefore, it is recommended to call sanic_jwt.handlers.extend_payload
inside your custom handler so as to make sure they are assigned properly.
Example:
from sanic_jwt.handlers import extend_payload
async def my_foo_bar_payload_extender(authenticator, payload, *args, **kwargs):
payload = extend_payload(authenticator, payload, *args, **kwargs)
payload.update({
'foo': 'bar'
})
return payload
SANIC_JWT_HANDLER_PAYLOAD_SCOPES
Default: None
Purpose: A handler method used to add scopes into a payload. It is a convenience method so that you do not need to extend the payload with the more verbose (yet, more flexible) SANIC_JWT_HANDLER_PAYLOAD_EXTEND
. It should return either a string
or a list
of strings
that meet the scope requirements. See the secion on Scopes for more details. Also, to make it easier for the developer, the user
instance that is returned by the authenticate
method is passed in as a parameter as seen below.
Example:
async def my_scope_extender(user, *args, **kwargs):
return user.scopes
SANIC_JWT_LEEWAY
Default: 180
Purpose: The number of seconds of leeway that the application will use to account for slight changes in system time configurations.
SANIC_JWT_REFRESH_TOKEN_ENABLED
Default: False
Purpose: Whether or not you would like to generate and accept refresh tokens.
SANIC_JWT_REFRESH_TOKEN_NAME
Default: 'refresh_token'
Purpose: The key to be used in the payload to identify the refresh token.
SANIC_JWT_SCOPES_NAME
Default: 'scopes'
Purpose: The key to be used in the payload to identify the scopes.
SANIC_JWT_SECRET
Default: 'This is a big secret. Shhhhh'
Purpose: When generating JWT tokens, a secret is used to uniquely identify and authenticate them. This should be a string unique to your application. Keep it safe.
SANIC_JWT_URL_PREFIX
Default: '/auth'
Purpose: The url prefix used for all URL endpoints. Note, the placement of /
.
SANIC_JWT_USER_ID
Default: 'user_id'
Purpose: The key or property of your user object that contains a user id.
SANIC_JWT_VERIFY_EXP
Default: True
Purpose: Whether or not to check the expiration on an access token. IMPORTANT: Changing this to False
means that access tokens will NOT expire. Make sure you know what you are doing before disabling this.