migrating from basic to JWT auth #34

Merged
Sarsoo merged 3 commits from jwt into master 2022-08-09 18:03:52 +01:00
16 changed files with 260 additions and 55 deletions

View File

@ -170,6 +170,10 @@ jobs:
- name: Set GCP Project - name: Set GCP Project
run: python admin.py set_project run: python admin.py set_project
# DEPLOY domain routes
- name: Deploy dispatch.yaml
run: gcloud app deploy dispatch.yaml --quiet
# DEPLOY app engine service, -nb for skipping compile # DEPLOY app engine service, -nb for skipping compile
- name: Deploy App Engine Service - name: Deploy App Engine Service
run: python admin.py app -nb run: python admin.py app -nb

View File

@ -5,7 +5,7 @@ from datetime import datetime
from google.cloud import tasks_v2 from google.cloud import tasks_v2
from music.api.decorators import login_or_basic_auth, admin_required from music.api.decorators import login_or_jwt, admin_required
blueprint = Blueprint('admin-api', __name__) blueprint = Blueprint('admin-api', __name__)
@ -16,9 +16,9 @@ logger = logging.getLogger(__name__)
@blueprint.route('/tasks', methods=['GET']) @blueprint.route('/tasks', methods=['GET'])
@login_or_basic_auth @login_or_jwt
@admin_required @admin_required
def get_tasks(user=None): def get_tasks(auth=None, user=None):
tasks = list(tasker.list_tasks(task_path)) tasks = list(tasker.list_tasks(task_path))

View File

