-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Modules splitting, structure and logic improvements
- 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: - a4e7b97 - 585a2ac - 5a2e5af - a36789e - 25b2a79 - c23988f Signed-off-by: Veljko TekeleroviΔ <[email protected]>
- Loading branch information
Showing
17 changed files
with
291 additions
and
17 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
# Python outputs | ||
__pycache__/ | ||
*.pyc | ||
|
||
# MacOS stuff | ||
.DS_Store |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 |
File renamed without changes.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
theme: jekyll-theme-hacker |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
# pip packages | ||
echo "Installing needed modules via pip..." | ||
pip3 install -r requirements.txt |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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) |
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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}]" |
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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"<AUTH> 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"<AUTH> 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"<AUTH> Adding new user: {newUser.username}, email: {newUser.email}") | ||
sharedStorage.store(newUser) | ||
|
||
return make_response("<h2>Welcome to the system</h2><br>Have a pleasant stay <strong>{}</strong> and enjoy the system :)".format(newUser.username), 201) | ||
except: | ||
current_app.logger.error("<REGISTRATION> Unable to parse POST request.") | ||
|
||
return make_response("Wrong parameters. Try again", 400) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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"<PROTECTED_ACCESS> 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 |
Binary file not shown.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
Flask==1.1.1 | ||
PyJWT==1.7.1 |
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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"<STORAGE> 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() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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)) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
# server start | ||
echo "Starting server in DEBUG mode" | ||
python3 main-module.py |