JSON Web Token Authentication support for Django REST Framework

Overview

REST framework JWT Auth


Notice

This project is currently unmaintained. Check #484 for more details and suggested alternatives.


build-status-image pypi-version

JSON Web Token Authentication support for Django REST Framework

Full documentation for the project is available at docs.

Overview

This package provides JSON Web Token Authentication support for Django REST framework.

If you want to know more about JWT, check out the following resources:

Requirements

  • Python (2.7, 3.3, 3.4, 3.5, 3.6)
  • Django (1.8, 1.9, 1.10, 1.11)
  • Django REST Framework (3.1, 3.2, 3.3, 3.4, 3.5, 3.6)

Installation

Install using pip...

$ pip install djangorestframework-jwt

Documentation & Support

Full documentation for the project is available at docs.

You may also want to follow the author on Twitter.

Comments
  • Add refresh token feature

    Add refresh token feature

    Added an optional endpoint that can refresh a token. This can be useful for a webapp to try to keep a user logged in without having to re-enter their password constantly. Got the idea from this post https://auth0.com/blog/2014/01/27/ten-things-you-should-know-about-tokens-and-cookies/

    Basically, if JWT_ALLOW_TOKEN_RENEWAL is True, then an extra field orig_iat (stands for original issue at time) is stored on the returned JWT token. You can then pass the token to the RefreshJSONWebToken view, and if orig_iat hasn't yet expired, you get a token with a refreshed expiration time (but with the same orig_iat, so you can only do refreshes up to some time limit, specified in JWT_TOKEN_RENEWAL_LIMIT). Getting a brand new token (by passing in username/password) will return a completely new orig_iat though.

    I also added a new handler, jwt_get_user_id_from_payload, that lets you specify how the user_id field is stored. Since it's possible for the custom payload handler to change how the user id is stored, it seems to make sense to have a way to retrieve it too.

    Feedback welcome (there are some things that could probably be cleaned up). I didn't add tests yet but if there is interest in this PR I will write some (I did write a number of tests in my application that uses this, so I am reasonably confident it works).

    opened by alvinchow86 32
  • request.user shows AnonymousUser in middleware

    request.user shows AnonymousUser in middleware

    I'm not sure if this is an issue or just because of the implementation, but I was using middleware to log user_id against records being edited, and I couldn't get request.user at the middleware level (and so of course request.user.is_authorized() returned False).

    The middleware was the last one on the list, and authorization was working (I would get a 403 requesting assets without my token header). I tried putting 'rest_framework_jwt.authentication.JSONWebTokenAuthentication' at the top of DEFAULT_AUTHENTICATION_CLASSES, and also making it the only item in the tuple; neither worked.

    I've managed to work around my issue by accessing the header and using the tools directly in the app to retrieve the user; essentially I copied chunks of your code into my middleware, but I thought you should know :)

    If you're interested, here's the middleware:

    from django.db.models import signals
    from django.utils.functional import curry
    from django.utils import timezone
    from threading import local
    
    import jwt
    from rest_framework.authentication import get_authorization_header
    from rest_framework_jwt.settings import api_settings
    from django.conf import settings
    try:
        from django.contrib.auth import get_user_model
    except ImportError:  # Django < 1.5
        from django.contrib.auth.models import User
    else:
        User = get_user_model()
    
    _user = local()
    
    class AuditMiddleware(object):
        def process_request(self, request):
            if not request.method in ('GET', 'HEAD', 'OPTIONS', 'TRACE'):
                if hasattr(request, 'user') and request.user.is_authenticated():
                    user = request.user
                else:
                    user = self.get_user_from_auth_header(request)
                    if user is not None:
                        request.user = user
    
                _user.value = user
    
                mark_whodid = curry(self.mark_whodid, _user.value)
                signals.pre_save.connect(mark_whodid,  dispatch_uid = (self.__class__, request,), weak = False)
    
        def process_response(self, request, response):
            signals.pre_save.disconnect(dispatch_uid =  (self.__class__, request,))
            return response
    
        def mark_whodid(self, user, sender, instance, **kwargs):
            # user logging here
    
        def get_user_from_auth_header(self, request):
            try:
                auth_keyword, token = get_authorization_header(request).split()
                jwt_header, claims, signature = token.split('.')
    
                try:
                    payload = api_settings.JWT_DECODE_HANDLER(token)
                    try:
                        user_id = api_settings.JWT_PAYLOAD_GET_USER_ID_HANDLER(payload)
    
                        if user_id:
                            user = User.objects.get(pk=user_id, is_active=True)
                            return user
                        else:
                            msg = 'Invalid payload'
                            return None
                    except User.DoesNotExist:
                        msg = 'Invalid signature'
                        return None
    
                except jwt.ExpiredSignature:
                    msg = 'Signature has expired.'
                    return None
                except jwt.DecodeError:
                    msg = 'Error decoding signature.'
                    return None
            except ValueError:
                return None
    
    opened by marcelkornblum 30
  • `NotImplementedError` on accessing `request.DATA`

    `NotImplementedError` on accessing `request.DATA`

    From django-rest-framework v3.2, any attempts to access request.DATA will raise a NotImplementedError with the following message:

    request.DATA has been deprecated in favor of request.data since version 3.0, and has been fully removed as of version 3.2.

    You can see the change commit here. The error originates here in views.py.

    Note that the request.data property appears to have been added in django-rest-framework v3, so simply changing request.DATA to request.data may make this package incompatible with earlier versions.

    opened by ghost 20
  • Add support for Django 1.10 and Django REST Framework 3.4

    Add support for Django 1.10 and Django REST Framework 3.4

    This branch takes a slightly different approach to 1.10/3.4 support than #252. It includes:

    • Updated documentation indicating change in support.
    • Add 1.10 and 3.4 to the Travis build matrix and tox file.
    • Consolidates the test views into tests/urls.py. 1.10 wasn't working with urlpatterns in the test_views and test_authentication files.
    • Tests the is_active behavior for the pre-1.10 ModelBackend and uses the new AllowAllUsersModelBackend to test the similar behavior for 1.10.
    • Turns on verbose for tox execution to show what is skipped for Travis builds.

    There is one oddity in this branch that I can't really explain well. test_jwt_login_custom_response_json did a check for the username with:

    self.assertEqual(response.data['user'], self.username)
    

    That data did not exist under 1.10 and I don't understand why. Someone with a bit more history with the project might want to check if that's ok or a behavioral regression that needs to be fixed first.

    opened by mblayman 16
  • Long running refresh tokens

    Long running refresh tokens

    related to #117 and #94

    But change the way we obtain a new JWT. Compare to #94, now the implementation try to be more compliant with https://auth0.com/docs/refresh-token

    The delegation endpoint is a POST, with the following body.

    {
      "client_id":       "YOUR_CLIENT_ID",
       "grant_type":      "urn:ietf:params:oauth:grant-type:jwt-bearer",
       "refresh_token":   "YOUR_REFRESH_TOKEN",
       "api_type":        "app"
    }
    

    instead of being a POST + empty body + the refresh token passed in Authorization header.

    /cc @fxdgear

    TODO before merging:

    • [ ] provide a squashed migration
    opened by ticosax 16
  • Wrong behavior if a field is empty

    Wrong behavior if a field is empty

    If authenticating using a recent JWT, the request fails with

    {"detail": "Invalid payload"}
    

    in case a field is empty (tested here with user account that was lacking an email address)

    opened by arnuschky 13
  • Allow using a cookie as a transport for the token

    Allow using a cookie as a transport for the token

    A new configuration 'JWT_AUTH_COOKIE' has been added. If it set to None (the default), everything works the same.

    It can be set to a string to use as the name of the cookie that can be used to carry the token. Note the 'Authorization' header is still accepted as the primary method, but it falls back to the designated cookie if present.

    When a token is requested and 'JWT_AUTH_COOKIE' is set, the response will set the given cookie with the token string.

    Fix #120

    opened by moises-silva 11
  • Refresh token not working

    Refresh token not working

    I'm using all the default settings (including the default URLS), and using CURL to refresh the token but I get the following error:

    {"non_field_errors":["orig_iat field is required."]}

    Any idea why this error is occurring?

    opened by mtmithani 11
  • Added a JWT verification view

    Added a JWT verification view

    This PR adds a verification view: verify_jwt_token, which checks if a JWT POSTed to it is valid and that the user exists.

    Much of the functionality is the same as the existing refresh view and serializer. I abstracted out the majority of the shared serializer logic to maximise DRYness. There are several other ways this could be implemented, however.

    I added tests for the new view and moved a test which was more relevant to this functionality from the refresh view tests.

    Suggestions for improvements welcome! If the serializer implementation is ok, it could probably use some unit tests.

    opened by Jwpe 11
  • Fix refresh tokens

    Fix refresh tokens

    Trying to get an new token, with an 'expired' token results in an signature error. The token is decoded with verification of 'exp' claim. Only the orig_iat claim should be verified in this case.

    Problem is the JWT_DECODE_HANDLER does not have an option to disable exp verification. Changing the default handler would be an backwards incompatible change... not sure how you guys would want to handle that. So in this pull it has it's own call to jwt.encode...

    Fixes #249

    opened by jor-rit 9
  • Need user id with token

    Need user id with token

    I am stuck with the JWT, can any one tell me how can I get user ID along with token.

    My url.py :

    from webservices import views
    
    router = routers.DefaultRouter()
    router.register(r'api/users', views.UserViewSet)
    
    
    urlpatterns = [
    url(r'^admin/', include(admin.site.urls)),
    url(r'^api/login/', obtain_jwt_token),
    ]
    
    opened by aashishsoni-zz 9
  • Cookie not removed in request when response is 401

    Cookie not removed in request when response is 401

    I'm using JWT in a httpOnly cookie and allowing multiple logins on the system. If I have 2 sessions opened with the same user (different JWT tokens) and if one of them logs out I reset all JWT tokens by changing the user's UUID. I also delete that session's cookie by means of:

            response = HttpResponse()
            response.delete_cookie("cookie.jwt",path="/")
    

    This logs out both browser sessions and that's OK, but the browser session in which I DID NOT explicitly log out keeps an invalid cookie in the browser and I can't get rid of it via javascript because its httpOnly (I want it to stay that way). All further requests to the server return as a 401 and I can't seem to change the response to add a "delete_cookie".

    Two questions:

    1. Why not always delete the cookie JWT_AUTH_COOKIE from the response if an exception is raised by JWT?

    2. How can I work around this issue?

    Thanks!

    opened by pedroflying 0
  • Can we use allauth only for the things, where we can get involved with email verification, and jwt for others purposes like login/get api request/ post api requests....????

    Can we use allauth only for the things, where we can get involved with email verification, and jwt for others purposes like login/get api request/ post api requests....????

    I am very confused about using allauth/jwt? I need email verification for account activation/deletion and many other things but i also need jwt token like thing to secure get/post apis.... What can i do???? I am beginner to django....!!!!!!! Sorry if my english is poor....!!!

    opened by SohaibShafiq1 0
  • Unreachable code when user is_active is False in.

    Unreachable code when user is_active is False in.

    https://github.com/jpadilla/django-rest-framework-jwt/blob/4021e0bd46eed8ca7dd0ea9337e2606951cb55fb/rest_framework_jwt/serializers.py#L53

    When using the default ModelBackend in django settings activated, the authenticate method in django.contrib.auth.backends.ModelBackend has the user_can_authenticate method. In our case user.is_active is False and as a result the whole authenticate method in this file returns None. As a result in line 53 there is a check if the user is_active but it is unreachable in this point and we render the wrong message which is in line 64.

    opened by tzimos 1
  • DeprecationWarning: The following fields will be removed in the future: `email` and `user_id`.

    DeprecationWarning: The following fields will be removed in the future: `email` and `user_id`.

    Hello,

    I am using email instead of username for authentication. So my custom user model does not have username field. My code is based on this

    Now I am setting up jwt authentication and I receive the following warning:

    rest_framework_jwt/utils.py:39: DeprecationWarning: The following fields will be removed in the future: `email` and `user_id`.
        DeprecationWarning
    

    Is there something that I can do to overcome this (other than using username, which will not be an option)

    thanks!

    opened by ajendrex 1
  • Status

    Status

    Hello there,

    For a long time now I've honestly thought I'd have the time and energy to come back and work on this project, and I think I still don't. I've not worked on any project recently needing it, which makes it harder to find time to come back.

    At some point I also noticed I no longer liked how the project had evolved and that it deserved a fair redesign and there are a few fundamental changes required. Now I'm certain that I wouldn't be able to work on this with the care required for a major version bump.

    I'm definitely not proud at how long it has taken me to make this or any update, I recognize that and I'm sorry for not having spoken out sooner.

    There's two efforts/projects that I'd like to point out to:

    Fork: https://github.com/Styria-Digital/django-rest-framework-jwt Drop-in updated replacement fork. I'm willing to transfer repo, pypi, etc, to keep this going.

    Alternative: https://github.com/davesque/django-rest-framework-simplejwt Many of the changes that I've wanted to work on are already done here. This is what I would probably use today.

    Learned quite a few things from this. Thanks to everyone, including past collaborators and contributors. ❤️

    Update:

    I've transferred the repository from @GetBlimp. Neither @GetBlimp nor @gcollazo are involved with this project anymore.

    discussion 
    opened by jpadilla 12
  • How to use this library by only using Http Only Cookie?

    How to use this library by only using Http Only Cookie?

    After using JWT token in un unsafe way for a little over an year I've finally decided that I would like to fix my current setup.

    I read everywhere that is not good to save a JWT token in the local client and that is best to use Http Only Cookie.

    I'm now trying to use JWT_AUTH_COOKIE in order to create an Http Only Cookie. I'm getting the Cookie correctly returned by the server when using getToken API. What I'm wondering now, is how I can refresh the token.

    What happens when I call refreshToken I get the following response:

    {"token":["This field is required."]}
    

    True, I'm not sending any token in the request's HEADER and that is what I want since the client isn't supposed to keep it saved anywhere.

    And that is where I'm getting confused:

    If i'm not wrong from now on every request the client does to the server, the cookie should be added to the request.

    Shouldn't the server check the cookie after it sees that no token has been passed in the Header?

    opened by pinkynrg 1
