diff --git a/.circleci/config.yml b/.circleci/config.yml index 175b28e..759dca9 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -4,34 +4,34 @@ jobs: working_directory: ~/repo/01-Authorization machine: image: ubuntu-2004:202107-02 - + steps: - checkout: - path: ~/repo + path: ~/repo - - run: + - run: name: Set .env file command: | - echo "AUTH0_DOMAIN=$auth0_domain" >> .env - echo "API_IDENTIFIER=$api_identifier" >> .env - - - run: - name: background server - command: sh exec.sh - background: true - + echo "AUTH0_DOMAIN=$auth0_domain" >> .env + echo "AUTH0_API_IDENTIFIER=$api_identifier" >> .env + - run: - name: Wait until server is online - command: | - until $(curl --output /dev/null --silent --fail http://localhost:3010/api/public); do - sleep 5 - done - + name: background server + command: sh exec.sh + background: true + + - run: + name: Wait until server is online + command: | + until $(curl --output /dev/null --silent --fail http://localhost:8000/api/public); do + sleep 5 + done + - run: name: Clone script test command: git clone -b v0.0.1 --depth 1 https://github.com/auth0-samples/api-quickstarts-tests test - - - run: + + - run: name: Prepare environment variables for test command: | cd test @@ -45,7 +45,7 @@ jobs: echo "AUTH0_CLIENT_SECRET_3=$client_secret_scopes_write" >> .env echo "AUTH0_CLIENT_ID_4=$client_id_scopes_readwrite" >> .env echo "AUTH0_CLIENT_SECRET_4=$client_secret_scopes_readwrite" >> .env - echo "API_URL=http://localhost:3010" >> .env + echo "API_URL=http://localhost:8000" >> .env - run: name: Install test script dependency @@ -60,4 +60,3 @@ workflows: jobs: - build: context: Quickstart API Tests - diff --git a/01-Authorization/.env.example b/01-Authorization/.env.example index 38fe206..b7ae24d 100644 --- a/01-Authorization/.env.example +++ b/01-Authorization/.env.example @@ -1,2 +1,2 @@ AUTH0_DOMAIN={DOMAIN} -API_IDENTIFIER={API_IDENTIFIER} +AUTH0_API_IDENTIFIER={API_IDENTIFIER} diff --git a/01-Authorization/.gitignore b/01-Authorization/.gitignore index ac641de..3db030e 100644 --- a/01-Authorization/.gitignore +++ b/01-Authorization/.gitignore @@ -1,3 +1,5 @@ .env db.sqlite3 -*.pyc \ No newline at end of file +*.pyc +requirements.in +__pycache__ diff --git a/01-Authorization/Dockerfile b/01-Authorization/Dockerfile index ec39fd1..1d21f92 100644 --- a/01-Authorization/Dockerfile +++ b/01-Authorization/Dockerfile @@ -4,13 +4,13 @@ WORKDIR /home/app #If we add the requirements and install dependencies first, docker can use cache if requirements don't change ADD requirements.txt /home/app -RUN pip install --no-cache-dir -r requirements.txt +RUN pip3 install --no-cache-dir -r requirements.txt ADD . /home/app # Migrate the database -RUN python manage.py migrate +RUN python3 manage.py migrate -CMD python manage.py runserver 0.0.0.0:3010 +CMD python manage.py runserver 0.0.0.0:8000 -EXPOSE 3010 \ No newline at end of file +EXPOSE 8000 diff --git a/01-Authorization/apiexample/settings.py b/01-Authorization/apiexample/settings.py deleted file mode 100644 index a39ea10..0000000 --- a/01-Authorization/apiexample/settings.py +++ /dev/null @@ -1,169 +0,0 @@ -""" -Django settings for apiexample project. - -Generated by 'django-admin startproject' using Django 1.11.4. - -For more information on this file, see -https://docs.djangoproject.com/en/1.11/topics/settings/ - -For the full list of settings and their values, see -https://docs.djangoproject.com/en/1.11/ref/settings/ -""" - -import os - -from dotenv import load_dotenv, find_dotenv - -# Build paths inside the project like this: os.path.join(BASE_DIR, ...) -BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) - - -# Quick-start development settings - unsuitable for production -# See https://docs.djangoproject.com/en/1.11/howto/deployment/checklist/ - -# SECURITY WARNING: keep the secret key used in production secret! -SECRET_KEY = 'a819@)zd0&536k68f%n3)t1+t(d@-44ehevjps-2)#@k7bsjc^' - -# SECURITY WARNING: don't run with debug turned on in production! -DEBUG = True - -ALLOWED_HOSTS = [] - - -# Application definition - -INSTALLED_APPS = [ - 'django.contrib.admin', - 'django.contrib.auth', - 'django.contrib.contenttypes', - 'django.contrib.sessions', - 'django.contrib.messages', - 'django.contrib.staticfiles', - 'rest_framework', - 'corsheaders' -] - -MIDDLEWARE = [ - 'corsheaders.middleware.CorsMiddleware', - 'django.middleware.security.SecurityMiddleware', - 'django.contrib.sessions.middleware.SessionMiddleware', - 'django.middleware.common.CommonMiddleware', - 'django.middleware.csrf.CsrfViewMiddleware', - 'django.contrib.auth.middleware.AuthenticationMiddleware', - 'django.contrib.auth.middleware.RemoteUserMiddleware', - 'django.contrib.messages.middleware.MessageMiddleware', - 'django.middleware.clickjacking.XFrameOptionsMiddleware', -] - -AUTHENTICATION_BACKENDS = [ - 'django.contrib.auth.backends.ModelBackend', - 'django.contrib.auth.backends.RemoteUserBackend', -] - -ROOT_URLCONF = 'apiexample.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 = 'apiexample.wsgi.application' - - -# Database -# https://docs.djangoproject.com/en/1.11/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/1.11/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/1.11/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/1.11/howto/static-files/ - -STATIC_URL = '/static/' - -REST_FRAMEWORK = { - 'DEFAULT_PERMISSION_CLASSES': ( - 'rest_framework.permissions.IsAuthenticated', - ), - 'DEFAULT_AUTHENTICATION_CLASSES': ( - 'rest_framework_jwt.authentication.JSONWebTokenAuthentication', - 'rest_framework.authentication.SessionAuthentication', - 'rest_framework.authentication.BasicAuthentication', - ), -} - -CORS_ORIGIN_WHITELIST = ( - 'http://localhost:3000', -) - -ENV_FILE = find_dotenv() -if ENV_FILE: - load_dotenv(ENV_FILE) - -AUTH0_DOMAIN = os.environ.get('AUTH0_DOMAIN') -API_IDENTIFIER = os.environ.get('API_IDENTIFIER') -PUBLIC_KEY = None -JWT_ISSUER = None - -if AUTH0_DOMAIN: - JWT_ISSUER = 'https://' + AUTH0_DOMAIN + '/' - -JWT_AUTH = { - 'JWT_PAYLOAD_GET_USERNAME_HANDLER': - 'auth0authorization.utils.jwt_get_username_from_payload_handler', - 'JWT_DECODE_HANDLER': - 'auth0authorization.utils.jwt_decode_token', - 'JWT_ALGORITHM': 'RS256', - 'JWT_AUDIENCE': API_IDENTIFIER, - 'JWT_ISSUER': JWT_ISSUER, - 'JWT_AUTH_HEADER_PREFIX': 'Bearer', -} diff --git a/01-Authorization/apiexample/urls.py b/01-Authorization/apiexample/urls.py deleted file mode 100644 index c9c6979..0000000 --- a/01-Authorization/apiexample/urls.py +++ /dev/null @@ -1,22 +0,0 @@ -"""apiexample URL Configuration - -The `urlpatterns` list routes URLs to views. For more information please see: - https://docs.djangoproject.com/en/1.11/topics/http/urls/ -Examples: -Function views - 1. Add an import: from my_app import views - 2. Add a URL to urlpatterns: url(r'^$', views.home, name='home') -Class-based views - 1. Add an import: from other_app.views import Home - 2. Add a URL to urlpatterns: url(r'^$', Home.as_view(), name='home') -Including another URLconf - 1. Import the include() function: from django.conf.urls import url, include - 2. Add a URL to urlpatterns: url(r'^blog/', include('blog.urls')) -""" -from django.contrib import admin -from django.urls import include, path - -urlpatterns = [ - path('admin/', admin.site.urls), - path('', include('auth0authorization.urls')) -] diff --git a/01-Authorization/apiexample/__init__.py b/01-Authorization/app/__init__.py similarity index 100% rename from 01-Authorization/apiexample/__init__.py rename to 01-Authorization/app/__init__.py diff --git a/01-Authorization/app/asgi.py b/01-Authorization/app/asgi.py new file mode 100644 index 0000000..b4c64ab --- /dev/null +++ b/01-Authorization/app/asgi.py @@ -0,0 +1,16 @@ +""" +ASGI config for example 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/4.2/howto/deployment/asgi/ +""" + +import os + +from django.core.asgi import get_asgi_application + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "app.settings") + +application = get_asgi_application() diff --git a/01-Authorization/app/authorization.py b/01-Authorization/app/authorization.py new file mode 100644 index 0000000..5bcf098 --- /dev/null +++ b/01-Authorization/app/authorization.py @@ -0,0 +1,133 @@ +import os + +from .middleware import JsonException + +from jwt import PyJWKClient, decode +from dotenv import load_dotenv +from functools import wraps +from django.http import HttpRequest, JsonResponse +from typing import Any + + +def authorized(function): + @wraps(function) + def wrap(request: HttpRequest, *args, **kwargs): + token: RequestToken = getRequestToken(request, mutateRequest=True) + + if token is None or token.isAuthorized() is False: + raise JsonException("Unauthorized.", 401) + + return function(request, token, *args, **kwargs) + + return wrap + + +def can(permission): + def decor(function): + @wraps(function) + def wrap(request: HttpRequest, *args, **kwargs): + token: RequestToken = getRequestToken(request, mutateRequest=True) + + if token is None or token.isAuthorized() is False: + raise JsonException("Unauthorized.", 401) + + if token.hasPermission(permission) is False: + raise JsonException("Forbidden.", 403) + + return function(request, token, *args, **kwargs) + + return wrap + + return decor + + +class RequestToken(object): + def __init__(self, token: str) -> None: + self._token: str = token + + if token is not None: + self._decoded: dict[str, Any] | None = self.__decode__(token) + else: + self._decoded = None + + def __decode__(self, token: str) -> dict[str, Any] | None: + try: + load_dotenv() + except: + raise JsonException("Environment variables must be configured.", 500) + + domain: str | None = os.environ.get("AUTH0_DOMAIN") + identifier: str | None = os.environ.get("AUTH0_API_IDENTIFIER") + + if domain is None or identifier is None: + raise JsonException( + "AUTH0_DOMAIN or AUTH0_API_IDENTIFIER environment variables must be configured.", + 500, + ) + + issuer: str = "https://{}/".format(domain) + + signingKey: Any = ( + PyJWKClient(issuer + ".well-known/jwks.json") + .get_signing_key_from_jwt(self._token) + .key + ) + + if signingKey is None: + raise JsonException( + "Could not retrieve a matching public key for the provided token.", 400 + ) + + try: + return decode( + jwt=self._token, + key=signingKey, + algorithms=["RS256"], + audience=identifier, + issuer=issuer, + ) + except: + raise JsonException("Could not decode the provided token.", 400) + + def __str__(self) -> str: + return self._token + + def __getattr__(self, name: str) -> Any: + return self._decoded[name] + + def hasPermission(self, permission: str) -> bool: + return permission in self._decoded["permissions"] + + def clear(self) -> None: + self._decoded = None + + def isAuthorized(self) -> bool: + return self._decoded is not None + + def dict(self) -> dict[str, Any]: + return self._decoded if self._decoded is not None else {} + + +def getRequestToken( + request: HttpRequest, mutateRequest: bool = False +) -> RequestToken | None: + bearerToken: str | None = request.headers.get("Authorization") + + if bearerToken is None or not bearerToken.startswith("Bearer "): + return None + + bearerToken = bearerToken.partition(" ")[2] + + if ( + request.META.get("token") is not None + and request.META.get("bearerToken") == bearerToken + ): + return request.META.get("token") + + token: RequestToken = RequestToken(bearerToken) + + if mutateRequest: + request.META["token"] = token + request.META["bearerToken"] = bearerToken + + return token diff --git a/01-Authorization/app/middleware.py b/01-Authorization/app/middleware.py new file mode 100644 index 0000000..08a56eb --- /dev/null +++ b/01-Authorization/app/middleware.py @@ -0,0 +1,24 @@ +import logging +from django.http import JsonResponse + + +class JsonException(Exception): + pass + + +class JsonExceptionMiddleware(object): + def __init__(self, get_response): + self.get_response = get_response + + def __call__(self, request): + response = self.get_response(request) + return response + + def process_exception(self, request, exception): + if not isinstance(exception, JsonException): + return None + + message, code = exception.args + logging.error(exception) + + return JsonResponse(data={"error": message}, status=code) diff --git a/01-Authorization/app/settings.py b/01-Authorization/app/settings.py new file mode 100644 index 0000000..fd29b48 --- /dev/null +++ b/01-Authorization/app/settings.py @@ -0,0 +1,124 @@ +""" +Django settings for example project. + +Generated by 'django-admin startproject' using Django 4.2.2. + +For more information on this file, see +https://docs.djangoproject.com/en/4.2/topics/settings/ + +For the full list of settings and their values, see +https://docs.djangoproject.com/en/4.2/ref/settings/ +""" + +from pathlib import Path + +# Build paths inside the project like this: BASE_DIR / 'subdir'. +BASE_DIR = Path(__file__).resolve().parent.parent + + +# Quick-start development settings - unsuitable for production +# See https://docs.djangoproject.com/en/4.2/howto/deployment/checklist/ + +# SECURITY WARNING: keep the secret key used in production secret! +SECRET_KEY = "django-insecure-7h)&4a0-1fl&+88+#axw^byqll9d=9iviunmt#snd1ebkpv$#h" + +# SECURITY WARNING: don't run with debug turned on in production! +DEBUG = True + +ALLOWED_HOSTS = [] + + +# Application definition + +INSTALLED_APPS = [ + "django.contrib.admin", + "django.contrib.auth", + "django.contrib.contenttypes", + "django.contrib.sessions", + "django.contrib.messages", + "django.contrib.staticfiles", +] + +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", + "app.middleware.JsonExceptionMiddleware", +] + +ROOT_URLCONF = "app.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 = "app.wsgi.application" + + +# Database +# https://docs.djangoproject.com/en/4.2/ref/settings/#databases + +DATABASES = { + "default": { + "ENGINE": "django.db.backends.sqlite3", + "NAME": BASE_DIR / "db.sqlite3", + } +} + + +# Password validation +# https://docs.djangoproject.com/en/4.2/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/4.2/topics/i18n/ + +LANGUAGE_CODE = "en-us" + +TIME_ZONE = "UTC" + +USE_I18N = True + +USE_TZ = True + + +# Static files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/4.2/howto/static-files/ + +STATIC_URL = "static/" + +# Default primary key field type +# https://docs.djangoproject.com/en/4.2/ref/settings/#default-auto-field + +DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" diff --git a/01-Authorization/app/urls.py b/01-Authorization/app/urls.py new file mode 100644 index 0000000..dade25c --- /dev/null +++ b/01-Authorization/app/urls.py @@ -0,0 +1,15 @@ +from django.urls import URLPattern, path +from django.views.generic import RedirectView + +from . import views + +urlpatterns: list[URLPattern] = [ + path( + route="", + view=RedirectView.as_view(permanent=False, url="/api/public"), + name="index", + ), + path(route="api/public", view=views.public, name="public"), + path(route="api/private", view=views.private, name="private"), + path(route="api/private-scoped", view=views.privateScoped, name="private_scoped"), +] diff --git a/01-Authorization/app/views.py b/01-Authorization/app/views.py new file mode 100644 index 0000000..19dbe43 --- /dev/null +++ b/01-Authorization/app/views.py @@ -0,0 +1,33 @@ +from django.http import HttpRequest, JsonResponse +from .authorization import RequestToken, authorized, can, getRequestToken + + +def public(request: HttpRequest()) -> JsonResponse: + token: RequestToken | None = getRequestToken(request) + + return JsonResponse( + data={ + "message": "Hello from a public endpoint! You don't need to be authenticated to see this.", + "token": token.dict(), + } + ) + + +@authorized +def private(request: HttpRequest, token: RequestToken) -> JsonResponse: + return JsonResponse( + data={ + "message": "Hello from a private endpoint! You need to be authenticated to see this.", + "token": token.dict(), + } + ) + + +@can("read:messages") +def privateScoped(request: HttpRequest, token: RequestToken) -> JsonResponse: + return JsonResponse( + data={ + "message": "Hello from a private endpoint! You need to be authenticated and have a scope of read:messages to see this.", + "token": token.dict(), + } + ) diff --git a/01-Authorization/apiexample/wsgi.py b/01-Authorization/app/wsgi.py similarity index 57% rename from 01-Authorization/apiexample/wsgi.py rename to 01-Authorization/app/wsgi.py index 250de16..7b76fd9 100644 --- a/01-Authorization/apiexample/wsgi.py +++ b/01-Authorization/app/wsgi.py @@ -1,16 +1,16 @@ """ -WSGI config for apiexample project. +WSGI config for example 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/1.11/howto/deployment/wsgi/ +https://docs.djangoproject.com/en/4.2/howto/deployment/wsgi/ """ import os from django.core.wsgi import get_wsgi_application -os.environ.setdefault("DJANGO_SETTINGS_MODULE", "apiexample.settings") +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "app.settings") application = get_wsgi_application() diff --git a/01-Authorization/auth0authorization/__init__.py b/01-Authorization/auth0authorization/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/01-Authorization/auth0authorization/admin.py b/01-Authorization/auth0authorization/admin.py deleted file mode 100644 index 8c38f3f..0000000 --- a/01-Authorization/auth0authorization/admin.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.contrib import admin - -# Register your models here. diff --git a/01-Authorization/auth0authorization/apps.py b/01-Authorization/auth0authorization/apps.py deleted file mode 100644 index 3e7225d..0000000 --- a/01-Authorization/auth0authorization/apps.py +++ /dev/null @@ -1,5 +0,0 @@ -from django.apps import AppConfig - - -class AuthorizationConfig(AppConfig): - name = 'auth0authorization' diff --git a/01-Authorization/auth0authorization/migrations/__init__.py b/01-Authorization/auth0authorization/migrations/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/01-Authorization/auth0authorization/models.py b/01-Authorization/auth0authorization/models.py deleted file mode 100644 index 71a8362..0000000 --- a/01-Authorization/auth0authorization/models.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.db import models - -# Create your models here. diff --git a/01-Authorization/auth0authorization/tests.py b/01-Authorization/auth0authorization/tests.py deleted file mode 100644 index 7ce503c..0000000 --- a/01-Authorization/auth0authorization/tests.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.test import TestCase - -# Create your tests here. diff --git a/01-Authorization/auth0authorization/urls.py b/01-Authorization/auth0authorization/urls.py deleted file mode 100644 index 4b80563..0000000 --- a/01-Authorization/auth0authorization/urls.py +++ /dev/null @@ -1,9 +0,0 @@ -from django.urls import path - -from . import views - -urlpatterns = [ - path('api/public', views.public), - path('api/private', views.private), - path('api/private-scoped', views.private_scoped), -] diff --git a/01-Authorization/auth0authorization/utils.py b/01-Authorization/auth0authorization/utils.py deleted file mode 100644 index bfdaad0..0000000 --- a/01-Authorization/auth0authorization/utils.py +++ /dev/null @@ -1,29 +0,0 @@ -import json -import os - -from django.contrib.auth import authenticate -import jwt -import requests - - -def jwt_get_username_from_payload_handler(payload): - username = payload.get('sub').replace('|', '.') - authenticate(remote_user=username) - return username - - -def jwt_decode_token(token): - header = jwt.get_unverified_header(token) - auth0_domain = os.environ.get('AUTH0_DOMAIN') - jwks = requests.get('https://{}/.well-known/jwks.json'.format(auth0_domain)).json() - public_key = None - for jwk in jwks['keys']: - if jwk['kid'] == header['kid']: - public_key = jwt.algorithms.RSAAlgorithm.from_jwk(json.dumps(jwk)) - - if public_key is None: - raise Exception('Public key not found.') - - api_identifier = os.environ.get('API_IDENTIFIER') - issuer = 'https://{}/'.format(auth0_domain) - return jwt.decode(token, public_key, audience=api_identifier, issuer=issuer, algorithms=['RS256']) diff --git a/01-Authorization/auth0authorization/views.py b/01-Authorization/auth0authorization/views.py deleted file mode 100644 index cad3ca0..0000000 --- a/01-Authorization/auth0authorization/views.py +++ /dev/null @@ -1,57 +0,0 @@ -import jwt -from functools import wraps - -from django.http import JsonResponse -from rest_framework.decorators import api_view, permission_classes -from rest_framework.permissions import AllowAny - -# Create your views here. - - -def get_token_auth_header(request): - """Obtains the access token from the Authorization Header - """ - auth = request.META.get("HTTP_AUTHORIZATION", None) - parts = auth.split() - token = parts[1] - - return token - - -def requires_scope(required_scope): - """Determines if the required scope is present in the access token - Args: - required_scope (str): The scope required to access the resource - """ - def require_scope(f): - @wraps(f) - def decorated(*args, **kwargs): - token = get_token_auth_header(args[0]) - decoded = jwt.decode(token, verify=False) - if decoded.get("scope"): - token_scopes = decoded["scope"].split() - for token_scope in token_scopes: - if token_scope == required_scope: - return f(*args, **kwargs) - response = JsonResponse({'message': 'You don\'t have access to this resource'}) - response.status_code = 403 - return response - return decorated - return require_scope - - -@api_view(['GET']) -@permission_classes([AllowAny]) -def public(request): - return JsonResponse({'message': 'Hello from a public endpoint! You don\'t need to be authenticated to see this.'}) - - -@api_view(['GET']) -def private(request): - return JsonResponse({'message': 'Hello from a private endpoint! You need to be authenticated to see this.'}) - - -@api_view(['GET']) -@requires_scope('read:messages') -def private_scoped(request): - return JsonResponse({'message': 'Hello from a private endpoint! You need to be authenticated and have a scope of read:messages to see this.'}) diff --git a/01-Authorization/exec.ps1 b/01-Authorization/exec.ps1 index 550bc17..41f9c54 100644 --- a/01-Authorization/exec.ps1 +++ b/01-Authorization/exec.ps1 @@ -1,2 +1,2 @@ docker build -t auth0-django-api . -docker run --env-file .env -p 3010:3010 -it auth0-django-api +docker run --env-file .env -p 8000:8000 -it auth0-django-api diff --git a/01-Authorization/exec.sh b/01-Authorization/exec.sh index f434677..053eefb 100644 --- a/01-Authorization/exec.sh +++ b/01-Authorization/exec.sh @@ -1,3 +1,3 @@ #!/usr/bin/env bash docker build -t auth0-django-api . -docker run --env-file .env -p 3010:3010 -it auth0-django-api +docker run --env-file .env -p 8000:8000 -it auth0-django-api diff --git a/01-Authorization/manage.py b/01-Authorization/manage.py index 174a9a3..1a64b14 100755 --- a/01-Authorization/manage.py +++ b/01-Authorization/manage.py @@ -1,22 +1,22 @@ #!/usr/bin/env python +"""Django's command-line utility for administrative tasks.""" import os import sys -if __name__ == "__main__": - os.environ.setdefault("DJANGO_SETTINGS_MODULE", "apiexample.settings") + +def main(): + """Run administrative tasks.""" + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "app.settings") try: from django.core.management import execute_from_command_line - except ImportError: - # The above import may fail for some other reason. Ensure that the - # issue is really that Django is missing to avoid masking other - # exceptions on Python 2. - try: - import django - except ImportError: - 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?" - ) - raise + 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/01-Authorization/pyproject.toml b/01-Authorization/pyproject.toml new file mode 100644 index 0000000..641894a --- /dev/null +++ b/01-Authorization/pyproject.toml @@ -0,0 +1,3 @@ +[tool.black] +target-version = ['py38'] +include = '\.pyi?$' diff --git a/01-Authorization/requirements.md b/01-Authorization/requirements.md new file mode 100644 index 0000000..b981e40 --- /dev/null +++ b/01-Authorization/requirements.md @@ -0,0 +1,13 @@ +# Updating Dependencies + +## Install the Tools + +```shell +pip3 install pipreqs && pip3 install pip-tools +``` + +## Run the Tools + +```shell +pipreqs --savepath=requirements.in && pip-compile --no-annotate --no-header --upgrade --rebuild --quiet --resolver=backtracking +``` diff --git a/01-Authorization/requirements.txt b/01-Authorization/requirements.txt index 03b971d..c634dab 100644 --- a/01-Authorization/requirements.txt +++ b/01-Authorization/requirements.txt @@ -1,9 +1,5 @@ -django~=2.2.7 -djangorestframework~=3.13.1 -django-cors-headers~=3.1.1 -drf-jwt~=1.13.3 -pyjwt~=1.7.1 -python-dotenv~=0.21.0 -requests~=2.28 -urllib3~=1.26 -cryptography~=3.4 +asgiref==3.7.2 +django==4.2.2 +pyjwt==2.7.0 +python-dotenv==1.0.0 +sqlparse==0.4.4