@ -7,7 +7,7 @@ import json
import logging import logging
from datetime import datetime from datetime import datetime
from music.api.decorators import login_required, login_or_basic_auth, \ from music.api.decorators import login_or_jwt, login_required, login_or_jwt, \
admin_required, cloud_task, validate_json, validate_args, spotify_link_required admin_required, cloud_task, validate_json, validate_args, spotify_link_required
from music.cloud import queue_run_user_playlist, offload_or_run_user_playlist from music.cloud import queue_run_user_playlist, offload_or_run_user_playlist
from music.cloud.tasks import update_all_user_playlists, update_playlists from music.cloud.tasks import update_all_user_playlists, update_playlists
@ -28,8 +28,8 @@ logger = logging.getLogger(__name__)
@blueprint.route('/playlists', methods=['GET']) @blueprint.route('/playlists', methods=['GET'])
@login_or_basic_auth @login_or_jwt
def all_playlists_route(user=None): def all_playlists_route(auth=None, user=None):
"""Retrieve all playlists for a given user """Retrieve all playlists for a given user
Args: Args:
@ -46,9 +46,9 @@ def all_playlists_route(user=None):
@blueprint.route('/playlist', methods=['GET', 'DELETE']) @blueprint.route('/playlist', methods=['GET', 'DELETE'])
@login_or_basic_auth @login_or_jwt
@validate_args(('name', str)) @validate_args(('name', str))
def playlist_get_delete_route(user=None): def playlist_get_delete_route(auth=None,user=None):
playlist = user.get_playlist(request.args['name'], raise_error=False) playlist = user.get_playlist(request.args['name'], raise_error=False)
@ -64,9 +64,9 @@ def playlist_get_delete_route(user=None):
@blueprint.route('/playlist', methods=['POST', 'PUT']) @blueprint.route('/playlist', methods=['POST', 'PUT'])
@login_or_basic_auth @login_or_jwt
@validate_json(('name', str)) @validate_json(('name', str))
def playlist_post_put_route(user=None): def playlist_post_put_route(auth=None, user=None):
request_json = request.get_json() request_json = request.get_json()
@ -161,8 +161,8 @@ def playlist_post_put_route(user=None):
@blueprint.route('/user', methods=['GET', 'POST']) @blueprint.route('/user', methods=['GET', 'POST'])
@login_or_basic_auth @login_or_jwt
def user_route(user=None): def user_route(auth=None, user=None):
assert user is not None assert user is not None
if request.method == 'GET': if request.method == 'GET':
@ -202,9 +202,9 @@ def user_route(user=None):
@blueprint.route('/users', methods=['GET']) @blueprint.route('/users', methods=['GET'])
@login_or_basic_auth @login_or_jwt
@admin_required @admin_required
def all_users_route(user=None): def all_users_route(auth=None, user=None):
return jsonify({ return jsonify({
'accounts': [i.to_dict() for i in User.collection.fetch()] 'accounts': [i.to_dict() for i in User.collection.fetch()]
}), 200 }), 200
@ -234,9 +234,9 @@ def change_password(user=None):
@blueprint.route('/playlist/run', methods=['GET']) @blueprint.route('/playlist/run', methods=['GET'])
@login_or_basic_auth @login_or_jwt
@validate_args(('name', str)) @validate_args(('name', str))
def run_playlist(user=None): def run_playlist(auth=None, user=None):
if os.environ.get('DEPLOY_DESTINATION', None) == 'PROD': if os.environ.get('DEPLOY_DESTINATION', None) == 'PROD':
queue_run_user_playlist(user.username, request.args['name']) # pass to either cloud tasks or functions queue_run_user_playlist(user.username, request.args['name']) # pass to either cloud tasks or functions
@ -264,8 +264,8 @@ def run_playlist_task(): # receives cloud tasks request for update
@blueprint.route('/playlist/run/user', methods=['GET']) @blueprint.route('/playlist/run/user', methods=['GET'])
@login_or_basic_auth @login_or_jwt
def run_user(user=None): def run_user(auth=None, user=None):
if user.type == 'admin': if user.type == 'admin':
user_name = request.args.get('username', user.username) user_name = request.args.get('username', user.username)
@ -288,19 +288,19 @@ def run_user_task():
@blueprint.route('/playlist/run/users', methods=['GET']) @blueprint.route('/playlist/run/users', methods=['GET'])
@login_or_basic_auth @login_or_jwt
@admin_required @admin_required
def run_users(user=None): def run_users(auth=None, user=None):
update_all_user_playlists() update_all_user_playlists()
return jsonify({'message': 'executed all users', 'status': 'success'}), 200 return jsonify({'message': 'executed all users', 'status': 'success'}), 200
@blueprint.route('/playlist/image', methods=['GET']) @blueprint.route('/playlist/image', methods=['GET'])
@login_or_basic_auth @login_or_jwt
@spotify_link_required @spotify_link_required
@validate_args(('name', str)) @validate_args(('name', str))
def image(user=None): def image(auth=None, user=None):
_playlist = user.get_playlist(request.args['name'], raise_error=False) _playlist = user.get_playlist(request.args['name'], raise_error=False)
if _playlist is None: if _playlist is None:

View File