Releases(1.11.0)
  • 1.11.0(Jun 23, 2017)

    Changelog

    Added

    • Test on Django 1.11 #325 by @orf
    • Allow jtw_payload_handler to work with User models that don't have an Email field #268 by @shanx

    Changes

    • Bump up PyJWT to 1.5.2 636539eb9452c415bbd53094186ee45d56473422
    • Don't require the 'token' key to override jwt_response_payload_handler #323 by @brianrower

    Docs

    • Fix typo in jwt_get_secret_key doc #343 by @blueyed
    Source code(tar.gz)
    Source code(zip)
  • 1.10.0(Mar 22, 2017)

    Changelog

    Added

    • Update of django, drf, python version handling in tox.ini #262 by @angvp
    • Allow using a cookie as a transport for the token #275 by @moises-silva | Docs
    • Allow secret to be kept on user model #310 by @jacoor | Docs

    Changes

    • Replace login with log in when used as a verb #295 by @rriehle

    Docs

    • Minor typo and formatting correction index.md #293 by @matthewhegarty
    • Minor fix: formatting for phrase brand-new #301 by @sumittada
    Source code(tar.gz)
    Source code(zip)
  • 1.9.0(Dec 1, 2016)

  • 1.8.0(Apr 3, 2016)

    This release drops official support for Django REST Framework version 2.x as well as end of life versions of Django.

    Support versions are now Django 1.8 and 1.9 with Django REST Framework 3.x.

    We also add support for custom user models with Django 1.8's UUIDField as primary key.

    Source code(tar.gz)
    Source code(zip)
  • 1.7.1(Aug 18, 2015)

  • 1.7.0(Aug 17, 2015)

    Changelog

    Changed

    • Upgrade PyJWT to v1.4.0. 9a43d6303acfdd0724468cbb95a72bde03f38bff
    • Rely on get_by_natural_key instead of fetching by user's id. a3b4d44d2fc34d7752793ccbade2684707bf41da
    • Include optional orig_iat in jwt_payload_handler. eb208893a223686efbf52ef81c37b915cc2edc00
    • Remove throttle override in JSONWebTokenAPIView. #138

    Fixed

    • Use correct input type for password form field. Fixes #133
    • Use USERNAME_FIELD in utils.jwt_payload_handler. Fixes #128

    Added

    • Add support for DRF 3.2
    • Add JWT_PAYLOAD_GET_USERNAME_HANDLER setting. a3b4d44d2fc34d7752793ccbade2684707bf41da
    • Add username to jwt_payload_handler. a3b4d44d2fc34d7752793ccbade2684707bf41da
    • Add deprecation warning to jwt_payload_handler. a3b4d44d2fc34d7752793ccbade2684707bf41da
    • Add deprecation warning to jwt_get_user_id_from_payload_handler. a3b4d44d2fc34d7752793ccbade2684707bf41da
    Source code(tar.gz)
    Source code(zip)
  • 1.6.0(Jun 12, 2015)

    Changelog

    Changed

    • Speed up tests. #108
    • Bump up PyJWT version to v1.3.0. 54048f751ec00d73abf110a231fca8f2340156ef
    • Drop support for Python 3.2. dfb320885e53b8ce988d3f2c6d4535488354b5d6
    • Added the ability to use custom renderers. #121

    Fixed

    • Verify now does not refresh token. #109
    • Fixed testing DRF 2 with Django 1.8. #118
    • Run tests only against latest DRF versions. #122

    Added

    • Add request to serializers' context. #119

    tgif

    Source code(tar.gz)
    Source code(zip)
  • 1.5.0(Apr 28, 2015)

    Changelog

    Fixed

    • Catch InvalidTokenError and raise exception #100
    • Fixed verify_expiration no longer supported by PyJWT #103
    • Add Python 3.2 to tox tests #95
    • Propagate request arg in all the doc strings #90
    Source code(tar.gz)
    Source code(zip)
  • 1.4.0(Mar 19, 2015)

  • 1.3.0(Mar 7, 2015)

    • Update en_US PO file by @migonzalvar. #83
    • Allow subclassing JSONWebTokenAuthentication by @cancan101. #80
    • Allow setting audience and issuer by @cancan101. #77
    • Added a JWT verification view by @Jwpe. #75
    • Support HyperlinkedModelSerializer by @semente. #73
    • Added JWT TestCase Class and Client by @davideme. #72

    Thanks to all that contributed to make this release happen.

    Source code(tar.gz)
    Source code(zip)
  • 1.2.0(Jan 24, 2015)

    • Changed potentially misleading error message by @skolsuper. #59
    • Added JWT_RESPONSE_PAYLOAD_HANDLER by @erichonkanen. #62
    • Fixed user import problem for custom users by @cenkbircanoglu. #70
    • Added translation utils by @migonzalvar. #68

    Thanks to all that contributed to make this release happen.

    Thank You

    Source code(tar.gz)
    Source code(zip)
  • 1.1.1(Dec 11, 2014)

  • 1.1.0(Dec 3, 2014)

  • 1.0.2(Oct 23, 2014)

  • 1.0.1(Sep 3, 2014)

  • 1.0.0(Aug 30, 2014)

    I've just released v1.0.0 to PyPI. This release should not introduce any breaking changes but made sense the package's major version was correctly incremented.

    This introduces one new feature: Refresh Tokens. This feature was introduced in PR #23 by @alvinchow86 and also introduces some fixes by @liamlin.

    A typical use case for might be a web app where you'd like to keep the user "logged in" the site without having to re-enter their password, or get kicked out by surprise before their token expired. Imagine they had a 1-hour token and are just at the last minute while they're still doing something. With mobile you could perhaps store the username/password to get a new token, but this is not a great idea in a browser. Each time the user loads the page, you can check if there is an existing non-expired token and if it's close to being expired, refresh it to extend their session. In other words, if a user is actively using your site, they can keep their "session" alive.

    This release also introduces a new setting: JWT_AUTH_HEADER_PREFIX. This allows you to modify the Authorization header value prefix that is required to be sent together with the token. The default value is JWT. This decision was introduced in PR #4 to allow using both this package and OAuth2 in DRF.

    Another common value used for tokens and Authorization headers is Bearer.

    Thanks to everyone that helped make this release happen. You're awesome!

    Source code(tar.gz)
    Source code(zip)
