Skip to content

Commit

Permalink
Merge pull request #120 from ECN/feature-implement-authentication
Browse files Browse the repository at this point in the history
Feature implement authentication
  • Loading branch information
campb303 authored Nov 16, 2020
2 parents 3872dbe + 80804cf commit 7e84b6e
Show file tree
Hide file tree
Showing 21 changed files with 667 additions and 149 deletions.
9 changes: 6 additions & 3 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,17 @@
# Testing
/coverage

# Productiom
# React Build Files
/build

# Misc
.DS_Store
# React local environment files
.env.local
.env.development.local
.env.test.local
.env.production.local

# Misc
.DS_Store
.vscode/

# Node Package Management
Expand All @@ -28,3 +30,4 @@ yarn-error.log*
/api/venv
__pycache__/
venv-manager.log
/api/.env
143 changes: 124 additions & 19 deletions api/api.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,114 @@
from flask import Flask
from flask import Flask, request, after_this_request
from flask_restful import Api, Resource
from flask_jwt_extended import (
JWTManager, create_access_token, create_refresh_token,
jwt_required, get_jwt_identity, jwt_refresh_token_required,
set_refresh_cookies, unset_refresh_cookies
)
from werkzeug.security import check_password_hash
import os, dotenv
import ECNQueue

# Load envrionment variables for ./.env
dotenv.load_dotenv()

# Create Flask App
app = Flask(__name__)

# Create API Interface
api = Api(app)


################################################################################
# Configure Flask-JWT-Extended
################################################################################

# Set JWT secret key and create JWT manager
app.config["JWT_SECRET_KEY"] = os.environ.get("JWT_SECRET_KEY")
# Set identity claim field key to sub for JWT RFC complience
# Flask-JWT-Extended uses 'identity' by default for compatibility reasons
app.config["JWT_IDENTITY_CLAIM"] = "sub"
# Set the key for error messages generated by Flask-JWT-Extended
app.config["JWT_ERROR_MESSAGE_KEY"] = "message"

# Look for JWTs in headers (for access) then cookies (for refresh)
app.config["JWT_TOKEN_LOCATION"] = ["headers", "cookies"]
# Restrict cookies to HTTPS in prod, allow HTTP in dev
app.config["JWT_COOKIE_SECURE"] = False if os.environ.get("ENVIRONMENT") == "dev" else True
# Restrict cookies using SameSite=strict flag
app.config["JWT_COOKIE_SAMESITE"] = "strict"
# Restrict refresh tokens to /token/refresh endpoint
app.config["JWT_REFRESH_COOKIE_PATH"] = '/tokens/refresh'
# Set the cookie key for CRSF validation string
# This is the default value. Adding it for easy reference
app.config["JWT_REFRESH_CSRF_HEADER_NAME"] = "X-CSRF-TOKEN"

tokenManager = JWTManager(app)



class Login(Resource):
def post(self) -> tuple:
"""Validates username/password and returns both access and refresh tokens.
Return Codes:
200 (OK): On success.
401 (Unauthroized): When username or password are incorrect.
422 (Unprocessable Entitiy): When the username or password can't be parsed.
Example:
curl -X POST
-H "Content-Type: application/json"
-d '{"username": "bob", "password": "super_secret"}'
{ "access_token": fjr09hfp09h932jp9ruj3.3r8ihf8h0w8hr08ifhj804h8i.8h48ith08ity409hip0t4 }
Returns:
tuple: Response containing tokens and HTTP response code.
"""
if not request.is_json:
return ({ "message": "JSON missing from request body"}, 422)

data = request.json

fields_to_check = ["username", "password"]
for field in fields_to_check:
if field not in data.keys():
return ({ "message": f"{field} missing from request body"}, 422)

if data["username"] != os.environ.get("SHARED_USERNAME"):
return ({ "message": "Username invalid"}, 401)
if not check_password_hash(os.environ.get("SHARED_PASSWORD_HASH"), data["password"]):
return ({ "message": "Password invalid"}, 401)

access_token = create_access_token(data["username"])
refresh_token = create_refresh_token(data["username"])

