Package: djwto
Welcome todjwto!djwto ("jot two") is an alternative library offering support for JWT based authentication on top of the Django framework. Its main features are:Authentication either through aBearertoken orCookies.Accesstoken can be divided into two parts where one part is not encoded and can be used by the client (hence the lib name).CSRFprotection by default.Customizable. Add your own code when you see fit.Full Auth Layer: protect your views by requiring the JWT tokens to be present in the income request. Also available for the permissions layer.DocumentationCompletedocumentationis also available at ReadTheDocs.InstallationInstall it through pip directly:pipinstalldjwtoThen make it available in yourINSTALLED_APPS:INSTALLED_APPS=['django.contrib.auth','django.contrib.contenttypes',...'djwto']And for using its defaults urls, add it to yoururls.pyproject file:fromdjango.contribimportadminfromdjango.urlsimportpath,includeurlpatterns=[path('',include('djwto.urls')),]RequirementsPython (3.7, 3.8, 3.9)Django 3+OverviewContents:JsonOne-cookieTwo-cookiesSettingsEndpointsSignalsCustomizationProtecting ViewsContributing and Bugsdjwto offers 3 main ways to process the JWT tokens, which is defined by the settingsDJWTO_MODE.  Despite of the mode running, the tokens are always returned asacccessandrefresh.The first is intended to be short-lived and used more often whereas the second lives longer and is only sent on a specific path as defined by the settingDJWTO_REFRESH_COOKIE_PATH. Its purpose, as the name implies, is to refresh and create a newaccesstoken.Here's an overview of each mode available:JSONIn this setting the tokens are returned as a direct JSON response to the client. Here's an example using therequestslibrary, running on a simple demo Django project:importrequestssess=requests.Session()r=sess.post('https://localhost:8001/login/',data={'username':'alice','password':'pass'})r.json(){'refresh':'eyJ0eXAiO.eyJpc3MiOiJ.QXq8sbgIEgT','access':'eyJ0eXAiOi.eyJpc3MiOiJ.TtSnWdrhWuX'}In order to make further requests to the backend the client would have to grab the tokens and add them to theAUTHORIZATIONheader with theBearerpattern.ONE-COOKIEIn this mode, the tokens are returned in Cookies. Here's an example:sess=requests.Session()r=sess.post('https://localhost:8001/login/',data={'username':'alice','password':'pass'})sess.cookies<RequestsCookieJar[Cookie(name='csrftoken',value='DB6kR7o'),Cookie(name='jwt_access',value='eyJ0.eyJpc.kJsR'),Cookie(name='jwt_refresh',value='eyJ0e.eyJ.wWr')]>Notice we have:csrftokenjwt_accessjwt_refreshThe first must be sent on aheaderon views protected by CSRF and the last two are the ones used for the auth layer.TWO-COOKIESSimilar toONE-COOKIEbut this time theaccesstoken is divided in two pieces. Here's an example:importbase64sess=requests.Session()r=sess.post('https://localhost:8001/login/',data={'username':'alice','password':'pass'})sess.cookies<RequestsCookieJar[Cookie(name='csrftoken',value='N1vJ9D'),Cookie(name='jwt_access_payload',value='eyJhdWQiO.ZXJuYW1lIj.FsaWN'),Cookie(name='jwt_access_token',value='eyJ0eXAi.OiJKV1QiLC.JhbGciOiJIU'),Cookie(name='jwt_refresh',value='eyJ0eXA.iOiJKV1Qi.LCJhbGc')]>base64.b64decode(sess.cookies['jwt_access_payload'])b'{"aud": "aud", "exp": "2021-06-18T02:32:55.144", "iat": "2021-06-17T18:12:55.144", "iss": "iss", "jti": "0b2d199d-f233-4203-bdab-693c03bca505", "refresh_iat": 1623953575, "sub": "sub", "type": "access", "user": {"id": 1, "perms": [], "username": "alice"}}'Now we have the following cookies:csrftokenjwt_access_payloadjwt_access_tokenjwt_refreshNotice the access token now have two components: payload and the token itself. The payload is the original payload encoded inbase64which can be used by the client receiving those cookies. The other part though is protected from javascript access and should only be used by the backend.SettingsHere's an overview of all settings available for djwto:DJWTO_ISS_CLAIM:Optional[str]=getattr(settings,'DJWTO_ISS_CLAIM',None)DJWTO_SUB_CLAIM:Optional[str]=getattr(settings,'DJWTO_SUB_CLAIM',None)DJWTO_AUD_CLAIM:Optional[Union[List[str],str]]=getattr(settings,'DJWTO_AUD_CLAIM',None)DJWTO_IAT_CLAIM:bool=getattr(settings,'DJWTO_IAT_CLAIM',True)DJWTO_JTI_CLAIM:bool=getattr(settings,'DJWTO_JTI_CLAIM',True)DJWTO_ALLOW_REFRESH_UPDATE:bool=getattr(settings,'DJWTO_ALLOW_REFRESH_UPDATE',True)DJWTO_ACCESS_TOKEN_LIFETIME=getattr(settings,'DJWTO_ACCESS_TOKEN_LIFETIME',timedelta(minutes=5))DJWTO_REFRESH_TOKEN_LIFETIME=getattr(settings,'DJWTO_REFRESH_TOKEN_LIFETIME',timedelta(days=1))DJWTO_NBF_LIFETIME:Optional[timedelta]=getattr(settings,'DJWTO_NBF_LIFETIME',timedelta(minutes=0))DJWTO_SIGNING_KEY:str=getattr(settings,'DJWTO_SIGNING_KEY',os.environ['DJWTO_SIGNING_KEY'])# Only set if Algorithm uses asymetrical signing.DJWTO_VERIFYING_KEY:Optional[str]=getattr(settings,'DJWTO_VERIFYING_KEY',None)DJWTO_ALGORITHM:str=getattr(settings,'DJWTO_ALGORITHM','HS256')DJWTO_MODE:Literal['JSON','ONE-COOKIE','TWO-COOKIES']=getattr(settings,'DJWTO_MODE','JSON')DJWTO_REFRESH_COOKIE_PATH:str=getattr(settings,'DJWTO_REFRESH_COOKIE_PATH','api/token/refresh')DJWTO_SAME_SITE:str=getattr(settings,'DJWTO_SAME_SITE','Lax')DJWTO_DOMAIN:Optional[str]=getattr(settings,'DJWTO_DOMAIN',None)DJWTO_CSRF:bool=getattr(settings,'DJWTO_CSRF',True)For a thorough view of each please refer to the officialdocs.EndpointsHere's an overview of each endpoint offered by djwto and how to use them. For each endpoint, if something goes wrong then a keyerrorshould be available in the response and its value should be an explanation for the event./login/Simply POST to this endpoint with data containing the userusernameand their inputpassword, the return response, if valid, should contain the newly created JWTs for the client.The return type depends on which mode djwto is running; here's an example onDJWTO_MODE=JSONmode making use of therequestslibrary:importrequestssess=requests.Session()sess.verify=False# For testing locallyr=sess.post('https://localhost:8001/login/',data={'username':'alice','password':'pass'})r.json(){'refresh':'eyJ0eXAiO.eyJpc3MiOiJ.QXq8sbgIEgT','access':'eyJ0eXAiOi.eyJpc3MiOiJ.TtSnWdrhWuX'}/validate/Sometimes the frontend may want to confirm whether a given token is still valid or not; that's the purpose of this endpoint. Supposing that theloginprocess has already taken place, here's an example ofaccesstoken validation on modeJSON:importrequestssess=requests.Session()sess.verify=False# For testing locallysess.headers.update({'AUTHORIZATION':f'Bearer{r.json()["access"]}'})r=sess.post('https://localhost:8001/validate_access/')print(r.json()){'msg':'Token is valid'}Here's an example forTWO-COOKIES:importrequestssess=requests.Session()sess.verify=False# For testing locallysess.post('https://localhost:8001/login/',data={'username':'alice','password':'pass'})sess.headers.update({'X-CSRFToken':sess.cookies['csrftoken']})r=sess.post('https://localhost:8001/validate_access/',headers={'REFERER':'https://localhost:8001'})Notice that theCSRFtoken is being sent in the header asX-CSRFTokenand also there's aREFERERvalue indicating from where the client is coming (this is mandatory as by Django's built-in csrf techniques protection).In order to validate therefresh, here's an example:importrequestssess=requests.Session()sess.verify=False# For testing locallysess.post('https://localhost:8001/login/',data={'username':'alice','password':'pass'})sess.headers.update({'X-CSRFToken':sess.cookies['csrftoken']})r=sess.post('https://localhost:8001/api/token/refresh/validate_refresh/',headers={'REFERER':'https://localhost:8001'},data={'jwt_type':'refresh'})Notice that thePOSTmust send the datajwt_type='refresh'to specify for the backend which token to use./refresh_access/Theaccesstoken is designed to be short-lived, that is, it grants access for clients for a brief period of time before it goes expired. The reasoning is that after it expires the API has a chance to validate whether the client can continue receiving new tokens or not (so in case the client logged out or was blacklisted for some reason they'd lose access thereafter).When the token expires, a new one can be obtained by posting the refresh token to this endpoint. Here's an example forJSONmode:importrequestssess=requests.Session()sess.verify=False# For testing locallyr=sess.post('https://localhost:8001/login/',data={'username':'alice','password':'pass'})sess.headers.update({'AUTHORIZATION':f'Bearer{r.json()["refresh"]}'})r=sess.post('https://localhost:8001/api/token/refresh/refresh_access/',headers={'REFERER':'https://localhost:8001'})print(r.json()){"refresh":...,"access":...}/update_refresh/At some scenarios it may be interesting for the JWT auth process to also be able to update therefreshtoken. This may occur for instance in an eCommerce environment when the customer is finishing the purchase process and may get blocked due expired token (which is highly undesirable). In order to allow this feature to be available, setsettings.DJWTO_ALLOW_REFRESH_UPDATEtoTrue. Here's an example forJSONmode:importrequestssess=requests.Session()sess.verify=False# For testing locallyr=sess.post('https://localhost:8001/login/',data={'username':'alice','password':'pass'})sess.headers.update({'AUTHORIZATION':f'Bearer{r.json()["refresh"]}'})r=sess.post('https://localhost:8001/api/token/refresh/update_refresh/')print(r.json()){"refresh":'...',"access":'...'}/logout/When logging a user out, ifJTIis available then the tokens will be blacklisted. In either case, the tokens are deleted (bothaccessandrefresh). The requestpathmust containsettings.DJWTO_REFRESH_COOKIE_PATH. Here's an example forJSONmode:importrequestssess=requests.Session()sess.verify=False# For testing locallyr=sess.post('https://localhost:8001/login/',data={'username':'alice','password':'pass'})sess.headers.update({'AUTHORIZATION':f'Bearer{r.json()["refresh"]}'})r=sess.post('https://localhost:8001/api/token/refresh/logout/')print(r.content)b'{"msg": "Token successfully blacklisted."}'For eitherONE-COOKIEorTWO-COOKIES:importrequestssess=requests.Session()sess.verify=False# For testing locallyr=sess.post('https://localhost:8001/login/',data={'username':'alice','password':'pass'})sess.headers.update({'X-CSRFToken':sess.cookies['csrftoken']})r=sess.post('https://localhost:8001/api/token/refresh/logout/',headers={'REFERER':'https://localhost:8001'},data={'jwt_type':'refresh'})print(r.content)b'{"msg": "Token successfully blacklisted."}'r=sess.delete('https://localhost:8001/api/token/refresh/logout/',headers={'REFERER':'https://localhost:8001'},data={'jwt_type':'refresh'})print(r.content)b'{"msg": "Tokens successfully deleted."}'Notice that the verbDELETEis also available with removes the cookies from the response. This option only works on for the cookie-based settings.If after blacklisting a token a request is sent for updating eitheraccessorrefresh, the process should fail:r=sess.post('https://localhost:8001/api/token/refresh/update_refresh/',headers={'REFERER':'https://localhost:8001'},data={'jwt_type':'refresh'})print(r.content)b'{"error": "Can\'t update refresh token."}'r=sess.post('https://localhost:8001/api/token/refresh/refresh_access/',headers={'REFERER':'https://localhost:8001'},data={'jwt_type':'refresh'})print(r.content)b'{"error": "Can\'t update access token."}'Signalsdjwto offers by default a set of signals that you can use in your projects for tracking down when certain events take place. In order to demonstrate how it works, consider a regular Django project created with an app calledtestapp.Here's a list of all available signals offered by the package:/jwt_logged_in/The signal is triggered when a successfull loggin happens. Here's how to connect it:defready(self):importdjwto.signalsassignalsdefhandler(sender,**kwargs):print('sender: ',sender)print('kwargs: ',kwargs)signals.jwt_logged_in.connect(handler,sender='GetTokensView')On each successfull login the new functionhandlerwill process with appropriate input arguments./jwt_login_fail/The signal is triggered when a loggin fails by any reason. Here's how to connect it:defready(self):importdjwto.signalsassignalsdefhandler(sender,**kwargs):print('sender: ',sender)print('kwargs: ',kwargs)signals.jwt_login_fail.connect(handler,sender='GetTokensView')/jwt_blacklisted/The signal is sent when a token is successfully blacklisted:defready(self):importdjwto.signalsassignalsdefhandler(sender,**kwargs):print('sender: ',sender)print('kwargs: ',kwargs)signals.jwt_blacklisted.connect(handler,sender='BlackListTokenView')/jwt_token_validated/The signal is sent each time a validation request is processed:defready(self):importdjwto.signalsassignalsdefhandler(sender,**kwargs):print('sender: ',sender)print('kwargs: ',kwargs)signals.jwt_token_validated.connect(handler,sender='ValidateTokensView')/jwt_access_refreshed/The signal is sent when theaccesstoken is successfully refreshed:defready(self):importdjwto.signalsassignalsdefhandler(sender,**kwargs):print('sender: ',sender)print('kwargs: ',kwargs)signals.jwt_access_refreshed.connect(handler,sender='RefreshAccessView')/jwt_refresh_updated/If the updating endpoint is available (as by thesettings) then when the updating of the refresh successfully happens this signal is sent. Example:defready(self):importdjwto.signalsassignalsdefhandler(sender,**kwargs):print('sender: ',sender)print('kwargs: ',kwargs)signals.jwt_refresh_updated.connect(handler,sender='UpdateRefreshView')Customizationdjwto offers the possibility for the client to customize how parts of the code should be processed, replacing the original logic. Let's suppose a regular Django project with an app calledtestapp.It's possible to specify customizations for djwto when the app isready. For instance, if your project requires to also bring the customer's email when the JWT creation is running, here's one way of doing it:fromdjango.appsimportAppConfigclassTestappConfig(AppConfig):default_auto_field='django.db.models.BigAutoField'name='testapp'defready(self):importdjwto.tokensastokensdefnew_process_user(user):return{user.USERNAME_FIELD:user.get_username(),'email':user.email,'id':user.pk,'perms':tokens.process_perms(user)}tokens.process_claims=new_process_userRunning the loggin process forTWO-COOKIES, we get now:importrequestsimportbase64sess=requests.Session()sess.verify=False# For testing locallyr=sess.post('https://localhost:8001/login/',data={'username':'alice','password':'pass'})base64.b64decode(sess.cookies['jwt_access_payload'])b'{"aud": "aud", "exp": 1624259339, "iat": 1624229339, "iss": "iss", "jti": "900f4f1a-3e0f-4843-9997-9fd8d032684e", "refresh_iat": 1624229339, "sub": "sub", "type": "access", "user": {"email": "alice@djwto.com", "id": 1, "perms": [], "username": "alice"}}'Now theuserkey retrieves the user email as well.Feel free to customize the code as you see fit.Protecting Viewsdjwto offers decorators that can be used on views in order to add the authentication layer protection. There are two functions for doing so:jwt_login_requireddjwto offers the decoratorjwt_loging_requiredfor guaranteeing a view to only be processed if the required and valid JWT token was sent in the request. Here's an example. Suppose again a regular Django project with the usualtestappwith a view defined as:importdjwto.authenticationasauth# type: ignorefromdjango.viewsimportViewfromdjango.utils.decoratorsimportmethod_decoratorfromdjango.http.responseimportHttpResponseclassProtectedView(View):defdispatch(self,request,*args,**kwargs):returnsuper().dispatch(request,*args,**kwargs)@method_decorator(auth.jwt_login_required)defget(self,request,*args,**kwargs):refresh_claims=request.payloadprint(refresh_claims)returnHttpResponse('worked!')Notice the decoratorauth.jwt_login_requiredprotecting the view. Now let's see what happens if we send a request without the JWT available:r=sess.get('https://localhost:8001/testapp/protect/')r.contentb'{"error": "Cookie\\"jwt_access_token\\" cannot be empty."}'If we properly login:r.contentb'worked!'jwt_perm_requireddjwto also offers the possibility of protecting views with permissions that should be available in the JWT token. Here's an example: decorate a view with thejwt_perm_requiredfunction like so:classPermsProtectedView(View):defdispatch(self,request,*args,**kwargs):returnsuper().dispatch(request,*args,**kwargs)@method_decorator(auth.jwt_login_required)@method_decorator(auth.jwt_perm_required(['perm1']))defget(self,request,*args,**kwargs):refresh_claims=request.payloadprint(refresh_claims)returnHttpResponse('perms also worked!')The function receives a list of permissions as input and only if the input JWT token contains those permissions is that the view will be processed.Now, sending the request with a regular JWT token returns:r=sess.get('https://localhost:8001/testapp/perms_protect/')r.contentb'{"error": "Invalid permissions for jwt token."}'Suppose now thatAlicehas the permissionperm1stored in the database. Here's the result then:r.contentb'perms also worked!'If you run these examples with settingsDJWTO_MODE=TWO-COOKIES, you'll be able to see what's inside the returned cookie, like so:base64.b64decode(sess.cookies['jwt_access_payload'])b'{"aud": "aud", "exp": 1624269024, "iat": 1624239024, "iss": "iss", "jti": "0e9bfcdc-d684-47b5-9677-0cb5e5e88893", "refresh_iat": 1624239024, "sub": "sub", "type": "access", "user": {"email": "alice@djwto.com", "id": 1, "perms": ["perm1"], "username": "alice"}}'Contributing and BugsContributions are very welcome! If you want to send a PR please consider first discussing your implementation on an issue.Also, if you find bugs (this is still an alpha project!) please let us know by also opening an issue on the official repository.