A JSON Web Token authentication plugin for the Django REST Framework.

Simple JWT Abstract Simple JWT is a JSON Web Token authentication plugin for the Django REST Framework. For full documentation, visit django-rest-fram

Simple JWT 3.3k Jan 01, 2023
OAuth2 goodies for the Djangonauts!

Django OAuth Toolkit OAuth2 goodies for the Djangonauts! If you are facing one or more of the following: Your Django app exposes a web API you want to

Jazzband 2.7k Jan 01, 2023
Djagno grpc authentication service with jwt auth

Django gRPC authentication service STEP 1: Install packages pip install -r requirements.txt STEP 2: Make migrations and migrate python manage.py makem

Saeed Hassani Borzadaran 3 May 16, 2022
Awesome Django authorization, without the database

rules rules is a tiny but powerful app providing object-level permissions to Django, without requiring a database. At its core, it is a generic framew

1.6k Dec 30, 2022
Simple Login - Login Extension for Flask - maintainer @cuducos

Login Extension for Flask The simplest way to add login to flask! Top Contributors Add yourself, send a PR! How it works First install it from PyPI. p

Flask Extensions 181 Jan 01, 2023
Flask JWT Router is a Python library that adds authorised routes to a Flask app.

Read the docs: Flask-JWT-Router Flask JWT Router Flask JWT Router is a Python library that adds authorised routes to a Flask app. Both basic & Google'

