Skip to content

Feature implement authentication #120

Merged
merged 31 commits into from
Nov 16, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
1de44ad
Add pyjwt to API requirements
Nov 4, 2020
0398df2
Add dotenv support to API
Nov 5, 2020
f6a15d3
Update gitignore to include api dotenv files
Nov 5, 2020
f9dd838
Add python-dotenv to pip requirements
Nov 5, 2020
982ce7b
Replace pyjwt with Flask-JWT-Extended
Nov 5, 2020
4e8fae4
Implement basic shared username/password auth using dotenv
Nov 8, 2020
dcf3c3d
Implement login, token generation and route protection with JWTs
Nov 8, 2020
271caab
Implement /tokens/refresh endpoint to get new access tokens
Nov 8, 2020
ba3c9b7
Update Login resource docs
Nov 10, 2020
b75f3d7
Make Item resource return tuple w/ quueue JSON and HTTP return code p…
Nov 10, 2020
5c5a100
Update Queue resource to return a tuple of data and HTTP response cod…
Nov 10, 2020
ffc049b
Correct refactoring error that broke Queue resource
Nov 10, 2020
5b752de
Update QueueList docs
Nov 11, 2020
4757b28
Add JWT_REFRESH_CRSF_HEADER for easier reference
Nov 11, 2020
4e40b02
Create base LoginForm -- nonfunctional
Nov 13, 2020
ca130f8
Create base auth utilities -- only login
Nov 13, 2020
50f03ec
Create AuthProvider component
Nov 13, 2020
4e79718
Create PrivateRoute component for checking auth
Nov 13, 2020
58e6b96
Move previous App component to AppView
Nov 13, 2020
d20a3b4
Update app entrypoint for auth routing
Nov 13, 2020
c3e9d2c
Include AuthProvider
Nov 13, 2020
34f56ae
Integrate API login and error handling
Nov 13, 2020
3334517
Add auth headers to API requests
Nov 14, 2020
27d51fa
Add view password toggle to login screen
Nov 15, 2020
8486ec5
Intensify login screen gradient
Nov 15, 2020
170a87d
Fix spelling error in token refresh endpoint
Nov 16, 2020
4a79cb4
Add token refresh function to auth utilities
Nov 16, 2020
64decd9
Add react-cookies to project
Nov 16, 2020
5f65436
Add CookieProvider
Nov 16, 2020
5e77e49
Remove debug calls
Nov 16, 2020
80804cf
Implement automatic token refresh
Nov 16, 2020
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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