@ -4,6 +4,7 @@ import logging
from flask import session, request, jsonify from flask import session, request, jsonify
from music.model.user import User from music.model.user import User
from music.auth.jwt_keys import validate_key
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -30,6 +31,19 @@ def is_basic_authed():
return False, None return False, None
def is_jwt_authed():
if request.headers.get('Authorization', None):
unparsed = request.headers.get('Authorization')
if unparsed.startswith('Bearer '):
token = validate_key(unparsed.removeprefix('Bearer ').strip())
if token is not None:
return token
def login_required(func): def login_required(func):
@functools.wraps(func) @functools.wraps(func)
def login_required_wrapper(*args, **kwargs): def login_required_wrapper(*args, **kwargs):
@ -42,6 +56,48 @@ def login_required(func):
return login_required_wrapper return login_required_wrapper
def login_or_jwt(func):
@functools.wraps(func)
def login_or_jwt_wrapper(*args, **kwargs):
if is_logged_in():
user = User.collection.filter('username', '==', session['username'].strip().lower()).get()
return func(*args, user=user, **kwargs)
else:
token = is_jwt_authed()
if token is not None:
user = User.collection.filter('username', '==', token['sub'].strip().lower()).get()
if user is not None:
return func(*args, auth=token, user=user, **kwargs)
else:
logger.warning(f'user {token["sub"]} not found')
return jsonify({'error': f'user {token["sub"]} not found'}), 404
else:
logger.warning('user not authorised')
return jsonify({'error': 'not authorised'}), 401
return login_or_jwt_wrapper
def jwt_required(func):
@functools.wraps(func)
def jwt_required_wrapper(*args, **kwargs):
token = is_jwt_authed()
if token is not None:
user = User.collection.filter('username', '==', token['sub'].strip().lower()).get()
if user is not None:
return func(*args, auth=token, user=user, **kwargs)
else:
logger.warning(f'user {token["sub"]} not found')
return jsonify({'error': f'user {token["sub"]} not found'}), 404
else:
logger.warning('user not authorised')
return jsonify({'error': 'not authorised'}), 401
return jwt_required_wrapper
def login_or_basic_auth(func): def login_or_basic_auth(func):
@functools.wraps(func) @functools.wraps(func)
def login_or_basic_auth_wrapper(*args, **kwargs): def login_or_basic_auth_wrapper(*args, **kwargs):

View File

@ -2,7 +2,7 @@ from flask import Blueprint, jsonify
from datetime import date from datetime import date
import logging import logging
from music.api.decorators import login_or_basic_auth, lastfm_username_required from music.api.decorators import login_or_jwt, lastfm_username_required
import music.db.database as database import music.db.database as database
@ -11,9 +11,9 @@ logger = logging.getLogger(__name__)
@blueprint.route('/today', methods=['GET']) @blueprint.route('/today', methods=['GET'])
@login_or_basic_auth @login_or_jwt
@lastfm_username_required @lastfm_username_required
def daily_scrobbles(user=None): def daily_scrobbles(auth=None, user=None):
net = database.get_authed_lastfm_network(user) net = database.get_authed_lastfm_network(user)

View File

@ -2,7 +2,7 @@ from flask import Blueprint, request, jsonify
import logging import logging
from music.api.decorators import login_or_basic_auth, spotify_link_required, validate_json from music.api.decorators import login_or_jwt, spotify_link_required, validate_json
import music.db.database as database import music.db.database as database
from spotframework.net.network import SpotifyNetworkException from spotframework.net.network import SpotifyNetworkException
@ -16,9 +16,9 @@ logger = logging.getLogger(__name__)
@blueprint.route('/play', methods=['POST']) @blueprint.route('/play', methods=['POST'])
@login_or_basic_auth @login_or_jwt
@spotify_link_required @spotify_link_required
def play(user=None): def play(auth=None, user=None):
request_json = request.get_json() request_json = request.get_json()
if 'uri' in request_json: if 'uri' in request_json:
@ -78,9 +78,9 @@ def play(user=None):
@blueprint.route('/next', methods=['POST']) @blueprint.route('/next', methods=['POST'])
@login_or_basic_auth @login_or_jwt
@spotify_link_required @spotify_link_required
def next_track(user=None): def next_track(auth=None, user=None):
net = database.get_authed_spotify_network(user) net = database.get_authed_spotify_network(user)
player = Player(net) player = Player(net)
@ -89,10 +89,10 @@ def next_track(user=None):
@blueprint.route('/shuffle', methods=['POST']) @blueprint.route('/shuffle', methods=['POST'])
@login_or_basic_auth @login_or_jwt
@spotify_link_required @spotify_link_required
@validate_json(('state', bool)) @validate_json(('state', bool))
def shuffle(user=None): def shuffle(auth=None, user=None):
request_json = request.get_json() request_json = request.get_json()
net = database.get_authed_spotify_network(user) net = database.get_authed_spotify_network(user)
@ -103,10 +103,10 @@ def shuffle(user=None):
@blueprint.route('/volume', methods=['POST']) @blueprint.route('/volume', methods=['POST'])
@login_or_basic_auth @login_or_jwt
@spotify_link_required @spotify_link_required
@validate_json(('volume', int)) @validate_json(('volume', int))
def volume(user=None): def volume(auth=None, user=None):
request_json = request.get_json() request_json = request.get_json()
if 0 <= request_json['volume'] <= 100: if 0 <= request_json['volume'] <= 100:

View File

@ -3,7 +3,7 @@ import logging
import json import json
import os import os
from music.api.decorators import admin_required, login_or_basic_auth, lastfm_username_required, \ from music.api.decorators import admin_required, login_or_jwt, lastfm_username_required, \
spotify_link_required, cloud_task, validate_args spotify_link_required, cloud_task, validate_args
import music.db.database as database import music.db.database as database
from music.cloud.tasks import refresh_all_user_playlist_stats, refresh_user_playlist_stats, refresh_playlist_task from music.cloud.tasks import refresh_all_user_playlist_stats, refresh_user_playlist_stats, refresh_playlist_task
@ -20,10 +20,10 @@ logger = logging.getLogger(__name__)
@blueprint.route('/count', methods=['GET']) @blueprint.route('/count', methods=['GET'])
@login_or_basic_auth @login_or_jwt
@spotify_link_required @spotify_link_required
@lastfm_username_required @lastfm_username_required
def count(user=None): def count(auth=None, user=None):
uri = request.args.get('uri', None) uri = request.args.get('uri', None)
playlist_name = request.args.get('playlist_name', None) playlist_name = request.args.get('playlist_name', None)
@ -69,11 +69,11 @@ def count(user=None):
@blueprint.route('/playlist/refresh', methods=['GET']) @blueprint.route('/playlist/refresh', methods=['GET'])
@login_or_basic_auth @login_or_jwt
@spotify_link_required @spotify_link_required
@lastfm_username_required @lastfm_username_required
@validate_args(('name', str)) @validate_args(('name', str))
def playlist_refresh(user=None): def playlist_refresh(auth=None, user=None):
playlist_name = request.args['name'] playlist_name = request.args['name']
@ -133,16 +133,16 @@ def run_playlist_artist_task():
@blueprint.route('/playlist/refresh/users', methods=['GET']) @blueprint.route('/playlist/refresh/users', methods=['GET'])
@login_or_basic_auth @login_or_jwt
@admin_required @admin_required
def run_users(user=None): def run_users(auth=None, user=None):
refresh_all_user_playlist_stats() refresh_all_user_playlist_stats()
return jsonify({'message': 'executed all users', 'status': 'success'}), 200 return jsonify({'message': 'executed all users', 'status': 'success'}), 200
@blueprint.route('/playlist/refresh/user', methods=['GET']) @blueprint.route('/playlist/refresh/user', methods=['GET'])
@login_or_basic_auth @login_or_jwt
def run_user(user=None): def run_user(auth=None, user=None):
if user.type == 'admin': if user.type == 'admin':
user_name = request.args.get('username', user.username) user_name = request.args.get('username', user.username)

View File

@ -1,7 +1,7 @@
from flask import Blueprint, request, jsonify from flask import Blueprint, request, jsonify
import logging import logging
from music.api.decorators import login_or_basic_auth, spotify_link_required from music.api.decorators import login_or_jwt, spotify_link_required
import music.db.database as database import music.db.database as database
from spotframework.engine.playlistengine import PlaylistEngine from spotframework.engine.playlistengine import PlaylistEngine
@ -12,9 +12,9 @@ logger = logging.getLogger(__name__)
@blueprint.route('/sort', methods=['POST']) @blueprint.route('/sort', methods=['POST'])
@login_or_basic_auth @login_or_jwt
@spotify_link_required @spotify_link_required
def sort(user=None): def sort(auth=None, user=None):
request_json = request.get_json() request_json = request.get_json()
net = database.get_authed_spotify_network(user) net = database.get_authed_spotify_network(user)