# This decorator is needed because Flask-RESTful's 'resourceful routing`
# doesn't allow for direct modification to the Flask response object.
# See: https://flask-restful.readthedocs.io/en/latest/quickstart.html#resourceful-routing
@after_this_request
def _does_this_work(response):
set_refresh_cookies(response, refresh_token)
return response

return ({ "access_token": access_token }, 200)

class RefreshAccessToken(Resource):
@jwt_refresh_token_required
def post(self):
username = get_jwt_identity()
access_token = create_access_token(username)
return ({"access_token": access_token}, 200)

class Item(Resource):
def get(self, queue: str, number: int) -> str:
@jwt_required
def get(self, queue: str, number: int) -> tuple:
"""Returns the JSON representation of the item requested.
Return Codes:
200 (OK): On success.
Example:
/api/ce/100 returns:
{
Expand All @@ -38,32 +133,40 @@ def get(self, queue: str, number: int) -> str:
item (int): The number of the item requested.
Returns:
str: JSON representation of the item requested.
tuple: Item as JSON and HTTP response code.
"""
return ECNQueue.Item(queue, number).toJson()
return (ECNQueue.Item(queue, number).toJson(), 200)

class Queue(Resource):
def get(self, queue: str) -> str:
@jwt_required
def get(self, queues: str) -> tuple:
"""Returns the JSON representation of the queue requested.
Return Codes:
200 (OK): On success.
Args:
queue (str): The queue requested.
queues (str): Plus (+) deliminited list of queues.
Returns:
str: JSON representation of the queue requested.
tuple: Queues as JSON and HTTP response code.
"""
queues_requested = queue.split("+")
queues_requested = queues.split("+")

queues = []
queue_list = []
for queue in queues_requested:
queues.append(ECNQueue.Queue(queue).toJson())
queue_list.append(ECNQueue.Queue(queue).toJson())

return queues
return (queue_list, 200)

class QueueList(Resource):
def get(self) -> list:
@jwt_required
def get(self) -> tuple:
"""Returns a list of dictionaries with the number of items in each queue.
Return Codes:
200 (OK): On success.
Example:
[
{
Expand All @@ -77,15 +180,17 @@ def get(self) -> list:
]
Returns:
list: Dictionaries with the number of items in each queue.
tuple: Queues and item counts as JSON and HTTP response code.
"""
return ECNQueue.getQueueCounts()

api.add_resource(QueueList, "/api/get_queues")
api.add_resource(Item, "/api/<string:queue>/<int:number>")
api.add_resource(Queue, "/api/<string:queue>")
return (ECNQueue.getQueueCounts(), 200)



api.add_resource(Login, "/login")
api.add_resource(RefreshAccessToken, "/tokens/refresh")
api.add_resource(Item, "/api/<string:queue>/<int:number>")
api.add_resource(Queue, "/api/<string:queues>")
api.add_resource(QueueList, "/api/get_queues")

if __name__ == "__main__":
app.run()
app.run()
2 changes: 2 additions & 0 deletions api/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ astroid==2.4.2
click==7.1.2
Flask==1.1.2
Flask-RESTful==0.3.8
Flask-JWT-Extended==3.24.1
gunicorn==20.0.4
isort==4.3.21
itsdangerous==1.1.0
Expand All @@ -12,6 +13,7 @@ MarkupSafe==1.1.1
mccabe==0.6.1
pylint==2.5.3
python-dateutil==2.8.1
python-dotenv==0.15.0
pytz==2020.1
six==1.15.0
toml==0.10.1
Expand Down
33 changes: 33 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,12 @@
"history": "^5.0.0",
"material-table": "^1.63.1",
"react": "^16.13.1",
"react-cookie": "^4.0.3",
"react-dom": "^16.13.1",
"react-relative-time": "0.0.7",
"react-router-dom": "^5.2.0",
"react-scripts": "3.4.1",
"react-table": "^7.5.1",
"react-router-dom": "^5.2.0"
"react-table": "^7.5.1"
},
"scripts": {
"start:frontend": "react-scripts start",
Expand Down
Loading

0 comments on commit 7e84b6e

Please sign in to comment.