├── .flaskenv ├── .gitignore ├── README.md ├── api ├── __init__.py ├── exception_views.py ├── messages │ ├── __init__.py │ ├── message.py │ ├── messages_service.py │ └── messages_views.py ├── security │ ├── __init__.py │ ├── auth0_service.py │ └── guards.py └── utils.py └── requirements.txt /.flaskenv: -------------------------------------------------------------------------------- 1 | FLASK_APP=api 2 | FLASK_RUN_PORT=6060 3 | FLASK_ENV=development -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.toptal.com/developers/gitignore/api/macos,windows,visualstudiocode,jetbrains+all,node,python,flask 3 | # Edit at https://www.toptal.com/developers/gitignore?templates=macos,windows,visualstudiocode,jetbrains+all,node,python,flask 4 | 5 | ### Flask ### 6 | instance/* 7 | !instance/.gitignore 8 | .webassets-cache 9 | .env 10 | 11 | ### Flask.Python Stack ### 12 | # Byte-compiled / optimized / DLL files 13 | __pycache__/ 14 | *.py[cod] 15 | *$py.class 16 | 17 | # C extensions 18 | *.so 19 | 20 | # Distribution / packaging 21 | .Python 22 | build/ 23 | develop-eggs/ 24 | dist/ 25 | downloads/ 26 | eggs/ 27 | .eggs/ 28 | lib/ 29 | lib64/ 30 | parts/ 31 | sdist/ 32 | var/ 33 | wheels/ 34 | share/python-wheels/ 35 | *.egg-info/ 36 | .installed.cfg 37 | *.egg 38 | MANIFEST 39 | 40 | # PyInstaller 41 | # Usually these files are written by a python script from a template 42 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 43 | *.manifest 44 | *.spec 45 | 46 | # Installer logs 47 | pip-log.txt 48 | pip-delete-this-directory.txt 49 | 50 | # Unit test / coverage reports 51 | htmlcov/ 52 | .tox/ 53 | .nox/ 54 | .coverage 55 | .coverage.* 56 | .cache 57 | nosetests.xml 58 | coverage.xml 59 | *.cover 60 | *.py,cover 61 | .hypothesis/ 62 | .pytest_cache/ 63 | cover/ 64 | 65 | # Translations 66 | *.mo 67 | *.pot 68 | 69 | # Django stuff: 70 | *.log 71 | local_settings.py 72 | db.sqlite3 73 | db.sqlite3-journal 74 | 75 | # Flask stuff: 76 | instance/ 77 | 78 | # Scrapy stuff: 79 | .scrapy 80 | 81 | # Sphinx documentation 82 | docs/_build/ 83 | 84 | # PyBuilder 85 | .pybuilder/ 86 | target/ 87 | 88 | # Jupyter Notebook 89 | .ipynb_checkpoints 90 | 91 | # IPython 92 | profile_default/ 93 | ipython_config.py 94 | 95 | # pyenv 96 | # For a library or package, you might want to ignore these files since the code is 97 | # intended to run in multiple environments; otherwise, check them in: 98 | # .python-version 99 | 100 | # pipenv 101 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 102 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 103 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 104 | # install all needed dependencies. 105 | #Pipfile.lock 106 | 107 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 108 | __pypackages__/ 109 | 110 | # Celery stuff 111 | celerybeat-schedule 112 | celerybeat.pid 113 | 114 | # SageMath parsed files 115 | *.sage.py 116 | 117 | # Environments 118 | .venv 119 | env/ 120 | venv/ 121 | ENV/ 122 | env.bak/ 123 | venv.bak/ 124 | 125 | # Spyder project settings 126 | .spyderproject 127 | .spyproject 128 | 129 | # Rope project settings 130 | .ropeproject 131 | 132 | # mkdocs documentation 133 | /site 134 | 135 | # mypy 136 | .mypy_cache/ 137 | .dmypy.json 138 | dmypy.json 139 | 140 | # Pyre type checker 141 | .pyre/ 142 | 143 | # pytype static type analyzer 144 | .pytype/ 145 | 146 | # Cython debug symbols 147 | cython_debug/ 148 | 149 | ### JetBrains+all ### 150 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider 151 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 152 | 153 | # User-specific stuff 154 | .idea/**/workspace.xml 155 | .idea/**/tasks.xml 156 | .idea/**/usage.statistics.xml 157 | .idea/**/dictionaries 158 | .idea/**/shelf 159 | 160 | # AWS User-specific 161 | .idea/**/aws.xml 162 | 163 | # Generated files 164 | .idea/**/contentModel.xml 165 | 166 | # Sensitive or high-churn files 167 | .idea/**/dataSources/ 168 | .idea/**/dataSources.ids 169 | .idea/**/dataSources.local.xml 170 | .idea/**/sqlDataSources.xml 171 | .idea/**/dynamic.xml 172 | .idea/**/uiDesigner.xml 173 | .idea/**/dbnavigator.xml 174 | 175 | # Gradle 176 | .idea/**/gradle.xml 177 | .idea/**/libraries 178 | 179 | # Gradle and Maven with auto-import 180 | # When using Gradle or Maven with auto-import, you should exclude module files, 181 | # since they will be recreated, and may cause churn. Uncomment if using 182 | # auto-import. 183 | # .idea/artifacts 184 | # .idea/compiler.xml 185 | # .idea/jarRepositories.xml 186 | # .idea/modules.xml 187 | # .idea/*.iml 188 | # .idea/modules 189 | # *.iml 190 | # *.ipr 191 | 192 | # CMake 193 | cmake-build-*/ 194 | 195 | # Mongo Explorer plugin 196 | .idea/**/mongoSettings.xml 197 | 198 | # File-based project format 199 | *.iws 200 | 201 | # IntelliJ 202 | out/ 203 | 204 | # mpeltonen/sbt-idea plugin 205 | .idea_modules/ 206 | 207 | # JIRA plugin 208 | atlassian-ide-plugin.xml 209 | 210 | # Cursive Clojure plugin 211 | .idea/replstate.xml 212 | 213 | # Crashlytics plugin (for Android Studio and IntelliJ) 214 | com_crashlytics_export_strings.xml 215 | crashlytics.properties 216 | crashlytics-build.properties 217 | fabric.properties 218 | 219 | # Editor-based Rest Client 220 | .idea/httpRequests 221 | 222 | # Android studio 3.1+ serialized cache file 223 | .idea/caches/build_file_checksums.ser 224 | 225 | ### JetBrains+all Patch ### 226 | # Ignores the whole .idea folder and all .iml files 227 | # See https://github.com/joeblau/gitignore.io/issues/186 and https://github.com/joeblau/gitignore.io/issues/360 228 | 229 | .idea/ 230 | 231 | # Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-249601023 232 | 233 | *.iml 234 | modules.xml 235 | .idea/misc.xml 236 | *.ipr 237 | 238 | # Sonarlint plugin 239 | .idea/sonarlint 240 | 241 | ### macOS ### 242 | # General 243 | .DS_Store 244 | .AppleDouble 245 | .LSOverride 246 | 247 | # Icon must end with two \r 248 | Icon 249 | 250 | 251 | # Thumbnails 252 | ._* 253 | 254 | # Files that might appear in the root of a volume 255 | .DocumentRevisions-V100 256 | .fseventsd 257 | .Spotlight-V100 258 | .TemporaryItems 259 | .Trashes 260 | .VolumeIcon.icns 261 | .com.apple.timemachine.donotpresent 262 | 263 | # Directories potentially created on remote AFP share 264 | .AppleDB 265 | .AppleDesktop 266 | Network Trash Folder 267 | Temporary Items 268 | .apdisk 269 | 270 | ### Node ### 271 | # Logs 272 | logs 273 | npm-debug.log* 274 | yarn-debug.log* 275 | yarn-error.log* 276 | lerna-debug.log* 277 | .pnpm-debug.log* 278 | 279 | # Diagnostic reports (https://nodejs.org/api/report.html) 280 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 281 | 282 | # Runtime data 283 | pids 284 | *.pid 285 | *.seed 286 | *.pid.lock 287 | 288 | # Directory for instrumented libs generated by jscoverage/JSCover 289 | lib-cov 290 | 291 | # Coverage directory used by tools like istanbul 292 | coverage 293 | *.lcov 294 | 295 | # nyc test coverage 296 | .nyc_output 297 | 298 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 299 | .grunt 300 | 301 | # Bower dependency directory (https://bower.io/) 302 | bower_components 303 | 304 | # node-waf configuration 305 | .lock-wscript 306 | 307 | # Compiled binary addons (https://nodejs.org/api/addons.html) 308 | build/Release 309 | 310 | # Dependency directories 311 | node_modules/ 312 | jspm_packages/ 313 | 314 | # Snowpack dependency directory (https://snowpack.dev/) 315 | web_modules/ 316 | 317 | # TypeScript cache 318 | *.tsbuildinfo 319 | 320 | # Optional npm cache directory 321 | .npm 322 | 323 | # Optional eslint cache 324 | .eslintcache 325 | 326 | # Microbundle cache 327 | .rpt2_cache/ 328 | .rts2_cache_cjs/ 329 | .rts2_cache_es/ 330 | .rts2_cache_umd/ 331 | 332 | # Optional REPL history 333 | .node_repl_history 334 | 335 | # Output of 'npm pack' 336 | *.tgz 337 | 338 | # Yarn Integrity file 339 | .yarn-integrity 340 | 341 | # dotenv environment variables file 342 | .env.test 343 | .env.production 344 | 345 | # parcel-bundler cache (https://parceljs.org/) 346 | .parcel-cache 347 | 348 | # Next.js build output 349 | .next 350 | out 351 | 352 | # Nuxt.js build / generate output 353 | .nuxt 354 | dist 355 | 356 | # Gatsby files 357 | .cache/ 358 | # Comment in the public line in if your project uses Gatsby and not Next.js 359 | # https://nextjs.org/blog/next-9-1#public-directory-support 360 | # public 361 | 362 | # vuepress build output 363 | .vuepress/dist 364 | 365 | # Serverless directories 366 | .serverless/ 367 | 368 | # FuseBox cache 369 | .fusebox/ 370 | 371 | # DynamoDB Local files 372 | .dynamodb/ 373 | 374 | # TernJS port file 375 | .tern-port 376 | 377 | # Stores VSCode versions used for testing VSCode extensions 378 | .vscode-test 379 | 380 | # yarn v2 381 | .yarn/cache 382 | .yarn/unplugged 383 | .yarn/build-state.yml 384 | .yarn/install-state.gz 385 | .pnp.* 386 | 387 | ### Node Patch ### 388 | # Serverless Webpack directories 389 | .webpack/ 390 | 391 | ### Python ### 392 | # Byte-compiled / optimized / DLL files 393 | 394 | # C extensions 395 | 396 | # Distribution / packaging 397 | 398 | # PyInstaller 399 | # Usually these files are written by a python script from a template 400 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 401 | 402 | # Installer logs 403 | 404 | # Unit test / coverage reports 405 | 406 | # Translations 407 | 408 | # Django stuff: 409 | 410 | # Flask stuff: 411 | 412 | # Scrapy stuff: 413 | 414 | # Sphinx documentation 415 | 416 | # PyBuilder 417 | 418 | # Jupyter Notebook 419 | 420 | # IPython 421 | 422 | # pyenv 423 | # For a library or package, you might want to ignore these files since the code is 424 | # intended to run in multiple environments; otherwise, check them in: 425 | # .python-version 426 | 427 | # pipenv 428 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 429 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 430 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 431 | # install all needed dependencies. 432 | 433 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 434 | 435 | # Celery stuff 436 | 437 | # SageMath parsed files 438 | 439 | # Environments 440 | 441 | # Spyder project settings 442 | 443 | # Rope project settings 444 | 445 | # mkdocs documentation 446 | 447 | # mypy 448 | 449 | # Pyre type checker 450 | 451 | # pytype static type analyzer 452 | 453 | # Cython debug symbols 454 | 455 | ### VisualStudioCode ### 456 | .vscode/* 457 | !.vscode/settings.json 458 | !.vscode/tasks.json 459 | !.vscode/launch.json 460 | !.vscode/extensions.json 461 | *.code-workspace 462 | 463 | # Local History for Visual Studio Code 464 | .history/ 465 | 466 | ### VisualStudioCode Patch ### 467 | # Ignore all local history of files 468 | .history 469 | .ionide 470 | 471 | ### Windows ### 472 | # Windows thumbnail cache files 473 | Thumbs.db 474 | Thumbs.db:encryptable 475 | ehthumbs.db 476 | ehthumbs_vista.db 477 | 478 | # Dump file 479 | *.stackdump 480 | 481 | # Folder config file 482 | [Dd]esktop.ini 483 | 484 | # Recycle Bin used on file shares 485 | $RECYCLE.BIN/ 486 | 487 | # Windows Installer files 488 | *.cab 489 | *.msi 490 | *.msix 491 | *.msm 492 | *.msp 493 | 494 | # Windows shortcuts 495 | *.lnk 496 | 497 | # End of https://www.toptal.com/developers/gitignore/api/macos,windows,visualstudiocode,jetbrains+all,node,python,flask -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Hello World API: Flask Auth0 Sample 2 | 3 | This Python code sample demonstrates how to implement authorization in a Flask API server using Auth0. 4 | 5 | ## Run the Project 6 | 7 | Create a virtual environment under the root project directory: 8 | 9 | **macOS/Linux:** 10 | 11 | ```bash 12 | python3 -m venv venv 13 | ``` 14 | 15 | **Windows:** 16 | 17 | ```bash 18 | py -3 -m venv venv 19 | ``` 20 | 21 | Activate the virtual environment: 22 | 23 | **macOS/Linux:** 24 | 25 | ```bash 26 | . venv/bin/activate 27 | ``` 28 | 29 | **Windows:** 30 | 31 | ```bash 32 | venv\Scripts\activate 33 | ``` 34 | 35 | Install the project dependencies: 36 | 37 | ```bash 38 | pip install -r requirements.txt 39 | ``` 40 | 41 | Create a `.env` file under the root project directory and populate it with the following content: 42 | 43 | ```bash 44 | CLIENT_ORIGIN_URL=http://localhost:4040 45 | AUTH0_AUDIENCE= 46 | AUTH0_DOMAIN= 47 | ``` 48 | 49 | Run the project in development mode: 50 | 51 | ```bash 52 | flask run 53 | ``` 54 | 55 | ## API Endpoints 56 | 57 | The API server defines the following endpoints: 58 | 59 | ### 🔓 Get public message 60 | 61 | ```bash 62 | GET /api/messages/public 63 | ``` 64 | 65 | #### Response 66 | 67 | ```bash 68 | Status: 200 OK 69 | ``` 70 | 71 | ```json 72 | { 73 | "message": "The API doesn't require an access token to share this message." 74 | } 75 | ``` 76 | 77 | ### 🔐 Get protected message 78 | 79 | ```bash 80 | GET /api/messages/protected 81 | ``` 82 | 83 | #### Response 84 | 85 | ```bash 86 | Status: 200 OK 87 | ``` 88 | 89 | ```json 90 | { 91 | "message": "The API successfully validated your access token." 92 | } 93 | ``` 94 | 95 | ### 🔐 Get admin message 96 | 97 | ```bash 98 | GET /api/messages/admin 99 | ``` 100 | 101 | #### Response 102 | 103 | ```bash 104 | Status: 200 OK 105 | ``` 106 | 107 | ```json 108 | { 109 | "message": "The API successfully recognized you as an admin." 110 | } 111 | ``` 112 | -------------------------------------------------------------------------------- /api/__init__.py: -------------------------------------------------------------------------------- 1 | ########################################## 2 | # External Modules 3 | ########################################## 4 | 5 | import os 6 | 7 | from flask import Flask 8 | from flask_cors import CORS 9 | from flask_talisman import Talisman 10 | 11 | from api import exception_views 12 | from api.messages import messages_views 13 | from api.security.auth0_service import auth0_service 14 | 15 | 16 | def create_app(): 17 | ########################################## 18 | # Environment Variables 19 | ########################################## 20 | client_origin_url = os.environ.get("CLIENT_ORIGIN_URL") 21 | auth0_audience = os.environ.get("AUTH0_AUDIENCE") 22 | auth0_domain = os.environ.get("AUTH0_DOMAIN") 23 | 24 | if not (client_origin_url and auth0_audience and auth0_domain): 25 | raise NameError("The required environment variables are missing. Check .env file.") 26 | 27 | ########################################## 28 | # Flask App Instance 29 | ########################################## 30 | 31 | app = Flask(__name__, instance_relative_config=True) 32 | 33 | ########################################## 34 | # HTTP Security Headers 35 | ########################################## 36 | 37 | csp = { 38 | 'default-src': ['\'self\''], 39 | 'frame-ancestors': ['\'none\''] 40 | } 41 | 42 | Talisman(app, 43 | frame_options='DENY', 44 | content_security_policy=csp, 45 | referrer_policy='no-referrer' 46 | ) 47 | 48 | auth0_service.initialize(auth0_domain, auth0_audience) 49 | 50 | @app.after_request 51 | def add_headers(response): 52 | response.headers['X-XSS-Protection'] = '0' 53 | response.headers['Cache-Control'] = 'no-store, max-age=0' 54 | response.headers['Pragma'] = 'no-cache' 55 | response.headers['Expires'] = '0' 56 | response.headers['Content-Type'] = 'application/json; charset=utf-8' 57 | return response 58 | 59 | ########################################## 60 | # CORS 61 | ########################################## 62 | 63 | CORS( 64 | app, 65 | resources={r"/api/*": {"origins": client_origin_url}}, 66 | allow_headers=["Authorization", "Content-Type"], 67 | methods=["GET"], 68 | max_age=86400 69 | ) 70 | 71 | ########################################## 72 | # Blueprint Registration 73 | ########################################## 74 | 75 | app.register_blueprint(messages_views.bp) 76 | app.register_blueprint(exception_views.bp) 77 | 78 | return app 79 | -------------------------------------------------------------------------------- /api/exception_views.py: -------------------------------------------------------------------------------- 1 | from flask import ( 2 | Blueprint, request, jsonify 3 | ) 4 | from werkzeug import exceptions 5 | 6 | bp_name = 'exceptions' 7 | bp = Blueprint(bp_name, __name__) 8 | 9 | 10 | @bp.app_errorhandler(exceptions.InternalServerError) 11 | def _handle_internal_server_error(ex): 12 | if request.path.startswith('/api/'): 13 | return jsonify(message=str(ex)), ex.code 14 | else: 15 | return ex 16 | 17 | 18 | @bp.app_errorhandler(exceptions.NotFound) 19 | def _handle_not_found_error(ex): 20 | if request.path.startswith('/api/'): 21 | return {"message": "Not Found"}, ex.code 22 | else: 23 | return ex 24 | -------------------------------------------------------------------------------- /api/messages/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/auth0-developer-hub/api_flask_python_hello-world/31de22d973dc8545d89c37e36f92ef0330b5c6aa/api/messages/__init__.py -------------------------------------------------------------------------------- /api/messages/message.py: -------------------------------------------------------------------------------- 1 | class Message: 2 | def __init__(self, text): 3 | self.text = text 4 | -------------------------------------------------------------------------------- /api/messages/messages_service.py: -------------------------------------------------------------------------------- 1 | from api.messages.message import Message 2 | 3 | 4 | def get_public_message(): 5 | return Message( 6 | "The API doesn't require an access token to share this message." 7 | ) 8 | 9 | 10 | def get_protected_message(): 11 | return Message( 12 | "The API successfully validated your access token." 13 | ) 14 | 15 | 16 | def get_admin_message(): 17 | return Message( 18 | "The API successfully recognized you as an admin." 19 | ) 20 | -------------------------------------------------------------------------------- /api/messages/messages_views.py: -------------------------------------------------------------------------------- 1 | from flask import ( 2 | Blueprint 3 | ) 4 | 5 | from api.messages.messages_service import ( 6 | get_public_message, 7 | get_protected_message, 8 | get_admin_message 9 | ) 10 | from api.security.guards import ( 11 | authorization_guard, 12 | permissions_guard, 13 | admin_messages_permissions 14 | ) 15 | 16 | bp_name = 'api-messages' 17 | bp_url_prefix = '/api/messages' 18 | bp = Blueprint(bp_name, __name__, url_prefix=bp_url_prefix) 19 | 20 | 21 | @bp.route("/public") 22 | def public(): 23 | return { 24 | "text": get_public_message().text 25 | } 26 | 27 | 28 | @bp.route("/protected") 29 | @authorization_guard 30 | def protected(): 31 | return { 32 | "text": get_protected_message().text 33 | } 34 | 35 | 36 | @bp.route("/admin") 37 | @authorization_guard 38 | @permissions_guard([admin_messages_permissions.read]) 39 | def admin(): 40 | return { 41 | "text": get_admin_message().text 42 | } 43 | -------------------------------------------------------------------------------- /api/security/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/auth0-developer-hub/api_flask_python_hello-world/31de22d973dc8545d89c37e36f92ef0330b5c6aa/api/security/__init__.py -------------------------------------------------------------------------------- /api/security/auth0_service.py: -------------------------------------------------------------------------------- 1 | from http import HTTPStatus 2 | 3 | import jwt 4 | 5 | from api.utils import json_abort 6 | 7 | 8 | class Auth0Service: 9 | """Perform JSON Web Token (JWT) validation using PyJWT""" 10 | 11 | def __init__(self): 12 | self.issuer_url = None 13 | self.audience = None 14 | self.algorithm = 'RS256' 15 | self.jwks_uri = None 16 | 17 | def initialize(self, auth0_domain, auth0_audience): 18 | self.issuer_url = f'https://{auth0_domain}/' 19 | self.jwks_uri = f'{self.issuer_url}.well-known/jwks.json' 20 | self.audience = auth0_audience 21 | 22 | def get_signing_key(self, token): 23 | try: 24 | jwks_client = jwt.PyJWKClient(self.jwks_uri) 25 | 26 | return jwks_client.get_signing_key_from_jwt(token).key 27 | except Exception as error: 28 | json_abort(HTTPStatus.INTERNAL_SERVER_ERROR, { 29 | "error": "signing_key_unavailable", 30 | "error_description": error.__str__(), 31 | "message": "Unable to verify credentials" 32 | }) 33 | 34 | def validate_jwt(self, token): 35 | try: 36 | jwt_signing_key = self.get_signing_key(token) 37 | 38 | payload = jwt.decode( 39 | token, 40 | jwt_signing_key, 41 | algorithms=self.algorithm, 42 | audience=self.audience, 43 | issuer=self.issuer_url, 44 | ) 45 | except Exception as error: 46 | json_abort(HTTPStatus.UNAUTHORIZED, { 47 | "error": "invalid_token", 48 | "error_description": error.__str__(), 49 | "message": "Bad credentials." 50 | }) 51 | return 52 | 53 | return payload 54 | 55 | 56 | auth0_service = Auth0Service() 57 | -------------------------------------------------------------------------------- /api/security/guards.py: -------------------------------------------------------------------------------- 1 | from functools import wraps 2 | from http import HTTPStatus 3 | from types import SimpleNamespace 4 | 5 | from flask import request, g 6 | 7 | from api.security.auth0_service import auth0_service 8 | from api.utils import json_abort 9 | 10 | unauthorized_error = { 11 | "message": "Requires authentication" 12 | } 13 | 14 | invalid_request_error = { 15 | "error": "invalid_request", 16 | "error_description": "Authorization header value must follow this format: Bearer access-token", 17 | "message": "Requires authentication" 18 | } 19 | 20 | admin_messages_permissions = SimpleNamespace( 21 | read="read:admin-messages" 22 | ) 23 | 24 | 25 | def get_bearer_token_from_request(): 26 | authorization_header = request.headers.get("Authorization", None) 27 | 28 | if not authorization_header: 29 | json_abort(HTTPStatus.UNAUTHORIZED, unauthorized_error) 30 | return 31 | 32 | authorization_header_elements = authorization_header.split() 33 | 34 | if len(authorization_header_elements) != 2: 35 | json_abort(HTTPStatus.BAD_REQUEST, invalid_request_error) 36 | return 37 | 38 | auth_scheme = authorization_header_elements[0] 39 | bearer_token = authorization_header_elements[1] 40 | 41 | if not (auth_scheme and auth_scheme.lower() == "bearer"): 42 | json_abort(HTTPStatus.UNAUTHORIZED, unauthorized_error) 43 | return 44 | 45 | if not bearer_token: 46 | json_abort(HTTPStatus.UNAUTHORIZED, unauthorized_error) 47 | return 48 | 49 | return bearer_token 50 | 51 | 52 | def authorization_guard(function): 53 | @wraps(function) 54 | def decorator(*args, **kwargs): 55 | token = get_bearer_token_from_request() 56 | validated_token = auth0_service.validate_jwt(token) 57 | 58 | g.access_token = validated_token 59 | 60 | return function(*args, **kwargs) 61 | 62 | return decorator 63 | 64 | 65 | def permissions_guard(required_permissions=None): 66 | def decorator(function): 67 | @wraps(function) 68 | def wrapper(): 69 | access_token = g.get("access_token") 70 | 71 | if not access_token: 72 | json_abort(401, unauthorized_error) 73 | return 74 | 75 | if required_permissions is None: 76 | return function() 77 | 78 | if not isinstance(required_permissions, list): 79 | json_abort(500, { 80 | "message": "Internal Server Error" 81 | }) 82 | 83 | token_permissions = access_token.get("permissions") 84 | 85 | if not token_permissions: 86 | json_abort(403, { 87 | "message": "Permission denied" 88 | }) 89 | 90 | required_permissions_set = set(required_permissions) 91 | token_permissions_set = set(token_permissions) 92 | 93 | if not required_permissions_set.issubset(token_permissions_set): 94 | json_abort(403, { 95 | "message": "Permission denied" 96 | }) 97 | 98 | return function() 99 | 100 | return wrapper 101 | 102 | return decorator 103 | -------------------------------------------------------------------------------- /api/utils.py: -------------------------------------------------------------------------------- 1 | from flask import jsonify, abort 2 | 3 | 4 | def json_abort(status_code, data=None): 5 | response = jsonify(data) 6 | response.status_code = status_code 7 | abort(response) -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | cffi==1.15.0 2 | click==8.0.3 3 | cryptography==35.0.0 4 | Flask==2.0.2 5 | Flask-Cors==3.0.10 6 | flask-talisman==0.8.1 7 | itsdangerous==2.0.1 8 | Jinja2==3.0.3 9 | MarkupSafe==2.0.1 10 | pycparser==2.21 11 | PyJWT==2.3.0 12 | python-dotenv==0.19.1 13 | six==1.16.0 14 | Werkzeug==2.0.2 15 | --------------------------------------------------------------------------------