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 an instance of sanic_jwt.Initialize
with your 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=authenticate)
What is an authentication function? We’ll get into it later, but for now all you need to know is that it is a function you control that takes a request
and decides if there is a valid user or not. This gives you the flexibility to roll with whatever user management system you want.
After initialization, 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>"
}
Want to check to make sure it is valid?
curl -X GET -H "Authorization: Bearer <JWT>" http://localhost:8000/auth/verify
Your response should be this:
{
"valid": true
}
You now have a working authentication system. Woohoo!
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:
def __init__(self, id, username, password):
self.user_id = id
self.username = username
self.password = password
def __repr__(self):
return "User(id='{}')".format(self.user_id)
def to_dict(self):
return {"user_id": self.user_id, "username": self.username}
users = [User(1, "user1", "abcxyz"), User(2, "user2", "abcxyz")]
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:
def __init__(self, id, username, password):
self.user_id = id
self.username = username
self.password = password
def __repr__(self):
return "User(id='{}')".format(self.user_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}
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
{"reasons":"Password is incorrect.","exception":"AuthenticationFailed"}
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 with 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:
authenticate
handler) is REQUIREDInstance¶
Most web applications need authentication. With Sanic JWT, all you do is create your Sanic app, 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(bp, app=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), then an access token generated by one will be acceptable on ALL your instances unless they have different a secret
. You can learn more about how to set that in Configuration.
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 (and by all means, feel free to do so!), just know that Sanic JWT will not create its own. When this happens, Sanic JWT instead will attach to the blueprint that you passed to it.
This is a very powerful tool that allows you to really gain some granularity in your applications’ authentication systems.
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, and can have its own set of Configuration.
Handlers¶
There is a group of methods that Sanic JWT uses to hook into your application code. This is how it is able to live alongside your application and seemlessly plug in.
Each handler 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):
...
1. 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
. Please do not just return None
. If you do, you will likely get a 500
error.
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)
2. store_refresh_token
*¶
Purpose: It is a handler to persist a refresh token to disk. See refresh tokens for more information. Sanic JWT create the refresh token, but you get to decide how it is stored.
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 refresh_token_enabled=True
in your configurations) then the application will raise a RefreshTokenNotImplemented
exception if you forget to implement this.
3. retrieve_refresh_token
*¶
Purpose: It is a handler to retrieve refresh token from disk. See refresh tokens for more information. Sanic JWT created the refresh token. You stored it. Now Sanic JWT wants it back, it is your job to retrieve it.
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 refresh_token_enabled=True
in your configurations) then the application will raise a RefreshTokenNotImplemented
exception if you forget to implement this.
4. retrieve_user
¶
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, and also the @inject_user
decorator that you will learn about later.
It should return:
dict
, orto_dict
method, orNone
As we said before, you are deciding on the user management system. Sanic JWT is acting as the gatekeeper. But, inherently tied in are a number of use cases where it would be convenient to get your user object. This is how you do it.
Example:
class User:
...
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
}
}
5. retrieve_user_secret
*¶
Purpose: It is a handler to retrieve or generate a unique secret for a user. All JWTs are encoded using a secret, and this allows for you to provide a unique secret for encoding per user. You must also initialize with user_secret_enabled
.
Example:
async def retrieve_user_secret(user_id):
return f"my-super-safe-dynamically-generated-secret|{user_id}"
Initialize(
app,
authenticate=lambda: True,
retrieve_user_secret=retrieve_user_secret)
Warning
* This parameter is not required. However, if you decide to enable user secrets (by setting user_secret_enabled=True
in your configurations) then the application will raise a UserSecretNotImplemented
exception if you forget to implement this.
6. add_scopes_to_payload
*¶
Purpose: It is a handler to add scopes to an access token. See Scopes for more information.
Scoping is a long discussion by itself. In short, it is a highly powerful tool to help with providing permissioning to your application. It is your job to add these scopes (if you want them) to the JWT. Then, you can specifiy which scopes are required on specific endpoints.
For now, all you need to do is return a list
of one or more strings
.
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)
7. override_scope_validator
*¶
Purpose: It is a handler to override the default scope validation. See Scopes for more information.
This could be useful if you decide to bake some additional logic into your scopes. At its most simplified level, Sanic JWT looks at scopes and compares fruit:apples
to fruit:apples
. What if sometimes fruit:oranges
should be accepted? You have the ability to code that override and make your own decision.
Note
Above, we said “Each of them can be either a method or an awaitable. You decide.” What we forgot to mention was that override_scope_validator
needs to be a regular callable
and not an awaitable
.
No async programming here. Sorry for the confusion.
Example:
def my_scope_override(is_valid,
required,
user_scopes,
require_all_actions,
*args,
**kwargs):
return False
Initialize(
app,
authenticate=lambda: True,
override_scope_validator=my_scope_override)
8. destructure_scopes
¶
Purpose: It is a handler that allows you to manipulate and handle the scopes before they are validated.
Sometimes, you may find the need to manipulate the scopes before they are validated against the protected resource. In this case, feel free to make changes:
Example:
async def my_destructure_scopes(scopes, *args, **kwargs):
return scopes.replace("|", ":")
@app.route("/protected/nonstandardscopes")
@scoped("foo|bar")
def scoped_sync_route(request):
return json({"nonstandardscopes": True})
Initialize(
app,
authenticate=lambda: True,
destructure_scopes=my_destructure_scopes)
9. extend_payload
¶
Purpose: It is a handler to allow the developer to modify the payload by adding additional claims to it before it is bundled up and packaged inside a JWT.
One of the most powerful concepts of the JWT is that you are able to pass data (aka claims) inside its payload for use by a client application, and reuse when that JWT is being returned for verification. It is simply a method that takes the existing payload
and returns it (with your brilliant modifications, of course)
Example:
async def my_extender(payload, user):
username = user.to_dict().get("username")
payload.update({"username": username})
return payload
Initialize(
app,
authenticate=lambda: True,
extend_payload=my_extender)
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.
from sanic_jwt import BaseEndpoint
class MagicLoginHandler(BaseEndpoint):
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=[
# The path will be relative to the url prefix (which defaults to /auth)
('/magic-login', MagicLoginHandler)
])
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 code, or ask a questionConfiguration
- 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. And, even though there are no plans (as of June 2018) to depracate this, some day it likely will be.
Endpoints and Responses¶
Sanic JWT sets itself up to run as a Sanic Blueprint at the /auth
path.
# Default
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:
# Custom
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
{
"me": {
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
.
Your custom Responses
should be hooked up to Sanic JWT using the responses_class
keyword argument on the Initialize
instance.
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, responses_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_response(
request,
access_token,
output,
refresh_token=refresh_token,
config=self.config)
return response
my_views = (
('/register', Register),
)
# Please note that this Initialize instance is incomplete.
# It is missing handlers for: authenticate, store_refresh_token, and
# retrieve_refresh_token.
# It is meant as illustrative purposes on how you might approach this.
# See https://github.com/ahopkins/sanic-jwt/issues/111 for more information.
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)
Microservices¶
One of the benefits of a lightweight framework like Sanic is that it makes building microservice architectures simple, and flexible. If you are building a microservice application, likely you do not want all of your services to have the /auth
endpoints!
Instead, you probably only want to authenticate against a single service, and use the token generated there among all your services. This can be easily accomplished with the auth_mode=True
Configuration. Set it to True
on your authentication service, and False
everywhere else. All the decorators will still work as expected.
# Authentication service
Initialize(app, authenticate=lambda: True)
# Every other service
Initialize(app, auth_mode=False)
Now, the /auth
endpoints are only on your authentication service, but the access token can be used on ANY of your other services.
Note
This works only if each of the services has the same secret
.
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)
Okay, great. You know what you are doing. It is still revommended that you NOT do this. Are you sure you know what you are doing?
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))
Custom Claims¶
Sometimes you may find a need to add claims to a JWT beyond what is built into Sanic JWT.
To do so, simply subclass Claim
and register them at initialization by providing the custom claim class in a list to custom_claims
.
from sanic_jwt import Claim, Initialize
MyCustomClaim(Claim):
key = 'foo'
def setup(self, payload, user):
return 'bar'
def verify(self, value):
return value == 'bar'
Initialize(..., custom_claims=[MyCustomClaim])
There are three attributes that a Claim
must have: key
, setup
, and verify
.
key
: The name of the claim and the key that will be inserted into the payload.setup
: A method to be run at the time the payload is created. It should return the value of the claim.verify
: A method to be run when a token is being verified. It should return a boolean
whether or not the claim has been met.Extra Verifications¶
Besides registering custom claims, sometimes you may find the need to do additional verifications on a payload. For example, perhaps you want to run checks that span more than one claim on the payload.
To accomplish this, you can register a list of methods (that each return a boolean
) at initialization by providing the list to extra_verifications
.
def check_number_of_claims(payload):
return len(payload.keys()) == 5
extra_verifications = [check_number_of_claims]
Initialize(
...,
extra_verifications=extra_verifications
)
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')
User level secrets¶
Sometimes, you may find it useful to have a different secret for every user of your application. One advantage of this could be the ability to invalidate any existing NON-expired access token for a single user, without interrupting any of your other users. Essentially, this could be equivalent to “logging out” (albeit, JWTs are by definition stateless, so it is only mimicking session based login/logout).
To implement this:
async def retrieve_user_secret(user_id):
return f"user_id|{user_id}"
Initialize(
app,
user_secret_enabled=True,
retrieve_user_secret=retrieve_user_secret,
)
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, app=app)
@bp.get('/users/<id>')
@protected(bp)
async def users(request, id):
...
Alternatively (and probably preferably), you can also access the decorator from the Initialize
instance. This makes it easier if you forget to pass the Blueprint
.
bp = Blueprint('Users')
sanicjwt = Initialize(bp, app=app)
@bp.get('/users/<id>')
@sanicjwt.protected()
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 configuration 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 stored for access tokencookie_refresh_token_name
- the name where the cookie is stored for refresh tokenInitialize(
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.
Cookie splitting, and suggested best practices
Sanic JWT comes with the ability to split the access token into two cookies. The reason would be to allow cookies to both (1) be secured from XSS, and (2) allow for browser clients to have access to the token and it’s payload.
Note
This is initially disabled, and is an opt-in feature. However, if your intent is to use Sanic JWT with a browser based application, and you want to have access to the payload on the client, then it is HIGHLY suggested that you use this method, and not Header tokens.
To use split cookies, you can enable it as follows:
Initialize(
app,
cookie_set=True,
cookie_split=True,
cookie_access_token_name='token-header-payload',)
This will split the cookie in two parts:
header.payload
signature
The first part will not have HttpOnly
set, but the signature part will. This keeps your token safe from being used since it cannot be verified by the backend. But, the payload can be accessible from JavaScript.
import jwtDecode from 'jwt-decode'
const payload = jwtDecode(getCookieValue('token-header-payload'))
Note
Setting this will override the cookie_httponly
configuration for the access token. Also, the above example sets cookie_access_token_name
, but it is not necessary. This is just to show that cookie_access_token_name
will control the name of the header.payload
cookie. To change the name of the signature
cookie, use cookie_split_signature_name
.
Query String Tokens¶
Sometimes, both header based authentication and cookie based authentication will not be enough. A third option is available to look for tokens inside query string arguments:
http://localhost?access_token=<JWT>
This can be enabled with query_string_set=True
. One potential use for this would be authentication of a websocket endpoint where sending headers and cookies may be more challenging due to Javascript limitations.
Warning
In most scenarios, it is advisable to not use query strings for authentication. One of the biggest reasons is that the tokens may be easily leaked if a URL is copied and pasted, or because the token may end up in server logs. However, the option is available if you need it and you feel comfortable that you can mitigate any risks.
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,)
This will now tell Sanic JWT to look for the cookie first. If it is not present, before throwing an exception, it will fallback and look for an Authorization
header.
Per view declaration¶
Perhaps you realize that you would like to make the declaration on a single view? Most of your views will operate using a cookie, but one particular endpoint (for whatever reason) will best be served to accept headers. Not a problem. You can simply pass in the configuration parameters right into the decorator!
Initialize(
app,
cookie_set=True,
cookie_strict=False,)
@app.route("/protected_by_header")
@protected(cookie_set=False)
async def protected_by_header_route(request):
...
Learn more about configuration overrides.
Note
This paradigm works for all configurations. Feel free to experiment and change config settings at the lowest level you might need them.
Advanced Decorators¶
Want to have a greater level of control? Instead of just importing the decorators from the sanic_jwt.decorators
module, you can also use the decorator directly off your initialized Sanic JWT instance!
sanicjwt = Initialize(app)
@app.route("/protected")
@sanicjwt.protected()
async def protected_route(request):
...
This also works for blueprints (and has the added advantage that you no longer need to pass the bp instance in.
bp = Blueprint('Users')
Initialize(bp, app=app)
@bp.get('/users/<id>')
@bp.protected()
async def users(request, id):
...
Note
This concept of having instance based decorators also works for the scoped
decorator: bp.scoped('foobar')
; and the @inject_user
decorator.
@inject_user
decorator¶
You’ve gone thru the hard work and added a retrieve_user
method. You might as well be able to reap the benefits by leveraging that method to inject your user data into your endpoints.
@app.route("/protected/user")
@inject_user()
@protected()
async def my_protected_user(request, user):
return json({"user_id": user.user_id})
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):
...
Note
If you provide a False
or None
value to the @scoped
decorator, it will effectively remove all protection. This means all requests, whether authenticated or not, will be accepted.
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. Every time an access token expires, the client needs to reauthenticate. Access tokens are generated, and sent to the client. They are not persisted. This means that they cannot be revoked.
Refresh tokens solve these two problems. 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 generated and sent to the client.
In a best practices scenario, refresh tokens and access tokens work together to provide a user friendly, yet secure, authentication environment.
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. Why? 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
)
Enable refresh tokens¶
Out of the box, Sanic JWT will not generate refresh tokens for you. If you want to make use of them, simply enable them as you would any other Configuration. The easiest is probably just to pass refresh_token_enabled
into Initialize
.
Initialize(
app,
authenticate=lambda: True,
refresh_token_enabled=True,)
Handlers¶
As mentioned, there are two required handlers you must create if you would like to provide refresh tokens to users.
1. 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 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)
2. 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. Our goal here is to enable the developer to build a more secure platform, not make decisions for them.
But, I really want one!¶
Okay, fine. If you really would like to issue a JWT, or any kind of token, you can. Sanic JWT gives you the control to override our default method of generating refresh tokens. Something like this would work:
import uuid
def generate_refresh_token(*args, **kwargs):
return str(uuid.uuid4())
Initialize(
...
generate_refresh_token=generate_refresh_token,
)
You as the developer have the control to issue whatever you would like. If you want that refresh token to be a JWT, go for it! You will need to generate it, and then validate it in the retrieve_refresh_token
handler. I’ll let the exercise be up to you, but feel free to post an issue and ask for help.
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)
But, it does not need to be a callable. This works too:
from sanic_jwt import Configuration
class MyConfiguration(Configuration):
set_access_token_name = 'jwt'
Initialize(
app,
configuration_class=MyConfiguration)
Okay … need to go even further? You can also have a setting evaluated on each request with the get_<setting>()
method:
auth_header_key = "x-authorization-header"
class MyConfig(Configuration):
def get_authorization_header(self, request):
if auth_header_key in request.headers:
return request.headers.get(auth_header_key)
return "authorization"
Initialize(
app,
configuration_class=MyConfig
)
This brings up an important point. If you go with the getter method, then in order to not waste resources, it will be evaluated only one time per request. The output of your getter will be cached for the lifespan of that request only.
As you can see, the getter method is passed the request
object as a parameter.
Temporary override¶
Sometimes you may need to only change a value for a brief moment. There is an auth.override()
method that allows you to handle these situations seemlesly. Anything executed inside the with
statement will have the modified value.
with app.auth.override(expiration_delta=1440):
token = app.auth.generate_access_token(user)
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
auth_mode
¶
/auth
endpoints or not. Helpful for microservice applications.True
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_expires
¶
expires
field to cookies. Should be a Python datetime
object, and therefore should be set in Python code and not via environment variables.None
cookie_httponly
¶
True
cookie_max_age
¶
max-age
field to the cookies. The number is expressed in seconds.0
cookie_path
¶
'/'
cookie_refresh_token_name
¶
'refresh_token'
cookie_secure
¶
secure
field to cookies. This should be used in production, but is disabled by default because it might lead to unintended frustrations in development.False
cookie_set
¶
True
.False
cookie_split
¶
True
, will enable split cookies (see protecting routes with cookies for more details). Overrides cookie_httponly
.False
cookie_split_signature_name
¶
cookie_split
turned on.access_token_signature
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
do_protection
¶
True
, unless you know what you are doing since it will effectively render the @protected
decorator useless and all traffic will be passed thru.True
expiration_delta
¶
60 * 5 * 6
, aka 30 minutesgenerate_refresh_token
¶
sanic_jwt.utils.generate_refresh_token
leeway
¶
60 * 3
, aka 3 minuteslogin_redirect_url
¶
/index.html
path_to_authenticate
¶
'/'
path_to_refresh
¶
'/refresh'
path_to_retrieve_user
¶
'/me'
path_to_verify
¶
'/verify'
private_key
¶
None
public_key
¶
Alias for secret
query_string_access_token_name
¶
'access_token'
query_string_refresh_token_name
¶
'refresh_token'
query_string_set
¶
True
.False
query_string_strict
¶
query_string_set
is enabled, an exception will be raised if the query string is not present. To allow for an authorization header to be used as a fallback, turn query_string_strict
to False
.True
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
¶
/
.'/auth'
user_id
¶
'user_id'
user_secret_enabled
¶
False
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.
- Deprecate
- get_refresh_token get_access_token retrieve_scopes
Version 1.3.2 - 2019-05-16¶
scoped
decorator for inline config changes outside of protected.Version 1.3.1 - 2019-04-25¶
False
and None
scopes.Version 1.3.0 - 2019-04-24¶
Version 1.2.2 - 2019-03-14¶
protected
decorator properly applied to built in views when initialized on a blueprintVersion 1.2.0 - 2018-08-06¶
SANIC_JWT_DO_PROTECTION
401
instead of 403
Version 1.1.4 - 2018-08-06¶
_do_protect
in @scoped
decoratorVersion 1.1.3 - 2018-08-06¶
exception
and reasons
keyreasons
in exception handling to be consistently formatted400
responses for debug
turned off, and 401
when turned onVersion 1.1.2 - 2018-06-18¶
Version 1.1.1 - 2018-06-14¶
args
and kwargs
to scope handlerVersion 1.1 - 2018-06-03¶
override_scope_validator
destructure_scopes
inject_user
Initialize
class for convenienceuser_id
from requestInitialize
Authentication.is_authenticated
to Authentication._check_authentication
Authentication.verify
to Authentication._verify
Authentication.get_access_token
to Authentication.generate_access_token
Authentication.get_refresh_token
to Authentication.generate_refresh_token
Authentication.retrieve_scopes
to Authentication.extract_scopes
abort
bug when using Sanic’s convenience method for exceptionsVersion 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.2?¶
Version 1.2 saw a a few minor fixes and changes. Some new features were added to allow custom claims and additional payload verifications. Version 1.1.4 is still stable, so there is no need to upgrade to 1.2.
In addition, there was a change that will (unfortunately) have some potential impact on users. Expired and invalid tokens will report as HTTP 401
.
What is new in Version 1.1?¶
The biggest changes are under the hood relating to how configuration settings are implemented. They are now fully dynamic allowing you to not only dynamically set them at run time, but also have them evaluated at the last minute to give you flexibility when needed.
Flexibility is really the name of the game for v. 1.1. Most of the features are to enable the developer that wants to dig deeper and gain more control. For example, the Authentication
now has a number of new renamed methods. Checkout the source code to see what they are (hint: they are the ones NOT with an _
at the beginning.)
Checkout the changelog for a more detailed description.
Note
It is recommended that you are at least using version 1.1.2. You can upgrade to version 1.1.4 for improved exception handling and some bug fixes.
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.