Skip to content

Commit

Permalink
Modules splitting, structure and logic improvements
Browse files Browse the repository at this point in the history
- 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
vexy authored Mar 17, 2020
1 parent a84fd15 commit 4ecbeb2
Show file tree
Hide file tree
Showing 16 changed files with 288 additions and 111 deletions.
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Python outputs
__pycache__/
*.pyc

# MacOS stuff
.DS_Store
69 changes: 49 additions & 20 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,29 +1,58 @@
# Authentication mechanism based on Flask + JWT
This repository represents the source code template for `Flask` implementation of `JWT` based authentication.
It can be used as starting point for more complex projects and requirements or can be completely customised to serve your 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 would be executing `startup.sh` script. Simple `(user)$ . startup.sh` would do the trick on UNIX based systems.

Alternatively, step by step guide would be:
- `clone` this repository (*aim for master*)
- `cd flask-auth-template`
- `pip3 install -r requirements.txt`
- `python3 auth-module.py`

**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+`. The conflict might be seen as "Module Import Error" no matter which Python you are using.
To solve this, you might have to "fix" your `PYTHONPATH`. Check out this [article](https://bic-berkeley.github.io/psych-214-fall-2016/using_pythonpath.html) for more information.
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.

### 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.

----

#### 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.
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ć
Copyright © 2020 Veljko Tekelerović
MIT License
79 changes: 0 additions & 79 deletions auth-module.py

This file was deleted.

3 changes: 3 additions & 0 deletions install-dependencies.sh
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
35 changes: 35 additions & 0 deletions main-module.py
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 added models/__init__.py
Empty file.
10 changes: 10 additions & 0 deletions models/user.py
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 added modules/__init__.py
Empty file.
72 changes: 72 additions & 0 deletions modules/auth.py
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)
48 changes: 48 additions & 0 deletions modules/protected.py
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 added paw_testkit.paw
Binary file not shown.
Empty file added services/__init__.py
Empty file.
31 changes: 31 additions & 0 deletions services/storage.py
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()
Loading

0 comments on commit 4ecbeb2

Please sign in to comment.