View File

@ -4,7 +4,7 @@ import logging
import os import os
import json import json
from music.api.decorators import login_or_basic_auth, cloud_task from music.api.decorators import login_or_jwt, cloud_task
from music.cloud.function import update_tag as serverless_update_tag from music.cloud.function import update_tag as serverless_update_tag
from music.tasks.update_tag import update_tag from music.tasks.update_tag import update_tag
@ -15,8 +15,8 @@ logger = logging.getLogger(__name__)
@blueprint.route('/tag', methods=['GET']) @blueprint.route('/tag', methods=['GET'])
@login_or_basic_auth @login_or_jwt
def tags(user=None): def tags(auth=None, user=None):
logger.info(f'retrieving tags for {user.username}') logger.info(f'retrieving tags for {user.username}')
return jsonify({ return jsonify({
'tags': [i.to_dict() for i in Tag.collection.parent(user.key).fetch()] 'tags': [i.to_dict() for i in Tag.collection.parent(user.key).fetch()]
@ -24,8 +24,8 @@ def tags(user=None):
@blueprint.route('/tag/<tag_id>', methods=['GET', 'PUT', 'POST', "DELETE"]) @blueprint.route('/tag/<tag_id>', methods=['GET', 'PUT', 'POST', "DELETE"])
@login_or_basic_auth @login_or_jwt
def tag_route(tag_id, user=None): def tag_route(tag_id, auth=None, user=None):
if request.method == 'GET': if request.method == 'GET':
return get_tag(tag_id, user) return get_tag(tag_id, user)
elif request.method == 'PUT': elif request.method == 'PUT':
@ -126,8 +126,8 @@ def delete_tag(tag_id, user):
@blueprint.route('/tag/<tag_id>/update', methods=['GET']) @blueprint.route('/tag/<tag_id>/update', methods=['GET'])
@login_or_basic_auth @login_or_jwt
def tag_refresh(tag_id, user=None): def tag_refresh(tag_id, auth=None, user=None):
logger.info(f'updating {tag_id} tag for {user.username}') logger.info(f'updating {tag_id} tag for {user.username}')
if os.environ.get('DEPLOY_DESTINATION', None) == 'PROD': if os.environ.get('DEPLOY_DESTINATION', None) == 'PROD':

View File

@ -1,10 +1,13 @@
from flask import Blueprint, session, flash, request, redirect, url_for, render_template from flask import Blueprint, session, flash, request, redirect, url_for, render_template, jsonify
from werkzeug.security import generate_password_hash from werkzeug.security import generate_password_hash
from music.model.user import User from music.model.user import User
from music.model.config import Config from music.model.config import Config
from music.auth.jwt_keys import generate_key
from urllib.parse import urlencode, urlunparse from urllib.parse import urlencode, urlunparse
import datetime import datetime
from datetime import timedelta
from numbers import Number
import logging import logging
from base64 import b64encode from base64 import b64encode
import requests import requests
@ -68,6 +71,51 @@ def logout():
flash('logged out') flash('logged out')
return redirect(url_for('index')) return redirect(url_for('index'))
@blueprint.route('/token', methods=['POST'])
def jwt_token():
"""Generate JWT
Returns:
HTTP Response: token request on POST
"""
request_json = request.get_json()
username = request_json.get('username', None)
password = request_json.get('password', None)
if username is None or password is None:
return jsonify({"message": 'username and password fields required', "status": "error"}), 400
user = User.collection.filter('username', '==', username.strip().lower()).get()
if user is None:
return jsonify({"message": 'user not found', "status": "error"}), 404
if user.check_password(password):
if user.locked:
logger.warning(f'locked account token attempt {username}')
return jsonify({"message": 'user locked', "status": "error"}), 403
user.last_keygen = datetime.datetime.utcnow()
user.update()
logger.info(f'generating token for {username}')
config = Config.collection.get("config/music-tools")
if isinstance(expiry := request_json.get('expiry', None), Number):
expiry = min(expiry, config.jwt_max_length)
else:
expiry = config.jwt_default_length
token = generate_key(user, timeout=timedelta(seconds=expiry))
return jsonify({"token": token, "status": "success"}), 200
else:
logger.warning(f'failed token attempt {username}')
return jsonify({"message": 'authentication failed', "status": "error"}), 401
@blueprint.route('/register', methods=['GET', 'POST']) @blueprint.route('/register', methods=['GET', 'POST'])
def register(): def register():

41
music/auth/jwt_keys.py Normal file
View File

@ -0,0 +1,41 @@
from ctypes import Union
from datetime import timedelta, datetime, timezone
import jwt
from music.model.user import User
from music.model.config import Config
def get_jwt_secret_key() -> str:
config = Config.collection.get("config/music-tools")
if config.jwt_secret_key is None or len(config.jwt_secret_key) == 0:
raise KeyError("no jwt secret key found")
return config.jwt_secret_key
def generate_key(user: User, timeout: datetime | timedelta = timedelta(minutes=60)) -> str:
if isinstance(timeout, timedelta):
exp = timeout + datetime.now(tz=timezone.utc)
else:
exp = timeout
payload = {
"exp": exp,
"iss": "mixonomer-api",
"sub": user.username
}
return jwt.encode(payload, get_jwt_secret_key(), algorithm="HS512")
def validate_key(key: str) -> dict:
try:
decoded = jwt.decode(key, get_jwt_secret_key(), algorithms=["HS512"], options={
"require": ["exp", "sub"]
})
return decoded
except Exception as e:
pass

View File

@ -19,3 +19,6 @@ class Config(Model):
"""Determines whether playlist and tag update operations are done by Cloud Tasks or Functions """Determines whether playlist and tag update operations are done by Cloud Tasks or Functions
""" """
secret_key = TextField() secret_key = TextField()
jwt_secret_key = TextField()
jwt_max_length = NumberField()
jwt_default_length = NumberField()

View File

@ -20,6 +20,7 @@ class User(Model):
type = TextField(default="user") type = TextField(default="user")
last_login = DateTime() last_login = DateTime()
last_keygen = DateTime()
last_refreshed = DateTime() last_refreshed = DateTime()
locked = BooleanField(default=False, required=True) locked = BooleanField(default=False, required=True)
validated = BooleanField(default=True, required=True) validated = BooleanField(default=True, required=True)

20
poetry.lock generated
View File

@ -462,6 +462,20 @@ python-versions = "*"
[package.dependencies] [package.dependencies]
pyasn1 = ">=0.4.6,<0.5.0" pyasn1 = ">=0.4.6,<0.5.0"
[[package]]
name = "pyjwt"
version = "2.4.0"
description = "JSON Web Token implementation in Python"
category = "main"
optional = false
python-versions = ">=3.6"
[package.extras]
tests = ["coverage[toml] (==5.0.4)", "pytest (>=6.0.0,<7.0.0)"]
docs = ["zope.interface", "sphinx-rtd-theme", "sphinx"]
dev = ["pre-commit", "mypy", "coverage[toml] (==5.0.4)", "pytest (>=6.0.0,<7.0.0)", "cryptography (>=3.3.1)", "zope.interface", "sphinx-rtd-theme", "sphinx"]
crypto = ["cryptography (>=3.3.1)"]
[[package]] [[package]]
name = "pylint" name = "pylint"
version = "2.14.5" version = "2.14.5"
@ -655,7 +669,7 @@ python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7"
[metadata] [metadata]
lock-version = "1.1" lock-version = "1.1"
python-versions = "~3.10" python-versions = "~3.10"
content-hash = "e3a9a1b705fe06227a9e09263b2e84aa6ee1bc6e41096732f06715a109daca4e" content-hash = "740e615c75f16d1d097c47b49a18fc3487be9541f257d5181d198693feb87380"
[metadata.files] [metadata.files]
astroid = [ astroid = [
@ -958,6 +972,10 @@ pyasn1-modules = [
{file = "pyasn1_modules-0.2.8-py3.6.egg", hash = "sha256:cbac4bc38d117f2a49aeedec4407d23e8866ea4ac27ff2cf7fb3e5b570df19e0"}, {file = "pyasn1_modules-0.2.8-py3.6.egg", hash = "sha256:cbac4bc38d117f2a49aeedec4407d23e8866ea4ac27ff2cf7fb3e5b570df19e0"},
{file = "pyasn1_modules-0.2.8-py3.7.egg", hash = "sha256:c29a5e5cc7a3f05926aff34e097e84f8589cd790ce0ed41b67aed6857b26aafd"}, {file = "pyasn1_modules-0.2.8-py3.7.egg", hash = "sha256:c29a5e5cc7a3f05926aff34e097e84f8589cd790ce0ed41b67aed6857b26aafd"},
] ]
pyjwt = [
{file = "PyJWT-2.4.0-py3-none-any.whl", hash = "sha256:72d1d253f32dbd4f5c88eaf1fdc62f3a19f676ccbadb9dbc5d07e951b2b26daf"},
{file = "PyJWT-2.4.0.tar.gz", hash = "sha256:d42908208c699b3b973cbeb01a969ba6a96c821eefb1c5bfe4c390c01d67abba"},
]
pylint = [ pylint = [
{file = "pylint-2.14.5-py3-none-any.whl", hash = "sha256:fabe30000de7d07636d2e82c9a518ad5ad7908590fe135ace169b44839c15f90"}, {file = "pylint-2.14.5-py3-none-any.whl", hash = "sha256:fabe30000de7d07636d2e82c9a518ad5ad7908590fe135ace169b44839c15f90"},
{file = "pylint-2.14.5.tar.gz", hash = "sha256:487ce2192eee48211269a0e976421f334cf94de1806ca9d0a99449adcdf0285e"}, {file = "pylint-2.14.5.tar.gz", hash = "sha256:487ce2192eee48211269a0e976421f334cf94de1806ca9d0a99449adcdf0285e"},

View File

@ -21,6 +21,7 @@ google-cloud-logging = "^3.2.1"
google-cloud-pubsub = "^2.13.4" google-cloud-pubsub = "^2.13.4"
google-cloud-tasks = "^2.10.0" google-cloud-tasks = "^2.10.0"
requests = "^2.28.1" requests = "^2.28.1"
PyJWT = "^2.4.0"
spotframework = { git = "https://github.com/Sarsoo/spotframework.git" } spotframework = { git = "https://github.com/Sarsoo/spotframework.git" }
fmframework = { git = "https://github.com/Sarsoo/pyfmframework.git" } fmframework = { git = "https://github.com/Sarsoo/pyfmframework.git" }

33
tests/test_auth.py Normal file
View File

@ -0,0 +1,33 @@
from ast import Assert
from time import sleep
import unittest
from datetime import timedelta
from music.model.user import User
from music.auth.jwt_keys import generate_key, validate_key
class TestAuth(unittest.TestCase):
def test_encode_decode(self):
test_user = User.collection.filter('username', '==', "test").get()
key = generate_key(test_user, timedelta(minutes=10))
decoded = validate_key(key)
self.assertEqual(decoded["sub"], test_user.username)
def test_timeout(self):
test_user = User.collection.filter('username', '==', "test").get()
key = generate_key(test_user, timedelta(seconds=2))
decoded = validate_key(key)
self.assertIsNotNone(decoded)
sleep(2.5)
decoded = validate_key(key)
self.assertIsNone(decoded)