From deaaf329b71e597da7120f7d8f5125257d082e0f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Veljko=20Tekelerovi=C4=87?= Date: Sat, 29 Feb 2020 20:30:26 +0100 Subject: [PATCH] Modules splitting, structure and logic improvements MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - improved code logic and reliability - extraction of distinct app routes into separate modules/files - extraction of functionality into separate modules - file structure updates - README and script updates Some of the update commits: - a4e7b972c1c6903b2a34ae486a57af6e73f224fb - 585a2ac35510a1a2c81f1b5b138c7b8da855681b - 5a2e5af4b3112c637d0c259c593d889e8329ef7f - a36789e213465b0e31541d6b23ce89773b07ee40 - 25b2a79bc277ba821e3e8370e4772948417cba01 - c23988f889a63a6080eed836bf0cbaf51f0f7938 Signed-off-by: Veljko Tekelerović --- .gitignore | 6 +++ README.md | 66 +++++++++++++++++++++++-------- auth-module.py => __init__.py | 0 _config.yml | 1 + install-dependencies.sh | 3 ++ main-module.py | 35 +++++++++++++++++ models/__init__.py | 0 models/user.py | 10 +++++ modules/__init__.py | 0 modules/auth.py | 72 ++++++++++++++++++++++++++++++++++ modules/protected.py | 48 +++++++++++++++++++++++ paw_testkit.paw | Bin 0 -> 21324 bytes requirements.txt | 2 + services/__init__.py | 0 services/storage.py | 31 +++++++++++++++ services/tokenizer.py | 31 +++++++++++++++ start.sh | 3 ++ 17 files changed, 291 insertions(+), 17 deletions(-) create mode 100644 .gitignore rename auth-module.py => __init__.py (100%) create mode 100644 _config.yml create mode 100644 install-dependencies.sh create mode 100644 main-module.py create mode 100644 models/__init__.py create mode 100644 models/user.py create mode 100644 modules/__init__.py create mode 100644 modules/auth.py create mode 100644 modules/protected.py create mode 100644 paw_testkit.paw create mode 100644 requirements.txt create mode 100644 services/__init__.py create mode 100644 services/storage.py create mode 100644 services/tokenizer.py create mode 100644 start.sh diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5bb5b49 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +# Python outputs +__pycache__/ +*.pyc + +# MacOS stuff +.DS_Store diff --git a/README.md b/README.md index 8632719..b3185d9 100644 --- a/README.md +++ b/README.md @@ -1,26 +1,58 @@ -# Simple JWT authentication template -This repository represents the source code template for `JWT` based authentication. -It can be used as starting point for more complex projects and needs. +# Authentication gate with Flask & JWT +This repository represents the source code template for micro webserver that provides authentication gate for your protected resources. -## Installation and usage -Perhaps the easiest way to start using this template would be as follows: - - `clone` this repository - - `cd flask-auth-template` - - `pip install requirements.txt` - -**NOTE:** if you're on `MacOS` platforms, there might be a struggle with running this project due to known collision of `Python2.7` and `Python3+`. By default `pip` will (assume) install dependencies to Python2 while `pip3` will place them under Python3. -If you're having trouble starting this project, try installing all the requirements using: `pip3 install requirements.txt` +It is written in `Python` using `Flask` framework and relies on `JWT` authentication mechanism. +Some of the provided strategies are to basic/simple for **serious**, production level webserver. Use this template as starting point for more complex projects and requirements. ### JWT based -`JSON Web Tokens` - or [JWT](https://jwt.io/) in short - are the foundation of the authentication mechanism presented here. -Be sure **not to forget** to decode/encode token generation at your own strategy. Follow code comments for exact place where you need to modify or customise this behaviour. +`JSON Web Tokens` - or [JWT](https://jwt.io/) in short - is the foundation authentication principle used in this template. +Be sure **not to forget** to encode/decode token generation at your own strategy. Follow code comments for exact place where you could modify or customise this behaviour. ### No database ! -Any DB layer has been **intentionally** omitted to allow space for your own implementation. In this form, the code template stores all generated tokens **in memory** and are only valid until next server restart. For more convenient mechanism, store your `tokens` in some form of persistent storage. +DB layer has been **intentionally omitted** to allow space for your own implementation. In present form, the code handles all tokens **in memory**, making the data available only while the server is running. All registration data (as well as tokens) will disappear after the server shut down. +For more convenient mechanism, store your tokens in some form of persistent storage, or reuse them in different way. -#### Word of wisdom -If you ever get stuck during coding, remember that `sudo` is your friend. If that doesn't help, just one cold 🍺 can magically make your life look way more beautiful. +### Modularised +Template is designed to support modular structure. Main application modules are stored in `modules` folder. If you need more modules, you can place them inside - as long as they are connected in the main module. + +### Different authentication strategies +Presented here is basic HTTP AUTHENTICATION through Authentication field. Note there are **way secure** authentication mechanisms, such as `OAuth`. + +### Installation +Before you begin: +``` +git clone +cd flask-auth-template +``` +Then proceed with installing dependencies: +```bash +# Run prepacked script +$ . install-dependencies.sh +# or +# install manually through pip3 +$ pip3 install -r requirements.txt +``` + +### Starting server +Template will setup and start a server listening on `localhost`. Check the debug output for more information. + +Start the server using: +```bash +python3 main-module.py +# or +# run using startup script +$ . start.sh +``` + +**:NOTE:** for `MacOS` users: +There might be a struggle with starting this project, due to known collision of `Python2.xx` and `Python3.xx` coexisting on same platform. The conflict might be manifested as good ol' *"Module Import Error"* no matter which Python you are using. +To solve this, you might have to "fix" (play around with) your `PYTHONPATH`. Check out this [article](https://bic-berkeley.github.io/psych-214-fall-2016/using_pythonpath.html) for more information. ---- -Copyright © 2020 Veljko Tekelerović +#### Word of wisdom +If you ever get stuck remember that `sudo` is your friend. If it doesn't help, start thinking how one cold 🍺 can magically improve your understanding of the 🌎. + + +Copyright © 2020 Veljko Tekelerović +MIT License diff --git a/auth-module.py b/__init__.py similarity index 100% rename from auth-module.py rename to __init__.py diff --git a/_config.yml b/_config.yml new file mode 100644 index 0000000..fc24e7a --- /dev/null +++ b/_config.yml @@ -0,0 +1 @@ +theme: jekyll-theme-hacker \ No newline at end of file diff --git a/install-dependencies.sh b/install-dependencies.sh new file mode 100644 index 0000000..478822a --- /dev/null +++ b/install-dependencies.sh @@ -0,0 +1,3 @@ +# pip packages +echo "Installing needed modules via pip..." +pip3 install -r requirements.txt diff --git a/main-module.py b/main-module.py new file mode 100644 index 0000000..aa1dd66 --- /dev/null +++ b/main-module.py @@ -0,0 +1,35 @@ + +# -*- coding: utf-8 -*- +from flask import Flask, Blueprint +from services.storage import sharedStorage + +from modules.auth import * +from modules.protected import * +# from protected import * + +# initialize main Flask object +if __name__ == '__main__': + app = Flask(__name__) + app.config['SECRET_KEY'] = 'some_secret_key' + + # register app blueprints + app.register_blueprint(authRoute) + app.register_blueprint(protectedRoute) + +# Publicly accessible routes +# ------------------------------ +@app.route('/') +def home(): + output = [] + for user in sharedStorage.asList(): + output.append(str(user)) + return jsonify({ + 'count': sharedStorage.totalCount(), + 'storage': output + }) + + +# --------------------------------- +# Server start procedure +if __name__ == '__main__': + app.run(debug=True) diff --git a/models/__init__.py b/models/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/models/user.py b/models/user.py new file mode 100644 index 0000000..902f8b2 --- /dev/null +++ b/models/user.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- + +class User(): + def __init__(self, username = "", password = "", email = ""): + self.username = username + self.password = password + self.email = email + + def __str__(self): + return f"[{self.username}] - [pwd:{self.password} eml:{self.email}]" diff --git a/modules/__init__.py b/modules/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/modules/auth.py b/modules/auth.py new file mode 100644 index 0000000..859af79 --- /dev/null +++ b/modules/auth.py @@ -0,0 +1,72 @@ +# -*- coding: utf-8 -*- +from flask import Flask, Blueprint +from flask import jsonify, request, make_response +from flask import current_app +from services.tokenizer import Tokenizer +from services.storage import sharedStorage +from models.user import User + +# public blueprint exposure +authRoute = Blueprint('auth', __name__) + +@authRoute.route('/login') +def login(): + # get authorization field from HTTP request, early exit if it's not present + auth = request.authorization + if not auth: + return make_response("HTTP Basic Authentication required 🤔", 401) #, {'WWW-Authenticate': 'Basic realm="Login required"'} + + try: # search our storage to check credentials + username = auth.username + password = auth.password + storedUser = sharedStorage.find(username) + + # 👇 perform validity check and password hashing 👇 + if storedUser is not None and storedUser.password == password: + current_app.logger.info(f" Security check completed, passwords match.") + # create new token using Tokenizer + tokenService = Tokenizer(current_app.config['SECRET_KEY']) + newToken = tokenService.createToken(username) + + utfDecodedToken = newToken.decode('UTF-8') + current_app.logger.info(f" New token created.") + return jsonify({'token': utfDecodedToken}) + except: + make_response("Bad request parameters. Try again", 400) + + return make_response("Wrong credentials.", 401) + +@authRoute.route('/logout') +def logout(): + current_app.logger.info("Someone logged out") + # remove token from the storage + return "You have been logged out.. But who are you ??" + +@authRoute.route('/register', methods=['POST']) +def registration(): + ''' + Expecting this JSON structure in body: + { + 'username' : "abc", + 'password': "abc", + 'email': "abc@abc" + } + ''' + try: #try to get the body data as JSON, fail otherwise + body = request.json + if body: + username = body['username'] + pwd = body['password'] # 👇 password hashing 👇 + email = body['email'] + + # add to our storage + newUser = User(username, pwd, email) + + current_app.logger.info(f" Adding new user: {newUser.username}, email: {newUser.email}") + sharedStorage.store(newUser) + + return make_response("

