From 4470aea69e23828cdd8b35c75e7057b9cf3d9806 Mon Sep 17 00:00:00 2001 From: Andrew Chen Wang <60190294+Andrew-Chen-Wang@users.noreply.github.com> Date: Thu, 9 Jul 2020 16:24:53 -0400 Subject: [PATCH] Initial commit --- LICENSE | 21 +++ README.md | 64 +++++++++ server/.gitignore | 124 +++++++++++++++++ server/README.md | 20 +++ server/manage.py | 21 +++ server/public/__init__.py | 0 server/public/admin.py | 3 + server/public/apps.py | 5 + server/public/migrations/0001_initial.py | 18 +++ server/public/migrations/__init__.py | 0 server/public/models.py | 3 + server/public/tests.py | 3 + server/public/urls.py | 19 +++ server/public/views.py | 21 +++ server/requirements.txt | 7 + server/server/__init__.py | 0 server/server/asgi.py | 16 +++ server/server/settings.py | 164 +++++++++++++++++++++++ server/server/urls.py | 22 +++ server/server/wsgi.py | 16 +++ 20 files changed, 547 insertions(+) create mode 100644 LICENSE create mode 100644 README.md create mode 100644 server/.gitignore create mode 100644 server/README.md create mode 100644 server/manage.py create mode 100644 server/public/__init__.py create mode 100644 server/public/admin.py create mode 100644 server/public/apps.py create mode 100644 server/public/migrations/0001_initial.py create mode 100644 server/public/migrations/__init__.py create mode 100644 server/public/models.py create mode 100644 server/public/tests.py create mode 100644 server/public/urls.py create mode 100644 server/public/views.py create mode 100644 server/requirements.txt create mode 100644 server/server/__init__.py create mode 100644 server/server/asgi.py create mode 100644 server/server/settings.py create mode 100644 server/server/urls.py create mode 100644 server/server/wsgi.py diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..7eb6fe0 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020 SimpleJWT + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..8292e8f --- /dev/null +++ b/README.md @@ -0,0 +1,64 @@ +# Template Repository for DRF SimpleJWT Apps + +Initially created: 3 July 2020 + +TL;DR: Django server repository setup for SimpleJWT. Test user: `test` and pw `test`. + +--- +### Example repositories + +- Android: [Andrew-Chen-Wang/mobile-auth-example](https://github.com/Andrew-Chen-Wang/mobile-auth-example) +- iOS: [Andrew-Chen-Wang/mobile-auth-example](https://github.com/Andrew-Chen-Wang/mobile-auth-example) + +--- +### Introduction + +This template repository is dedicated to generating +a Django + DRF server with SimpleJWT already setup. +The purpose of this is to easily create repositories +that demonstrate clear usage of SimpleJWT. + +If you're not using a frontend framework like React +or some kind of mobile device not using a web browser, +then please use session authentication. I.e. if you're +using plain HTML with Jinja 2 template tags, use the +built-in session authentication middlewear as that +is proven to be the safest and thus far never broken +method of secure authentication. + +Note: this template repository is adopted from +[Andrew-Chen-Wang/mobile-auth-example](https://github.com/Andrew-Chen-Wang/mobile-auth-example) +for Android and iOS usage. The license is Apache 2.0 +for that example repository. + +--- +### Usage + +1. To generate a repository using this template, +press "Use this template" (highlighted in green). +Note, this will NOT create a fork of the repository. +2. Create your git repository, connect via the ssh remote, and pull. +3. `cd server` to get your terminal/cmd into the server directory. +4. To run the server, create a virtual environment `virtualenv venv && source venv/bin/activate`, install packages `pip install -r requirements.txt` -- the requirements.txt file is inside the server subdirectory -- and do `python manage.py migrate && python manage.py runserver`. + - Again, make sure when you do this, you are inside the server directory on your terminal/cmd. + - On Windows, you should do `venv\Scripts\activate` instead of `source venv/bin/activate` +5. If you're writing for an example repository, please create +a new directory labeled with the name of the framework (e.g. jwt-ios), +and add its `.gitignore`. Please use the +[github/gitignore](https://github.com/github/gitignore) repository. +Provide detailed instructions if necessary. + +A default user with the username `test` and password `test` have been created. + +This repository does not come with throttling, but **it is +highly recommended that you add throttling to your entire +project.** You can use a third-party package called +Django-ratelimit or DRF's internal throttling mechanism. +Django-ratelimit is more extensive -- covering Django views, +as well -- and thus more supported by SimpleJWT. + +--- +### License + +This repository is licensed under the +[MIT License](https://github.com/SimpleJWT/drf-SimpleJWT-server-template/blob/master/LICENSE). diff --git a/server/.gitignore b/server/.gitignore new file mode 100644 index 0000000..c0c219b --- /dev/null +++ b/server/.gitignore @@ -0,0 +1,124 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +pip-wheel-metadata/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +.python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# celery beat schedule file +celerybeat-schedule + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ diff --git a/server/README.md b/server/README.md new file mode 100644 index 0000000..31f54bf --- /dev/null +++ b/server/README.md @@ -0,0 +1,20 @@ +# Django Server + +The backend works by using Django, Django Rest Framework, and DRF SimpleJWT. + +For this demonstration, SimpleJWT utilizes the refresh and access token methodology. The client sends its credentials to the server once and receives an access and refresh token. Everytime you want to do authentication on a view, the client will send the access token; however, that access token expires (in our case, in 5 minutes for security reasons). Once it expires, instead of resending the credentials, we use the refresh token to get a new access token. + +If the refresh token expires (after 1 day for security reasons), the client needs to send the username and password again. + +### Running the server + +1. Create a virtual environment and install the packages: `virtualenv venv && source venv/bin/activate && pip install -r requirements.txt`. + - Again, make sure when you do this, you are inside the server directory on your terminal/cmd. + - On Windows, you should do `venv\Scripts\activate` instead of `source venv/bin/activate` +2. Run the server: `python manage.py migrate && python manage.py runserver` + +A default user with the username `test` and password `test` have been created. + +### Other suggestions + +I also suggest you use a rate limiter, either provided by Django Rest Framework or a more sophisticated one like django-ratelimit so that you can rate limit across your entire application, not just your REST API. \ No newline at end of file diff --git a/server/manage.py b/server/manage.py new file mode 100644 index 0000000..1c81878 --- /dev/null +++ b/server/manage.py @@ -0,0 +1,21 @@ +#!/usr/bin/env python +"""Django's command-line utility for administrative tasks.""" +import os +import sys + + +def main(): + os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'server.settings') + try: + from django.core.management import execute_from_command_line + except ImportError as exc: + raise ImportError( + "Couldn't import Django. Are you sure it's installed and " + "available on your PYTHONPATH environment variable? Did you " + "forget to activate a virtual environment?" + ) from exc + execute_from_command_line(sys.argv) + + +if __name__ == '__main__': + main() diff --git a/server/public/__init__.py b/server/public/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/server/public/admin.py b/server/public/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/server/public/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/server/public/apps.py b/server/public/apps.py new file mode 100644 index 0000000..903db63 --- /dev/null +++ b/server/public/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class PublicConfig(AppConfig): + name = 'public' diff --git a/server/public/migrations/0001_initial.py b/server/public/migrations/0001_initial.py new file mode 100644 index 0000000..bce6420 --- /dev/null +++ b/server/public/migrations/0001_initial.py @@ -0,0 +1,18 @@ +# Generated by Django 3.0.3 on 2020-03-02 21:13 + +from django.db import migrations +from django.contrib.auth.models import User + + +def create_user(apps, schema_editor): + User.objects.create_superuser("test", password="test") + + +class Migration(migrations.Migration): + + dependencies = [ + ] + + operations = [ + migrations.RunPython(create_user) + ] diff --git a/server/public/migrations/__init__.py b/server/public/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/server/public/models.py b/server/public/models.py new file mode 100644 index 0000000..71a8362 --- /dev/null +++ b/server/public/models.py @@ -0,0 +1,3 @@ +from django.db import models + +# Create your models here. diff --git a/server/public/tests.py b/server/public/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/server/public/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/server/public/urls.py b/server/public/urls.py new file mode 100644 index 0000000..f9e5576 --- /dev/null +++ b/server/public/urls.py @@ -0,0 +1,19 @@ +from django.urls import path, include +from . import views +from rest_framework_simplejwt.views import TokenObtainPairView, TokenRefreshView + +from rest_framework import routers +router = routers.DefaultRouter() +router.register('ping', views.PingViewSet, basename="ping") + +urlpatterns = [ + path('api/token/access/', TokenRefreshView.as_view(), name='token_get_access'), + path('api/token/both/', TokenObtainPairView.as_view(), name='token_obtain_pair'), + path('api/', include(router.urls)) +] + +""" +- For the first view, you send the refresh token to get a new access token. +- For the second view, you send the client credentials (username and password) + to get BOTH a new access and refresh token. +""" diff --git a/server/public/views.py b/server/public/views.py new file mode 100644 index 0000000..c9fe283 --- /dev/null +++ b/server/public/views.py @@ -0,0 +1,21 @@ +from rest_framework.viewsets import GenericViewSet +from rest_framework.mixins import ListModelMixin +from rest_framework.response import Response +from rest_framework.status import HTTP_200_OK +from rest_framework.permissions import IsAuthenticated + + +class PingViewSet(GenericViewSet, ListModelMixin): + """ + Helpful class for internal health checks + for when your server deploys. Typical of AWS + applications behind ALB which does default 30 + second ping/health checks. + """ + permission_classes = [IsAuthenticated] + + def list(self, request, *args, **kwargs): + return Response( + data={"id": request.GET.get("id")}, + status=HTTP_200_OK + ) diff --git a/server/requirements.txt b/server/requirements.txt new file mode 100644 index 0000000..5ee85ed --- /dev/null +++ b/server/requirements.txt @@ -0,0 +1,7 @@ +asgiref==3.2.3 +Django==3.0.7 +djangorestframework==3.11.0 +djangorestframework-simplejwt==4.4.0 +PyJWT==1.7.1 +pytz==2019.3 +sqlparse==0.3.1 diff --git a/server/server/__init__.py b/server/server/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/server/server/asgi.py b/server/server/asgi.py new file mode 100644 index 0000000..2526a47 --- /dev/null +++ b/server/server/asgi.py @@ -0,0 +1,16 @@ +""" +ASGI config for server project. + +It exposes the ASGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/3.0/howto/deployment/asgi/ +""" + +import os + +from django.core.asgi import get_asgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'server.settings') + +application = get_asgi_application() diff --git a/server/server/settings.py b/server/server/settings.py new file mode 100644 index 0000000..f99a9af --- /dev/null +++ b/server/server/settings.py @@ -0,0 +1,164 @@ +""" +Django settings for server project. +Generated by 'django-admin startproject' using Django 3.0.3. +""" +import os +# IMPORTANT STUFF BELOW!!!!! +# IMPORTANT STUFF BELOW!!!!! +# IMPORTANT STUFF BELOW!!!!! +# IMPORTANT STUFF BELOW!!!!! +# IMPORTANT STUFF BELOW!!!!! +# IMPORTANT STUFF BELOW!!!!! +# IMPORTANT STUFF BELOW!!!!! + +BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +# SECURITY WARNING: keep the secret key used in production secret! +SECRET_KEY = '3hls$)92@wy0z^lq67@w1a(qzx4*$)pj)_*1$m!h$k#dl(odq&' +# SECURITY WARNING: don't run with debug turned on in production! +DEBUG = True + +# The last one is my private IP address for Android development +ALLOWED_HOSTS = ["127.0.0.1", "localhost", "192.168.0.12"] + +# IMPORTANT STUFF +# IMPORTANT STUFF +# IMPORTANT STUFF +# IMPORTANT STUFF +# IMPORTANT STUFF +# IMPORTANT STUFF +# IMPORTANT STUFF +# IMPORTANT STUFF + +INSTALLED_APPS = [ + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', + 'rest_framework', + 'rest_framework_simplejwt.token_blacklist', + 'public' +] + +REST_FRAMEWORK = { + 'DEFAULT_AUTHENTICATION_CLASSES': ( + 'rest_framework_simplejwt.authentication.JWTAuthentication', + ) +} + +from datetime import timedelta + +SIMPLE_JWT_SIGNING_KEY = "b=72^ado*%1(v3r7rga9ch)03xr=d*f)lroz94kosf!61((9=i" + + +SIMPLE_JWT = { + 'ACCESS_TOKEN_LIFETIME': timedelta(seconds=5), + 'REFRESH_TOKEN_LIFETIME': timedelta(seconds=15), + 'ROTATE_REFRESH_TOKENS': False, + 'BLACKLIST_AFTER_ROTATION': True, + + 'ALGORITHM': 'HS256', + 'SIGNING_KEY': SIMPLE_JWT_SIGNING_KEY, + 'VERIFYING_KEY': None, + 'AUDIENCE': None, + 'ISSUER': None, + + 'AUTH_HEADER_TYPES': ('Bearer',), + 'USER_ID_FIELD': 'id', + 'USER_ID_CLAIM': 'user_id', + + 'AUTH_TOKEN_CLASSES': ('rest_framework_simplejwt.tokens.AccessToken',), + 'TOKEN_TYPE_CLAIM': 'token_type', + + 'JTI_CLAIM': 'jti', + + 'SLIDING_TOKEN_REFRESH_EXP_CLAIM': 'refresh_exp', + 'SLIDING_TOKEN_LIFETIME': timedelta(minutes=5), + 'SLIDING_TOKEN_REFRESH_LIFETIME': timedelta(days=1), +} + + + + +# Not as important... + +MIDDLEWARE = [ + 'django.middleware.security.SecurityMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.common.CommonMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', +] + +ROOT_URLCONF = 'server.urls' + +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [], + 'APP_DIRS': True, + 'OPTIONS': { + 'context_processors': [ + 'django.template.context_processors.debug', + 'django.template.context_processors.request', + 'django.contrib.auth.context_processors.auth', + 'django.contrib.messages.context_processors.messages', + ], + }, + }, +] + +WSGI_APPLICATION = 'server.wsgi.application' + + +# Database +# https://docs.djangoproject.com/en/3.0/ref/settings/#databases + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), + } +} + + +# Password validation +# https://docs.djangoproject.com/en/3.0/ref/settings/#auth-password-validators + +AUTH_PASSWORD_VALIDATORS = [ + { + 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', + }, +] + + +# Internationalization +# https://docs.djangoproject.com/en/3.0/topics/i18n/ + +LANGUAGE_CODE = 'en-us' + +TIME_ZONE = 'UTC' + +USE_I18N = True + +USE_L10N = True + +USE_TZ = True + + +# Static files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/3.0/howto/static-files/ + +STATIC_URL = '/static/' diff --git a/server/server/urls.py b/server/server/urls.py new file mode 100644 index 0000000..0069206 --- /dev/null +++ b/server/server/urls.py @@ -0,0 +1,22 @@ +"""server URL Configuration + +The `urlpatterns` list routes URLs to views. For more information please see: + https://docs.djangoproject.com/en/3.0/topics/http/urls/ +Examples: +Function views + 1. Add an import: from my_app import views + 2. Add a URL to urlpatterns: path('', views.home, name='home') +Class-based views + 1. Add an import: from other_app.views import Home + 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') +Including another URLconf + 1. Import the include() function: from django.urls import include, path + 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) +""" +from django.contrib import admin +from django.urls import path, include + +urlpatterns = [ + path('admin/', admin.site.urls), + path('', include('public.urls')) +] diff --git a/server/server/wsgi.py b/server/server/wsgi.py new file mode 100644 index 0000000..c65f7e2 --- /dev/null +++ b/server/server/wsgi.py @@ -0,0 +1,16 @@ +""" +WSGI config for server project. + +It exposes the WSGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/3.0/howto/deployment/wsgi/ +""" + +import os + +from django.core.wsgi import get_wsgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'server.settings') + +application = get_wsgi_application()