Joe Gasewicz 52 Jan 03, 2023
A Login/Registration GUI Application with SQLite database for manipulating data.

Login-Register_Tk A Login/Registration GUI Application with SQLite database for manipulating data. What is this program? This program is a GUI applica

Arsalan 1 Feb 01, 2022
Django Auth Protection This package logout users from the system by changing the password in Simple JWT REST API.

Django Auth Protection Django Auth Protection This package logout users from the system by changing the password in REST API. Why Django Auth Protecti

Iman Karimi 5 Oct 26, 2022
An open source Flask extension that provides JWT support (with batteries included)!

Flask-JWT-Extended Features Flask-JWT-Extended not only adds support for using JSON Web Tokens (JWT) to Flask for protecting views, but also many help

Landon Gilbert-Bland 1.4k Jan 04, 2023
API with high performance to create a simple blog and Auth using OAuth2 ⛏

DogeAPI API with high performance built with FastAPI & SQLAlchemy, help to improve connection with your Backend Side to create a simple blog and Cruds

Yasser Tahiri 111 Jan 05, 2023
Authentication with fastapi and jwt cd realistic

Authentication with fastapi and jwt cd realistic Dependencies bcrypt==3.1.7 data

Fredh Macau 1 Jan 04, 2022
Crie seus tokens de autenticação com o AScrypt.