Welcome to the system


Have a pleasant stay {} and enjoy the system :)".format(newUser.username), 201) + except: + current_app.logger.error(" Unable to parse POST request.") + + return make_response("Wrong parameters. Try again", 400) diff --git a/modules/protected.py b/modules/protected.py new file mode 100644 index 0000000..fd92393 --- /dev/null +++ b/modules/protected.py @@ -0,0 +1,48 @@ +# -*- coding: utf-8 -*- +from flask import Flask, Blueprint +from flask import jsonify, request, make_response +from flask import current_app +from services.tokenizer import Tokenizer +from services.storage import sharedStorage +from models.user import User +from functools import wraps + +# public blueprint exposure +protectedRoute = Blueprint('protected', __name__) + +# protector function wraping other functions +def token_access_required(f): + @wraps(f) + def decorated(*args, **kwargs): + token = request.args.get('token') #get token from URL + + if not token: + return jsonify({'message': 'Protected area. Valid access token required'}), 403 + try: # make sure we can decode token + tokenSupport = Tokenizer(current_app.config['SECRET_KEY']) + decodedUser = tokenSupport.decodeToken(token)['user'] + # and that we have a storage entry for this user + storedUser = sharedStorage.find(decodedUser) + if storedUser and storedUser.username == decodedUser: + current_app.logger.info(f" Username pair matches, protected entry allowed.") + else: + return jsonify({'message': 'Invalid access token supplied.'}), 401 + except: + return jsonify({'message': 'Invalid access token supplied.'}), 401 + return f(*args, **kwargs) + + return decorated + +# Protected ROUTES DEFINITION: (split further to standalone Blueprints) +# ----------------------------- +@protectedRoute.route('/protected') +@token_access_required +def protected(): + resp_body = jsonify({'message': 'Welcome to protected area, you made it'}) + return resp_body + +@protectedRoute.route('/protected2') +@token_access_required +def protected2(): + resp_body = jsonify({'message': 'Welcome to protected area, you made it'}) + return resp_body diff --git a/paw_testkit.paw b/paw_testkit.paw new file mode 100644 index 0000000000000000000000000000000000000000..6b8ec98c2bd9c0af6d5396af1dc40a7d45999d94 GIT binary patch literal 21324 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 DucNbX literal 0 HcmV?d00001 diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..c250825 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +Flask==1.1.1 +PyJWT==1.7.1 diff --git a/services/__init__.py b/services/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/services/storage.py b/services/storage.py new file mode 100644 index 0000000..c51043d --- /dev/null +++ b/services/storage.py @@ -0,0 +1,31 @@ +# -*- coding: utf-8 -*- +from models.user import User + +# represents a basic in memory storage heap +class UserDataStorage(): + def __init__(self): + self.allUsers = [] + + def store(self, userData): + self.allUsers.append(userData) + print(f" New user stored: {userData.username}") + + # search all usernames and return matching user + def find(self, targetUsername): + result = None + for user in self.allUsers: + if user.username == targetUsername: + result = user + break + return result + + # returns a list of all stored objects + def asList(self): + return list(self.allUsers) + + def totalCount(self): + return len(list(self.allUsers)) + + +# shared reference to a single storage +sharedStorage = UserDataStorage() diff --git a/services/tokenizer.py b/services/tokenizer.py new file mode 100644 index 0000000..030b471 --- /dev/null +++ b/services/tokenizer.py @@ -0,0 +1,31 @@ +# -*- coding: utf-8 -*- +import jwt +import datetime + +class Tokenizer(): + def __init__(self, key): + self.secretKey = key + + # 👇 DIFFERENT STRATEGIES POSSIBLE 👇 + def createToken(self, username): + # define content as a mix of username and expiration date + tokenExpiry = self.setupExpiry() + tokenContent = { + 'user': username, + 'expiration': tokenExpiry + } + + # 'crypt' it this way: + fullToken = jwt.encode(tokenContent, self.secretKey, algorithm='HS256') + return fullToken + + # returns a decoded token + def decodeToken(self, rawToken): + output = jwt.decode(rawToken, self.secretKey, algorithms=['HS256']) + return output + + + # 👇 DIFFERENT STRATEGIES POSSIBLE 👇 + def setupExpiry(self): + # sets token expiration to 30 minutes from now + return str(datetime.datetime.utcnow() + datetime.timedelta(minutes=30)) diff --git a/start.sh b/start.sh new file mode 100644 index 0000000..9baf654 --- /dev/null +++ b/start.sh @@ -0,0 +1,3 @@ +# server start +echo "Starting server in DEBUG mode" +python3 main-module.py