added JWT Authentication (expiring bearer token)

This commit is contained in:
Daniel Schick 2023-06-26 08:38:45 +02:00
parent b9d35b9244
commit 3f211919af
11 changed files with 166 additions and 21 deletions

View File

@ -2,10 +2,52 @@
"info": { "info": {
"_postman_id": "9242b2d1-196b-4b2e-af57-c0e9eb141dba", "_postman_id": "9242b2d1-196b-4b2e-af57-c0e9eb141dba",
"name": "BreCal", "name": "BreCal",
"description": "Bremen Calling relevant API calls",
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json",
"_exporter_id": "10427908" "_exporter_id": "10427908"
}, },
"item": [ "item": [
{
"name": "Login user",
"event": [
{
"listen": "test",
"script": {
"exec": [
"pm.environment.set(\"LOGON_TOKEN\", responseBody)"
],
"type": "text/javascript"
}
}
],
"request": {
"auth": {
"type": "noauth"
},
"method": "POST",
"header": [],
"body": {
"mode": "raw",
"raw": "{\r\n \"username\" : \"Londo\",\r\n \"password\" : \"Hallowach\"\r\n}",
"options": {
"raw": {
"language": "json"
}
}
},
"url": {
"raw": "http://{{PATH}}/login",
"protocol": "http",
"host": [
"{{PATH}}"
],
"path": [
"login"
]
}
},
"response": []
},
{ {
"name": "Participant GET", "name": "Participant GET",
"request": { "request": {
@ -14,7 +56,7 @@
"bearer": [ "bearer": [
{ {
"key": "token", "key": "token",
"value": "xxxTest", "value": "{{LOGON_TOKEN}}",
"type": "string" "type": "string"
} }
] ]
@ -48,7 +90,7 @@
"bearer": [ "bearer": [
{ {
"key": "token", "key": "token",
"value": "xxxTest", "value": "{{LOGON_TOKEN}}",
"type": "string" "type": "string"
} }
] ]
@ -76,7 +118,7 @@
"bearer": [ "bearer": [
{ {
"key": "token", "key": "token",
"value": "xxxTest", "value": "{{LOGON_TOKEN}}",
"type": "string" "type": "string"
} }
] ]
@ -113,7 +155,7 @@
"bearer": [ "bearer": [
{ {
"key": "token", "key": "token",
"value": "xxxTest", "value": "{{LOGON_TOKEN}}",
"type": "string" "type": "string"
} }
] ]
@ -150,7 +192,7 @@
"bearer": [ "bearer": [
{ {
"key": "token", "key": "token",
"value": "xxxTest", "value": "{{LOGON_TOKEN}}",
"type": "string" "type": "string"
} }
] ]
@ -173,6 +215,16 @@
{ {
"name": "Notifications GET", "name": "Notifications GET",
"request": { "request": {
"auth": {
"type": "bearer",
"bearer": [
{
"key": "token",
"value": "{{LOGON_TOKEN}}",
"type": "string"
}
]
},
"method": "GET", "method": "GET",
"header": [], "header": [],
"url": { "url": {
@ -202,7 +254,7 @@
"bearer": [ "bearer": [
{ {
"key": "token", "key": "token",
"value": "xxxTest", "value": "{{LOGON_TOKEN}}",
"type": "string" "type": "string"
} }
] ]
@ -230,7 +282,7 @@
"bearer": [ "bearer": [
{ {
"key": "token", "key": "token",
"value": "xxxTest", "value": "{{LOGON_TOKEN}}",
"type": "string" "type": "string"
} }
] ]
@ -264,7 +316,7 @@
"bearer": [ "bearer": [
{ {
"key": "token", "key": "token",
"value": "xxxTest", "value": "{{LOGON_TOKEN}}",
"type": "string" "type": "string"
} }
] ]
@ -301,7 +353,7 @@
"bearer": [ "bearer": [
{ {
"key": "token", "key": "token",
"value": "xxxTest", "value": "{{LOGON_TOKEN}}",
"type": "string" "type": "string"
} }
] ]
@ -333,7 +385,7 @@
"bearer": [ "bearer": [
{ {
"key": "token", "key": "token",
"value": "xxxTest", "value": "{{LOGON_TOKEN}}",
"type": "string" "type": "string"
} }
] ]
@ -359,5 +411,28 @@
}, },
"response": [] "response": []
} }
],
"auth": {
"type": "bearer"
},
"event": [
{
"listen": "prerequest",
"script": {
"type": "text/javascript",
"exec": [
""
]
}
},
{
"listen": "test",
"script": {
"type": "text/javascript",
"exec": [
""
]
}
}
] ]
} }

View File

@ -2,7 +2,6 @@ from flask import Flask
import os import os
import logging import logging
import secrets
from . import local_db from . import local_db
from .api import shipcalls from .api import shipcalls
@ -48,6 +47,3 @@ def create_app(test_config=None):
logging.info('App started') logging.info('App started')
return app return app
def create_api_key():
return secrets.token_urlsafe(16)

View File

@ -1,12 +1,15 @@
from flask import Blueprint, request from flask import Blueprint, request
from webargs.flaskparser import parser from webargs.flaskparser import parser
from .. import impl from .. import impl
from ..services.auth_guard import auth_guard
import json import json
bp = Blueprint('berths', __name__) bp = Blueprint('berths', __name__)
@bp.route('/berths', methods=['get']) @bp.route('/berths', methods=['get'])
@auth_guard() # no restriction by role
def GetBerths(): def GetBerths():
if 'Authorization' in request.headers: if 'Authorization' in request.headers:

View File

@ -1,5 +1,6 @@
from flask import Blueprint, request from flask import Blueprint, request
from .. import impl from .. import impl
from ..services.auth_guard import auth_guard
import logging import logging
import json import json
@ -7,6 +8,7 @@ bp = Blueprint('notifications', __name__)
@bp.route('/notifications', methods=['get']) @bp.route('/notifications', methods=['get'])
@auth_guard() # no restriction by role
def GetNotifications(): def GetNotifications():
# TODO: verify token # TODO: verify token

View File

@ -1,10 +1,12 @@
from flask import Blueprint, request from flask import Blueprint, request
from .. import impl from .. import impl
from ..services.auth_guard import auth_guard
import json import json
bp = Blueprint('participant', __name__) bp = Blueprint('participant', __name__)
@bp.route('/participant', methods=['get']) @bp.route('/participant', methods=['get'])
@auth_guard() # no restriction by role
def GetParticipant(): def GetParticipant():
if 'Authorization' in request.headers: if 'Authorization' in request.headers:

View File

@ -3,6 +3,7 @@ from webargs.flaskparser import parser
from marshmallow import Schema, fields from marshmallow import Schema, fields
from ..schemas import model from ..schemas import model
from .. import impl from .. import impl
from ..services.auth_guard import auth_guard
import logging import logging
import json import json
@ -12,6 +13,7 @@ bp = Blueprint('shipcalls', __name__)
# TODO: verify token # TODO: verify token
@bp.route('/shipcalls', methods=['get']) @bp.route('/shipcalls', methods=['get'])
@auth_guard() # no restriction by role
def GetShipcalls(): def GetShipcalls():
if 'Authorization' in request.headers: if 'Authorization' in request.headers:
token = request.headers.get('Authorization') token = request.headers.get('Authorization')
@ -24,6 +26,7 @@ def GetShipcalls():
@bp.route('/shipcalls', methods=['post']) @bp.route('/shipcalls', methods=['post'])
@auth_guard() # no restriction by role
def PostShipcalls(): def PostShipcalls():
try: try:
@ -38,6 +41,7 @@ def PostShipcalls():
@bp.route('/shipcalls', methods=['put']) @bp.route('/shipcalls', methods=['put'])
@auth_guard() # no restriction by role
def PutShipcalls(): def PutShipcalls():
try: try:

View File

@ -1,10 +1,12 @@
from flask import Blueprint, request from flask import Blueprint, request
from .. import impl from .. import impl
from ..services.auth_guard import auth_guard
import json import json
bp = Blueprint('ships', __name__) bp = Blueprint('ships', __name__)
@bp.route('/ships', methods=['get']) @bp.route('/ships', methods=['get'])
@auth_guard() # no restriction by role
def GetShips(): def GetShips():
if 'Authentication' in request.headers: if 'Authentication' in request.headers:
token = request.headers.get('Authentication') token = request.headers.get('Authentication')

View File

@ -1,6 +1,7 @@
from flask import Blueprint, request from flask import Blueprint, request
from ..schemas import model from ..schemas import model
from .. import impl from .. import impl
from ..services.auth_guard import auth_guard
import json import json
import logging import logging
@ -8,6 +9,7 @@ bp = Blueprint('times', __name__)
@bp.route('/times', methods=['get']) @bp.route('/times', methods=['get'])
@auth_guard() # no restriction by role
def GetTimes(): def GetTimes():
options = {} options = {}
@ -17,6 +19,7 @@ def GetTimes():
@bp.route('/times', methods=['post']) @bp.route('/times', methods=['post'])
@auth_guard() # no restriction by role
def PostTimes(): def PostTimes():
try: try:
@ -36,6 +39,7 @@ def PostTimes():
@bp.route('/times', methods=['put']) @bp.route('/times', methods=['put'])
@auth_guard() # no restriction by role
def PutTimes(): def PutTimes():
try: try:
@ -51,6 +55,7 @@ def PutTimes():
@bp.route('/times', methods=['delete']) @bp.route('/times', methods=['delete'])
@auth_guard() # no restriction by role
def DeleteTimes(): def DeleteTimes():
# TODO check if I am allowd to delete this thing by deriving the participant from the bearer token # TODO check if I am allowd to delete this thing by deriving the participant from the bearer token

View File

@ -5,6 +5,7 @@ import bcrypt
from ..schemas import model from ..schemas import model
from .. import local_db from .. import local_db
from ..services import jwt_handler
def GetUser(options): def GetUser(options):
@ -15,15 +16,19 @@ def GetUser(options):
commands = pydapper.using(local_db.connection_pool) commands = pydapper.using(local_db.connection_pool)
data = commands.query("SELECT id, participant_id, first_name, last_name, user_name, user_email, user_phone, password_hash, api_key FROM user WHERE user_name = ?username? OR user_email = ?username?", data = commands.query("SELECT id, participant_id, first_name, last_name, user_name, user_email, user_phone, password_hash, api_key FROM user WHERE user_name = ?username? OR user_email = ?username?",
model=model.User, param={"username" : options["username"]}) model=model.User, param={"username" : options["username"]})
print(data) # print(data)
if len(data) == 1: if len(data) == 1:
if bcrypt.checkpw(options["password"].encode("utf-8"), bytes(data[0].password_hash, "utf-8")): if bcrypt.checkpw(options["password"].encode("utf-8"), bytes(data[0].password_hash, "utf-8")):
return json.dumps({ "id": data[0].id, result = {
"participant_id": data[0].participant_id, "id": data[0].id,
"first_name": data[0].first_name, "participant_id": data[0].participant_id,
"last_name": data[0].last_name, "first_name": data[0].first_name,
"user_name": data[0].user_name, "last_name": data[0].last_name,
"user_phone": data[0].user_phone}), 200 "user_name": data[0].user_name,
"user_phone": data[0].user_phone
}
token = jwt_handler.generate_jwt(payload=result, lifetime=60) # generate token valid 60 mins
return token, 200
if len(data) > 1: if len(data) > 1:
return json.dumps("credential lookup mismatch"), 500 return json.dumps("credential lookup mismatch"), 500

View File

@ -0,0 +1,34 @@
import json
from flask import request
from .jwt_handler import decode_jwt
def check_jwt():
# get header and try to get payload
# this will throw an exception if the payload is missing, invalid or expired
token = request.headers.get('Authorization')
if not token:
raise Exception('Missing access token')
jwt = token.split('Bearer ')[1]
try:
return decode_jwt(jwt)
except Exception as e:
raise Exception(f'invalid access token: {e}')
# magic. use this to decorate the api calls
# https://brunotatsuya.dev/blog/jwt-authentication-and-authorization-for-python-flask-rest-apis
def auth_guard(role=None):
def wrapper(route_function):
def decorated_function(*args, **kwargs):
# Authentication gate
try:
user_data = check_jwt()
except Exception as e:
return json.dumps({"message" : f'{e}', "status": 401}), 401
if role and role not in user_data['roles']:
return json.dumps({"message": 'Authorization required.', "status" : 403}), 403
# get on to original route
return route_function(*args, **kwargs)
decorated_function.__name__ = route_function.__name__
return decorated_function
return wrapper

View File

@ -0,0 +1,17 @@
import os
import jwt
import datetime
import secrets
def create_api_key():
return secrets.token_urlsafe(16)
def generate_jwt(payload, lifetime=None):
if lifetime:
payload['exp'] = (datetime.datetime.now() + datetime.timedelta(minutes=lifetime)).timestamp()
return jwt.encode(payload, os.environ.get('SECRET_KEY'), algorithm="HS256")
def decode_jwt(token):
return jwt.decode(token, os.environ.get('SECRET_KEY'), algorithms=["HS256"])