AScrypt tokens O AScrypt é uma forma de gerar tokens de autenticação para sua aplicação de forma rápida e segura. Todos os tokens que foram, mesmo que

Jaedson Silva 0 Jun 24, 2022
Ready to use and customizable Authentications and Authorisation management for FastAPI ⚡

AuthenticationX 💫 Ready-to-use and customizable Authentications and Oauth2 management for FastAPI ⚡ Source Code: https://github.com/yezz123/AuthX Doc

Yasser Tahiri 404 Dec 27, 2022
Flask Implementation of a login page and some basic functionality.

login_page Flask Implementation of a login page and some basic functionality. How to Run $ chmod +x run.sh setup.sh $ # run setup.sh only if the datab

3 Jun 03, 2021
Foundation Auth Proxy is an abstraction on Foundations' authentication layer and is used to authenticate requests to Atlas's REST API.

foundations-auth-proxy Setup By default the server runs on http://0.0.0.0:5558. This can be changed via the arguments. Arguments: '-H' or '--host': ho

Dessa - Open Source 2 Jul 03, 2020
A recipe sharing API built using Django rest framework.

Recipe Sharing API This is the backend API for the recipe sharing platform at https://mesob-recipe.netlify.app/ This API allows users to share recipes

Hannah 21 Dec 30, 2022
Object Moderation Layer

django-oml Welcome to the documentation for django-oml! OML means Object Moderation Layer, the idea is to have a mixin model that allows you to modera

Angel Velásquez 12 Aug 22, 2019
Mock authentication API that acceccpts email and password and returns authentication result.

Mock authentication API that acceccpts email and password and returns authentication result.

Herman Shpryhau 1 Feb 11, 2022
Django CAS 1.0/2.0/3.0 client authentication library, support Django 2.0, 2.1, 2.2, 3.0 and Python 3.5+

django-cas-ng django-cas-ng is Django CAS (Central Authentication Service) 1.0/2.0/3.0 client library to support SSO (Single Sign On) and Single Logou

django-cas-ng 347 Dec 18, 2022
User Authentication in Flask using Flask-Login

User-Authentication-in-Flask Set up & Installation. 1 .Clone/Fork the git repo and create an environment Windows git clone https://github.com/Dev-Elie

ONDIEK ELIJAH OCHIENG 31 Dec 11, 2022