Sanic JWT¶
Sanic JWT adds authentication protection and endpoints to Sanic.
It is both easy to get up and running, and extensible for the developer. It can act to protect endpoints and also provide authentication scoping, all wrapped into a nice JWT.
Pick your favorite user management system, run a single class to initialize, and you are all set.
Getting Started¶
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)
You now will have a couple endpoints at your disposal:
/auth
/auth/verify
To obtain a token, just send a POST call to the /auth
endpoint:
curl -X POST http://localhost:8000/auth
You should get back a bit of JSON like this:
{
"access_token": "<JWT>"
}
Installation¶
Install from pypi using:
pip install sanic-jwt
Not much else to say. Depending upon whether or not you change the encryption algorithm, you may be prompted to also install cryptography. But, we only do that if you know you will need it.:
pip install cryptography
Simple Usage¶
Let’s take a look at a real simple example on how to use Sanic JWT to see the core concepts. Suppose we have a very simple user management system that stores User
objects in a list
. You can also access a user through a dict
index on the user_id
and the username
.
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={})".format(self.id)
def to_dict(self):
return {
'user_id': self.user_id,
'username': self.username,
}
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}
We want to be able to pass in a username and a password to authenticate our user, and then receive back an access token that can be used later on to access protected (aka private) data.
To get Sanic JWT started, we know that we need to initialize with the authenticate
method. The job of this method is to take the request
and determine if there is a valid user to be authenticated. Since the developer decides upon the user management system, it is our job to figure out what this method should do.
Very simple. Since we want to pass a username and a password to authenticate our user, we just need to check that the credentials are correct. If yes, we return the user. If no, we raise an exception.
from sanic_jwt import exceptions
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
Warning
In a real production setting it is advised to not tell the user why their authentication failed. Simply raising exceptions.AuthenticationFailed
should be enough. Here, for example purposes, we added some helper messages just to make it clear where we are failing.
Our whole application now looks like this:
from sanic import Sanic
from sanic_jwt import exceptions
from sanic_jwt import Initialize
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
app = Sanic()
Initialize(
app,
authenticate=authenticate,)
if __name__ == "__main__":
app.run(host="127.0.0.1", port=8888)
Let’s try and get an access token now:
curl -iv -H "Content-Type: application/json" -d '{"username": "user1", "password": "wrongpassword"}' http://localhost:8888/auth
Here is our response:
* Trying 127.0.0.1...
* TCP_NODELAY set
* Connected to localhost (127.0.0.1) port 8888 (#0)
> POST /auth HTTP/1.1
> Host: localhost:8888
> User-Agent: curl/7.55.1
> Accept: */*
> Content-Type: application/json
> Content-Length: 50
>
* upload completely sent off: 50 out of 50 bytes
< HTTP/1.1 401 Unauthorized
HTTP/1.1 401 Unauthorized
< Connection: keep-alive
Connection: keep-alive
< Keep-Alive: 60
Keep-Alive: 60
< Content-Length: 22
Content-Length: 22
< Content-Type: text/plain; charset=utf-8
Content-Type: text/plain; charset=utf-8
<
* Connection #0 to host localhost left intact
Password is incorrect.
Oops! Looks like we entered the wrong password. Let’s try again:
curl -iv -H "Content-Type: application/json" -d '{"username": "user1", "password": "abcxyz"}' http://localhost:8888/auth
Response:
* Trying 127.0.0.1...
* TCP_NODELAY set
* Connected to localhost (127.0.0.1) port 8888 (#0)
> POST /auth HTTP/1.1
> Host: localhost:8888
> User-Agent: curl/7.55.1
> Accept: */*
> Content-Type: application/json
> Content-Length: 43
>
* upload completely sent off: 43 out of 43 bytes
< HTTP/1.1 200 OK
HTTP/1.1 200 OK
< Connection: keep-alive
Connection: keep-alive
< Keep-Alive: 60
Keep-Alive: 60
< Content-Length: 140
Content-Length: 140
< Content-Type: application/json
Content-Type: application/json
<
* Connection #0 to host localhost left intact
{"access_token":"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoxLCJleHAiOjE1MTY2NTExNDB9.vmfQbfx0H8vIR6wILlLqS82bJILdwecfWlFRQuHb3Ck"}
That looks better. We can head over to jwt.io and enter the access_token
to see what the token consists of.
Header
{
"typ": "JWT",
"alg": "HS256"
}
Payload
{
"user_id": 1,
"exp": 1516651140
}
Now, we can confirm that this token works.
curl -iv -H "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoxLCJleHAiOjE1MTY2NTExNDB9.vmfQbfx0H8vIR6wILlLqS82bJILdwecfWlFRQuHb3Ck" http://localhost:8888/auth/verify
Response:
* Trying 127.0.0.1...
* TCP_NODELAY set
* Connected to localhost (127.0.0.1) port 8888 (#0)
> GET /auth/verify HTTP/1.1
> Host: localhost:8888
> User-Agent: curl/7.55.1
> Accept: */*
> Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoxLCJleHAiOjE1MTY2NTExNDB9.vmfQbfx0H8vIR6wILlLqS82bJILdwecfWlFRQuHb3Ck
>
< HTTP/1.1 200 OK
HTTP/1.1 200 OK
< Connection: keep-alive
Connection: keep-alive
< Keep-Alive: 60
Keep-Alive: 60
< Content-Length: 14
Content-Length: 14
< Content-Type: application/json
Content-Type: application/json
<
* Connection #0 to host localhost left intact
{"valid":true}
Excellent. Now that we can generate and verify tokens, we can get to work.
Best of luck creating an authentication scheme that works for you. If you have any questions about how to implement Sanic JWT (or to make it better), please create an issue or get in touch.
What is a JWT?¶
JSON Web Tokens (“JWT”) are an easy way to pass authentication information to a web-based backend system. They are easy to work with, but admittedly they can be confusing for someone who has never used them. In short, here are some key concepts you should know.
This is meant to give someone a high level overview of JWTs and a practical working knowledge of what is needed to get up and running with them in an application. For more information, I suggest you read jwt.io.
What does it look like?¶
JWTs are string of a bunch of characters:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
Upon closer inspection, it consists of three parts separated by a period
:
- eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
- eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9
- TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
In order, these parts are the Header, the Payload, and the Signature.
They can be decoded using base64.
The Header
{
"alg": "HS256",
"typ": "JWT"
}
The Payload
{
"sub": "1234567890",
"name": "John Doe",
"admin": true
}
The Signature
<some mess of stuff>
The Parts of a JWT¶
First, the header contains information about how the token’s is encoded, and what it is.
Third, the signature is used to verify that it came from a trusted source. It is encrypted using a secret only known to the application. The secret is NOT passed along inside the token, and should NOT be shared.
Both of these sections, you will not need to concern yourself with to get started.
The Second section is the payload. This bit of JSON contains key/value pairs of information. Each one of these is called (in JWT terminology) a claim.
In the above example, there are three claims: sub
, name
, and admin
.
The Payload¶
As an application developer, this is where you can send information from your server that authenticated a user (with a valid user name and password, for example) to a client application that needs to know what to display for the user. You can provide it with almost any bit of information you want that fits in JSON of course. And, because of the signature, you can be comfortable that the information inside the payload has not been compromised.
Warning
The payload is readable to anyone that gets a hold of it. DO NOT pass sensitive information in it.
While there are no real restrictions on what claims are inside of a JWT, there are some industry standards. Below is a list that Sanic JWT has integrated for you to easily use.
exp
- (short for expires) This claim is a timestamp that dictates when the access token will no longer be available. Because JWT access tokens cannot be invalidated after they are issued, they are typically given a short life span.
nbf
- (short for not before) This claim is a timestamp that allows the token to be created and issued, but not yet enabled for usage until after a certain time.
iat
- (short for issued at) This claim is a timestamp that provides the creation time of the JWT.
iss
- (short for issuer) This claim is typically a URI or other identifier to say who created and issued the token.
aud
- (short for audience) This claim identifies what service the JWT is intended to be used with. Typically it is a URI or other identifier that says the name of the client server that is supposed to be validating the token.
In addition to these claims, there is another claim that generally is important for Sanic JWT: user_id
. This is meant to be some unique indentification of the user that requested the token. Other than that, you are free to add whatever information you would like. See Payloads for information on how to modify the payload in Sanic JWT.
Initialization¶
Sanic JWT operates under the hood by creating a Blueprint, and attaching a few routes to your application. This is accomplished by instanciating the Initialize
class.
from sanic_jwt import Initialize
from sanic import Sanic
async def authenticate(request):
return dict(user_id='some_id')
app = Sanic()
Initialize(app, authenticate=authenticate)
Concept¶
Sanic JWT is a user authentication system that does not require the developer to settle on any single user management system. This part is left up to the developer. Therefore, you (as the developer) are left with the responsibility of telling Sanic JWT how to tie into your user management system.
The Initialize
class¶
This is the gateway worker into Sanic JWT. When initialized, it allows you to pass run time configurations to it, and gives you a window into customizing how the module will work for you. There are five main parts to it when initializing:
- the instance of your Sanic app or a Sanic blueprint | REQUIRED
- handler methods | the
authenticate
handler REQUIRED - any runtime configurations you want to make
- custom view classes
- component overrides
Instance¶
In most cases, the developer wants to add authentication to their web application. Simply instantiate Sanic, and then tell Sanic JWT.
from sanic_jwt import Initialize
from sanic import Sanic
app = Sanic()
Initialize(app, authenticate=lambda: True)
You can now go ahead and protect any route (whether on a blueprint or not).
from sanic_jwt import protected
from sanic.response import json
...
@app.route("/")
@protected
async def test(request):
return json({ "protected": True })
What if we ONLY want the authentication on some subset of our web application? Say, a Blueprint. Not a problem. Just initialize on the blueprint instance and continue as normal.
from sanic_jwt import Initialize
from sanic import Sanic, Blueprint
app = Sanic()
bp = Blueprint('my_blueprint')
Initialize(app, authenticate=lambda: True)
app.blueprint(bp)
Warning
If you are initializing on a blueprint, be careful of the ordering of app.blueprint()
and Initialize
. Putting them in the wrong order will cause the authentication endpoints to not properly attach.
Note
If you decide to initialize more than one instance of Sanic JWT (on multiple blueprints, for example), than an access token generated by one will be acceptable on ALL your instances unless they have different a secret
.
Under the hood, Sanic JWT creates its own Blueprint
for holding all of the Endpoints and Responses. If you decide to use your own blueprint, just know that Sanic JWT will not create its own, and instead attach to the one that you passed to it.
This is a very powerful tool that allows you to really gain some granularity in your applications authentication system.
async def authenticate(request, *args, **kwargs):
return get_my_user()
app = Sanic()
bp1 = Blueprint('my_blueprint_1')
bp2 = Blueprint('my_blueprint_2')
Initialize(app, authenticate=authenticate)
Initialize(bp1, authenticate=authenticate, access_token_name='mytoken')
Initialize(bp2, authenticate=authenticate, access_token_name='yourtoken')
In the above example, I now have three independent instances of Sanic JWT running side by side. Each is isolated to its own environment.
Handlers¶
There is a set of methods that Sanic JWT uses to hook into your application code. Each of them can be either a method or an awaitable. You decide.
# This works
async def authenticate(request, *args, **kwargs):
...
# And so does this
def authenticate(request, *args, **kwargs):
...
authenticate
- Required¶
Purpose: Just like Django’s authenticate
method, this is responsible for taking a given request
and deciding whether or not there is a valid user to be authenticated. If yes, it MUST return:
- a
dict
with auser_id
key, or - an instance with an id and
to_dict
property.
By default, it looks for the id on the user_id
property of a user instance. However, you can change that to another property.
If your user should not be authenticated, then you should raise an exception, preferably AuthenticationFailed
.
Example:
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 = await User.get(username=username)
if user is None:
raise exceptions.AuthenticationFailed("User not found.")
if password != user.password:
raise exceptions.AuthenticationFailed("Password is incorrect.")
return user
Initialize(app, authenticate)
store_refresh_token
- Optional *¶
Default: None
Purpose: It is a handler to persist a refresh token to disk. See refresh tokens for more information.
Example:
async def store_refresh_token(user_id, refresh_token, *args, **kwargs):
key = 'refresh_token_{user_id}'.format(user_id=user_id)
await aredis.set(key, refresh_token)
Initialize(
app,
authenticate=lambda: True,
store_refresh_token=store_refresh_token)
Warning
* This parameter is not required. However, if you decide to enable refresh tokens (by setting SANIC_JWT_REFRESH_TOKEN_ENABLED=True
) then the application will raise a RefreshTokenNotImplemented
exception if you forget to implement this.
retrieve_refresh_token
- Optional *¶
Default: None
Purpose: It is a handler to retrieve refresh token from disk. See refresh tokens for more information.
Example:
async def retrieve_refresh_token(user_id, *args, **kwargs):
key = 'refresh_token_{user_id}'.format(user_id=user_id)
return await aredis.get(key)
Initialize(
app,
authenticate=lambda: True,
retrieve_refresh_token=retrieve_refresh_token)
Warning
* This parameter is not required. However, if you decide to enable refresh tokens (by setting SANIC_JWT_REFRESH_TOKEN_ENABLED=True
) then the application will raise a RefreshTokenNotImplemented
exception if you forget to implement this.
retrieve_user
- Optional¶
Default: None
Purpose: It is a handler to retrieve a user object from your application. It is used to return the user object in the /auth/me
endpoint. It should return:
- a
dict
, or - an instance with a
to_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}
async def retrieve_user(request, payload, *args, **kwargs):
if payload:
user_id = payload.get('user_id', None)
user = await User.get(user_id=user_id)
return user
else:
return None
Initialize(
app,
authenticate=lambda: True,
retrieve_user=retrieve_user)
You should now have an endpoint at /auth/me
that will return a serialized form of your currently authenticated user.
{
"me": {
"user_id": "4",
"username": "joe",
"email": "joe@joemail.com",
"verified": true
}
}
Warning
* This parameter is not required. However, if you decide to enable refresh tokens (by setting SANIC_JWT_REFRESH_TOKEN_ENABLED=True
) then the application will raise a RefreshTokenNotImplemented
exception if you forget to implement this.
add_scopes_to_payload
- Optional *¶
Default: None
Purpose: It is a handler to add scopes to an access token. See Scopes for more information.
Example:
async def add_scopes_to_payload(user):
return await user.get_scopes()
Initialize(
app,
authenticate=lambda: True,
add_scopes_to_payload=add_scopes_to_payload)
Runtime Configuration¶
There are several ways to configure the settings for Sanic JWT. One of the easiest is to simply pass the configurations as keyword objects on Initialize.
Initialize(
app,
access_token_name='mytoken',
cookie_access_token_name='mytoken',
cookie_set=True,
user_id='id',
claim_iat=True,
cookie_domain='example.com',)
Additional Views¶
Sometimes you may need to add some endpoints to the authentication system. When this need arises, create a class based view, and map it as a tuple with the path and handler.
As an example, perhaps you would like to create a “passwordless” login. You could create a form that sends a POST with a user’s email address to a MagicLoginHandler
. That handler sends out an email with a link to your /auth
endpoint that makes sure the link came from the email.
class MagicLoginHandler(HTTPMethodView):
async def options(self, request):
return response.text('', status=204)
async def post(self, request):
helper = MyCustomUserAuthHelper(app, request)
token = helper.get_make_me_a_magic_token()
helper.send_magic_token_to_user_email()
# Persist the token
key = f'magic-token-{token}'
await app.redis.set(key, helper.user.uuid)
response = {
'magic-token': token
}
return json(response)
def check_magic_token(request):
token = request.json.get('magic_token', '')
key = f'magic-token-{token}'
retrieval = await request.app.redis.get(key)
if retrieval is None:
raise Exception('Token expired or invalid')
retrieval = str(retrieval)
user = User.get(uuid=retrieval)
return user
Initialize(
app,
authenticate=check_magic_token,
class_views=[
('/magic-login', MagicLoginHandler) # The path will be relative to the url prefix (which defaults to /auth)
])
Note
Your class based views will probably also need to handle preflight requests, so do not forget to add an options response.
async def options(self, request):
return response.text('', status=204)
Component Overrides¶
There are three components that are used under the hood that you can subclass and control:
Authentication
- for more advanced usage, see source codeConfiguration
- see Configuration for more informationResponses
- see Endpoints and Responses for more information
Simply import, modify, and attach.
from sanic_jwt import Authentication, Configuration, Responses, Initialize
class MyAuthentication(Authentication):
pass
class MyConfiguration(Configuration):
pass
class MyResponses(Responses):
pass
Initialize(
app,
authentication_class=MyAuthentication,
configuration_class=MyConfiguration,
responses_class=MyResponses,)
The initialize
method¶
The old method for initializing Sanic JWT was to do so with the initialize
method. It still works, and is in fact now just a wrapper for the Initialize
class. However, it is recommended that you use the class because it is more explicit that you are declaring a new instance.
Endpoints and Responses¶
Sanic JWT sets itself up to run as a Sanic Blueprint at the /auth
path.
http://localhost:8000/auth
This is can be changed via the url_prefix
setting. See settings for more.
Initialize(app, url_prefix='/api/authentication')
All Sanic JWT endpoints will now be available at:
http://localhost:8000/api/authentication
Default Endpoints¶
By default, there are four endpoints that ship with Sanic JWT. You can change the path that they attach to by following configuration pattern below:
Initialize(
app,
path_to_authenticate='/my_authenticate',
path_to_retrieve_user='/my_retrieve_user',
path_to_verify='/my_verify',
path_to_refresh='/my_refresh',
)
Authenticate¶
/auth
POST
authenticate
method is truthy.Request
curl -X POST -H "Content-Type: application/json" -d '{"username": "<USERNAME>", "password": "<PASSWORD>"}' http://localhost:8000/auth
Response
200 Response
{
"access_token": "<JWT>"
Verification¶
/auth/verify
GET
Request
curl -X GET -H "Authorization: Bearer <JWT>" http://localhost:8000/auth/verify
Response
200 Response
{
"valid": true
}
## or
400 Response
{
"valid": false,
"reason": "Signature has expired"
}
Current User Details¶
/auth/me
GET
Request
curl -X GET -H "Authorization: Bearer <JWT>" http://localhost:8000/auth/me
Response
200 Response
{
"user_id": 123456
}
Note
Because this package does not know about you user management layer, you need to have a user object that either is a dict
or a python object instance with a to_dict()
method. The output of these methods will be used to generate the /me
response.
Refresh Token¶
/auth/refresh
POST
Request
curl -X POST -H "Content-Type: application/json" -H "Authorization: Bearer <JWT>" -d '{"refresh_token": "<REFRESH TOKEN>"}' http://localhost:8000/auth/refresh
Response
{
"access_token": "<JWT>"
}
Note
Do not forget to supply an existing access_token
. Even if it is expired, you must send the token along so that the application can get the user_id
from the token’s payload and cross reference it with the refresh_token
. Think of it as an additional level of security. To understand why, checkout Issue #52.
Modify Responses¶
The responses for each of the default endpoints is extendable by subclassing the Responses
class, and hooking into the appropriate method. Just make sure you return a dict
.
from sanic_jwt import Responses
class MyResponses(Responses):
@staticmethod
def extend_authenticate(request,
user=None,
access_token=None,
refresh_token=None):
return {}
@staticmethod
def extend_retrieve_user(request, user=None, payload=None):
return {}
@staticmethod
def extend_verify(request, user=None, payload=None):
return {}
@staticmethod
def extend_refresh(request,
user=None,
access_token=None,
refresh_token=None,
purported_token=None,
payload=None):
return {}
Initialize(app, response_class=MyResponses)
Custom Endpoints¶
Sometimes you may find the need to add another endpoint to your authentication system. You can do this by hooking it up at initialization.
from sanic_jwt import BaseEndpoint
class MyEndpoint(BaseEndpoint):
...
my_views = (
('/my-view', MyEndpoint),
)
Initialize(app, class_views=my_views)
Example:
What if we wanted a /register
endpoint? It could easily be added like this:
from sanic_jwt import BaseEndpoint
class Register(BaseEndpoint):
async def post(self, request, *args, **kwargs):
username = request.json.get('username', None)
email = request.json.get('email', None)
helper = MyCustomUserAuthHelper()
user = helper.register_new_user(username, email)
access_token, output = await self.responses.get_access_token_output(
request,
user,
self.config,
self.instance)
refresh_token = await self.instance.auth.get_refresh_token(request, user)
output.update({
self.config.refresh_token_name: refresh_token
})
response = self.responses.get_token_reponse(
request,
access_token,
output,
refresh_token=refresh_token,
config=self.config)
return response
my_views = (
('/register', Register),
)
Initialize(app, class_views=my_views)
You hook up your custom endpoints at initialization by providing Initialize
with a class_views
argument naming your endpoint and its path.
my_endpoints = (
('/path/to/endpoint', MyCustomClassBasedView)
)
Note
It must be a class based view. While it is certainly possible to subclass Sanic’s sanic.views.HTTPMethodView
, it is recommended that you subclass sanic_jwt.BaseEndpoint
instead so you have access to:
self.instance
(the current Sanic JWT),self.config
(all current configurations), andself.responses
(the current response class instance).
Exception Handling¶
You can customize how Sanic JWT handles responses on an exception by subclassing the Responses
class, and overriding exception_response
.
from sanic_jwt import Responses
class MyResponses(Responses):
@staticmethod
def exception_response(request, exception):
exception_message = str(exception)
return json({
'error': True,
'message': f'You encountered an exception: {exception_message}'
}, status=exception.status_code)
Initialize(app, response_class=MyResponses)
Payloads¶
As discussed, JWTs have a payload that is essentially a key/value store of information.
With Sanic JWT, there are three main uses of the payload:
- passing claims (See What is a JWT? for more information)
- passing scope (See Scopes for more information)
- passing arbitrary information to the client
Built in Claims¶
Sanic JWT ships with the capability to add, and later verify, five standard claims: exp
, nbf
, iat
, iss
, and aud
.
Expires - exp
¶
Warning
It is possible to disable token expiration. Do NOT do this unless you know what you are doing and why you are doing it.
Initialize(app, verify_exp=False)
Audience - aud
¶
claim_aud
to a str
Initialize(app, claim_aud='my_client_domain.com')
Issued at - iat
¶
claim_iat
to True
Initialize(app, claim_iat=True)
Issuer - iss
¶
claim_iss
to a str
Initialize(app, claim_iss='my_server_domain.com')
Not before - NBF
¶
claim_nbf
to True
, and claim_nbf_delta
to an offset in secondsInitialize(app, claim_nbf=True, claim_nbf_delta=(60 * 3))
Payload Handlers¶
As discussed, there are a few handlers on the Initialize
instance that can be used to modify the payload.
Adding Scopes¶
add_scopes_to_payload
@scoped
decorator, then you will need a way to inject the payload
with the user’s scopes. It should return either a single scope, or a list of scopes. Read about scopes for more information.str
or a list
of str
async def my_scope_extender(user, *args, **kwargs):
return user.scopes
Initialize(app, add_scopes_to_payload=my_scope_extender)
Note
The return of the authenticate
method will be injected into this handler as user
for your convenience.
Extending the payload¶
extend_payload
dict
def my_foo_bar_payload_extender(payload, *args, **kwargs):
payload.update({
'foo': 'bar'
})
return payload
Initialize(app, extend_payload=my_foo_bar_payload_extender)
Token signing¶
JWTs need to be digitally signed to allow for cryptographically verifying that an access token was generated by your application.
secret = 'XXXXXXXXXXXXXXXXXXXXXXXX'
Initialize(
app,
secret=mysecret)
There are several hashing algorithms that can be used to accomplish this. Check out the Configuration page to see which algorithms are supported, and read this for more information.
If you decide to use an RSA or an EC algorithm, then you must provide Sanic JWT with both a public key and a private key to handle the encoding and decoding of the tokens.
from pathlib import Path
public_ec_key = Path('/path') / 'to' / 'my-ec-public-key.pem'
private_ec_key = Path('/path') / 'to' / 'my-ec-private-key.pem'
Initialize(
app,
public_key=public_ec_key,
private_key=private_ec_key,
algorithm='ES256')
Protecting Routes¶
The purpose of this package, beyond the creation of JWTs, is to protect routes so that only users with a valid access token can reach certain resources. Endpoints in your application can be protected using the @protected
decorator.
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})
Now, anyone can access the /
route. But, only users that pass a valid access token can reach protected
.
If you have initialized Sanic JWT on a Blueprint
, then you will need to pass the instance of that blueprint into the @protected
decorator.
bp = Blueprint('Users')
Initialize(bp)
@bp.get('/users/<id>')
@protected(bp)
async def users(request, id):
...
Class based views¶
Using the standard Sanic methodology, you can protect class based views with the same decorator.
class PublicView(HTTPMethodView):
def get(self, request):
return json({"protected": False})
class ProtectedView(HTTPMethodView):
decorators = [protected()]
async def get(self, request):
return json({"protected": True})
app.add_route(PublicView.as_view(), '/')
app.add_route(ProtectedView.as_view(), '/protected')
Passing the Token¶
There are two general methodologies for passing a token: cookie based, and header based. By default, Sanic JWT will expect you to send tokens thru HTTP headers.
curl -X GET -H "Authorization: Bearer <JWT>" http://localhost:8000/auth/me
Header Tokens¶
Header tokens are passed by adding an Authorization
header that consists of two parts:
- the word
Bearer
- the JWT access token
If you would like, you can modify this behavior by changing the settings for authorization_header
and authorization_header_prefix
.
Initialize(
app,
authorization_header='somecustomheader',
authorization_header_prefix='MeFirst',)
curl -X GET -H "somecustomheader: MeFirst <JWT>" http://localhost:8000/auth/me
Cookie Tokens¶
If you would like to use tokens in cookies instead of headers, you need to first set cookie_set=True
Initialize(app, cookie_set=True)
Now, Sanic JWT will reject any request that does not have a valid access token in its cookie. As the developer, you can control how the cookie is generated with the following settings:
cookie_domain
- changes domain associated with a cooke (defaults to ‘’)cookie_httponly
- whether to set an httponly flag on the cookie (defaults to True
)cookie_access_token_name
- the name where the cookie is storedInitialize(
app,
cookie_set=True,
cookie_domain='mydomain.com',
cookie_httponly=False,
cookie_access_token_name='some-token',)
Warning
If you are using cookies to pass JWTs, then it is recommended that you do not disable cookie_httponly
. Doing so means that any javascript running on the client can access the token. Bad news.
Both Header and Cookie¶
If you enable cookie_set
, you will get a MissingAuthorizationCookie
exception if the cookie is not present. However, sometimes you may want to fall back and look for a header token if the cookie is not there.
Is such cases, change cookie_strict
to False
.
Initialize(
app,
cookie_set=True,
cookie_strict=False,)
Per view declaration¶
Coming soon - the ability to decide at the view level which token to accept
Scopes¶
In addition to protecting routes to authenticated users, they can be scoped to require one or more scopes by applying the @scoped()
decorator. This means that only users with a particular scope can access a particular endpoint.
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.
Requirements for a scoped¶
A scope is a string that consists of two parts:
- namespace
- actions
For example, it might look like this: user:read
.
Example Scopes¶
Example #1
scope: user
namespace: user
action: --
Example #2
scope: user:read
namespace: user
action: read
Example #3
scope: user:read:write
namespace: user
action: [read, write]
Example #4
scope: :read
namespace: --
action: read
Example #5
scope: :read:write
namespace: --
action: [read, write]
How are scopes accepted?¶
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 that the access token has in its payload.
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' # False required_scope = 'user:write' user_scope = 'user:read'
Examples¶
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
The @scoped decorator¶
Basics¶
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")
@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 with a ['user', 'admin']
scope, a payload MUST contain both user
and admin
.
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.
If you have initialized Sanic JWT on a Blueprint
, then you will need to pass the instance of that blueprint into the @scoped
decorator.
bp = Blueprint('Users')
Initialize(bp)
@bp.get('/users/<id>')
@scoped(['user', 'admin'], initialized_on=bp)
async def users(request, id):
...
Parameters¶
The @scoped()
decorator takes three parameters:
scopes
requires_all
- defaultTrue
require_all_actions
- defaultTrue
scopes
- Required¶
Either a single string
, or a list
of strings that are the defined scopes for the route. Or, a callable
or awaitable
that returns the same.
@scoped('user')
...
Or
@scoped(['user', 'admin'])
...
Or
def get_some_scopes(request, *args, **kwargs):
return ['user', 'admin']
@scoped(get_some_scopes)
...
Or
async def get_some_scopes(request, *args, **kwargs):
return await something_that_returns_scopes()
@scoped(get_some_scopes)
...
require_all
- Optional¶
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
- Optional¶
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
Sample Code¶
from sanic import Sanic
from sanic.response import json
from sanic_jwt import exceptions
from sanic_jwt import initialize
from sanic_jwt.decorators import protected
from sanic_jwt.decorators import scoped
class User(object):
def __init__(self, id, username, password, scopes):
self.user_id = id
self.username = username
self.password = password
self.scopes = scopes
def __str__(self):
return "User(id='%s')" % self.id
users = [
User(1, 'user1', 'abcxyz', ['user']),
User(2, 'user2', 'abcxyz', ['user', 'admin']),
User(3, 'user3', 'abcxyz', ['user:read']),
User(4, 'user4', 'abcxyz', ['client1']),
]
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
async def my_scope_extender(user, *args, **kwargs):
return user.scopes
app = Sanic()
Initialize(
app,
authenticate=authenticate,
add_scopes_to_payload=my_scope_extender)
@app.route("/")
async def test(request):
return json({"hello": "world"})
@app.route("/protected")
@protected()
async def protected_route(request):
return json({"protected": True, "scoped": False})
@app.route("/protected/scoped/1")
@protected()
@scoped('user')
async def protected_route1(request):
return json({"protected": True, "scoped": True})
@app.route("/protected/scoped/2")
@protected()
@scoped('user:read')
async def protected_route2(request):
return json({"protected": True, "scoped": True})
@app.route("/protected/scoped/3")
@protected()
@scoped(['user', 'admin'])
async def protected_route3(request):
return json({"protected": True, "scoped": True})
@app.route("/protected/scoped/4")
@protected()
@scoped(['user', 'admin'], False)
async def protected_route4(request):
return json({"protected": True, "scoped": True})
@app.route("/protected/scoped/5")
@scoped('user')
async def protected_route5(request):
return json({"protected": True, "scoped": True})
@app.route("/protected/scoped/6/<id>")
@scoped(lambda *args, **kwargs: 'user')
async def protected_route6(request, id):
return json({"protected": True, "scoped": True})
def client_id_scope(request, *args, **kwargs):
return 'client' + kwargs.get('id')
@app.route("/protected/scoped/7/<id>")
@scoped(client_id_scope)
async def protected_route7(request, id):
return json({"protected": True, "scoped": True})
if __name__ == "__main__":
app.run(host="127.0.0.1", port=8888)
Refresh Tokens¶
What is a refresh token?¶
Access tokens are disposable. Because they cannot be expired, they have a short lifespan. However, without a refresh token, the client would need to reauthenticate every time the access token expired. Access tokens are generated, and sent to the client. They are not persisted.
Refresh tokens solve this problem. It is a token that is stored by the server. At any time a client can send the refresh token to the server and ask for a new access token.
The server takes the refresh token, looks up in its data store to see if it is acceptable. If yes, then a new access token is stored.
Configuration¶
Sanic JWT facilitates the creation and passing of refresh tokens. However, just like with authentication, the storage and retrieval of the tokens is left to the developer. This allows the you to decide how to persist the token, and allows you to deactivate a token at any time.
There are three steps needed:
- Enable refresh tokens via the settings configuration (
refresh_token_enabled
) - Initialize Sanic JWT with a method for storing refresh tokens (
store_refresh_token
) - Initialize Sanic JWT with a method for retrieving refresh tokens (
retrieve_refresh_token
)
Handlers¶
store_refresh_token
¶
When running Initialize
, pass it an attribute that can go to your data store and persist a refresh token. The method is passed user_id
(which comes from the user object returned from the authenticate
method), and refresh_token
.
It can be either a callable or an awaitable. Here are two different examples that all do the same thing: persist a refresh_token
to Redis.
async def store_refresh_token(user_id, refresh_token, *args, **kwargs):
key = f'refresh_token_{user_id}'
await aredis.set(key, refresh_token)
def store_refresh_token(user_id, refresh_token, *args, **kwargs):
key = f'refresh_token_{user_id}'
redis.set(key, refresh_token)
Then you hook it up to the initialize script like this:
Initialize(
app,
authenticate=lambda: True,
store_refresh_token=store_refresh_token)
retrieve_refresh_token
¶
When running Initialize
, pass it an attribute that can go to your data store and retrieve a refresh token. The method is passed user_id
(which comes from the user object returned from the authenticate
method), and the request
object to determine if it contains what is needed to retrieve a token.
It can be either a callable or an awaitable. Here are two different examples that all do the same thing: retrieve a refresh_token
from Redis.
async def retrieve_refresh_token(request, user_id, *args, **kwargs):
key = f'refresh_token_{user_id}'
return await aredis.get(key)
def retrieve_refresh_token(request, user_id, *args, **kwargs):
key = f'refresh_token_{user_id}'
return redis.get(key)
Then you hook it up to the initialize script like this:
Initialize(
app,
authenticate=lambda: True,
retrieve_refresh_token=retrieve_refresh_token)
Using the refresh token¶
In order to get a new access token, you need to hit the refresh token endpoint. See Endpoints and Responses for more information.
Can I have an expirable refresh token?¶
This question has come up a couple times in the past. Allow us to explain why this is not a feature of Sanic JWT.
When enabled, Sanic JWT issues a refresh token that is a utf-8
encoded string containing 24 characters. It is not a JWT. Therefore, it does not have a payload and is not subject to validation.
The core of deciding whether or not to accept a refresh token is left to the developer. That is the purpose of store_refresh_token
and retrieve_refresh_token
.
Therefore, if you would like to expire the token, then this is something for you to handle at the application layer.
For more information on this, see Issue #34 and Issue #66.
We agree. Having the control expire a token is wonderful. Having it be done automatically? Even better. But, this is something that seems better left to the individual developer to decide upon, rather than Sanic JWT making that choice for you.
Exceptions¶
There is a standard set of exceptions that Sanic JWT uses to communicate. Here is a subset of exceptions that you may find helpful while creating your application.
AuthenticationFailed
MissingAuthorizationHeader
MissingAuthorizationCookie
InvalidAuthorizationHeader
MissingRegisteredClaim
Unauthorized
It is recommended that you use exceptions in your Sanic JWT implementation. If an exception occurs, then you can control what message to return to the client. See Endpoints and Responses for more information.
Configuration¶
How to add settings¶
There are several ways to configure Sanic JWT depeding upon your project’s complexity and use case.
The Sanic way¶
Any way that Sanic offers to load configration will work. Simply convert the setting name to all caps, and add the SANIC_JWT_
prefix.
app = Sanic()
app.config.SANIC_JWT_ACCESS_TOKEN_NAME = 'jwt'
Initialize(app)
If you choose this approach, Sanic JWT will only know about configurations set _BEFORE_ you call Initialize
.
Inline at initialization¶
One of the easiest methods is to simply name the setting and value as a keyword argument on the Initialize
object.
Initialize(
app,
access_token_name='jwt')
Configuration
class¶
For a more fine grain control, you can subclass the Configuration
class and provide the settings as attributes on the class.
from sanic_jwt import Configuration
class MyConfiguration(Configuration):
access_token_name='jwt'
Initialize(
app,
configuration_class=MyConfiguration)
What if you need to calculate a setting? No problem. Each of the settings can be declared at initialization with the set_<setting>()
method.
from sanic_jwt import Configuration
class MyConfiguration(Configuration):
def set_access_token_name(self):
return 'jwt'
Initialize(
app,
configuration_class=MyConfiguration)
Settings¶
access_token_name
¶
'access_token'
algorithm
¶
'HS256'
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
authorization_header
¶
'authorization'
authorization_header_prefix
¶
'Bearer'
authorization_header_refresh_prefix
¶
'Refresh'
claim_aud
¶
None
claim_iat
¶
True
value, then the iat claim will be generated for all requests.False
claim_iss
¶
None
, requires a str
valueclaim_nbf
¶
True
value, then the nbf
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: nbf_delta
(the number of seconds to add to the timestamp) and leeway
(the number of seconds of leeway you want to allow for).False
claim_nbf_delta
¶
60 * 3
cookie_access_token_name
¶
'access_token'
cookie_domain
¶
''
cookie_httponly
¶
True
cookie_refresh_token_name
¶
'refresh_token'
cookie_set
¶
True
.False
cookie_strict
¶
cookie_set
is enabled, an exception will be raised if the cookie is not present. To allow for an authorization header to be used as a fallback, turn cookie_strict
to False
.True
cookie_token_name
¶
Alias for cookie_access_token_name
debug
¶
False
expiration_delta
¶
60 * 5 * 6
, aka 30 minutesleeway
¶
60 * 3
, aka 3 minutespath_to_authenticate
¶
'/'
path_to_refresh
¶
'/refresh'
path_to_retrieve_user
¶
'/me'
path_to_verify
¶
'/verify'
private_key
¶
None
public_key
¶
Alias for secret
refresh_token_enabled
¶
False
refresh_token_name
¶
'refresh_token'
scopes_enabled
¶
False
scopes_name
¶
'scopes'
secret
¶
'This is a big secret. Shhhhh'
strict_slashes
¶
False
url_prefix
¶
/
.'/'
user_id
¶
'user_id'
verify_exp
¶
True
Warning
IMPORTANT: Changing verify_exp to False
means that access tokens will NOT expire. Make sure you know what you are doing before disabling this.
This is a potential SECURITY RISK.
Examples¶
Basic¶
Protecting Class Based Views¶
Refresh token¶
Dynamic Scoping¶
Passwordless Login¶
User registration¶
Contributing¶
Shout out to @vltr
¶
A special shout out to @vltr for all his work on Sanic JWT. This project received a nice kick and was taken up a notch due to his generous contribution of time, and skill.
Want to help?¶
Truly, this project has been a labor of love. We want to empower you to have an easy experience implementing best practices for a secure web application, yet still make it possible to be highly in control.
Do you think we did something wrong? Tell us
Have a better way? We’re listening
If you want to get involved, here’s how.
Changelog¶
The format is based on Keep a Changelog and this project adheres to Semantic Versioning.
Version 1.0.2 - 2018-03-04¶
BaseEndpoint
Version 1.0.1 - 2018-02-27¶
OPTIONS
handler method for BaseEndpoint
class_views
Version 1.0.0 - 2018-02-25¶
Initialize
class@protected
implementation on class based viewsSANIC_JWT_PAYLOAD_HANDLER
SANIC_JWT_HANDLER_PAYLOAD_EXTEND
SANIC_JWT_HANDLER_PAYLOAD_SCOPES
Legend¶
- Added for new features.
- Changed for changes in existing functionality.
- Deprecated for once-stable features removed in upcoming releases.
- Removed for deprecated features removed in this release.
- Fixed for any bug fixes.
- Security to invite users to upgrade in case of vulnerabilities.
What is new in Version 1.0?¶
If you have been using Sanic JWT, there should really not be that much different, although under the hood a lot has changed. For starters, the initialize
method still works. But, the new recommended way to start Sanic JWT is to use the new Initialize
class as seen above.
Using this class allows you to subclass it and really dive deep into modifying and configuring your project just the way you need it. Want to change the authentication responses? No problem. Want to add some new authentication endpoints? Easy.
One of the bigger changes is that we have enabled a new way to add configuration settings. You can of course continue to set them as recommended by Sanic by making them in all capital letters, and giving it a SANIC_JWT_
prefix.
app.config.SANIC_JWT_ACCESS_TOKEN_NAME = 'mytoken'
Or, you can simply pass your configurations into the Initialize
class as keyword arguments.
Initialize(
app,
access_token_name='mytoken'
)
Do you need some more complicated logic, or control? Then perhaps you want to subclass the Configuration
class.
class MyConfig(Configuration):
access_token_name='mytoken'
def get_refresh_token_name(self):
return some_crazy_logic_to_get_token_name()
Initialize(
app,
configuration_class=MyConfig
)
The point is, with Version 1, we made the entire package extremely adaptable and extensible for you to get done what you need without making decisions for you.
Have fun, and happy coding.