Skip to content

Commit

Permalink
Merge 2fbb506 into 2474b32
Browse files Browse the repository at this point in the history
  • Loading branch information
mugoh committed Jan 12, 2019
2 parents 2474b32 + 2fbb506 commit a8936ec
Show file tree
Hide file tree
Showing 15 changed files with 348 additions and 147 deletions.
9 changes: 0 additions & 9 deletions app/__init__.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
from flask import Flask
from flask_jwt_extended import JWTManager

from app.api.v1 import auth_blueprint, app_blueprint
from app.api.v1.views.user import blacklisted_tokens
from instance.config import APP_CONFIG


Expand All @@ -11,14 +9,7 @@ def create_app(config_setting):
app.register_blueprint(auth_blueprint)
app.register_blueprint(app_blueprint)

jwt = JWTManager(app)

app.config.from_object(
APP_CONFIG[config_setting.strip().lower()])

@jwt.token_in_blacklist_loader
def check_blacklisted_token(token):
jti = token['jti']
return jti in blacklisted_tokens

return app
22 changes: 22 additions & 0 deletions app/api/v1/models/tokens.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
"""
This module contains the class that creates blacklisted token objects
and checks for token validity
"""


class Token:

def __init__(self, token):
self.signature = token
self.save()

def save(self):
blacklisted_tokens.add(self)

@classmethod
def check_if_blacklisted(cls, given_token):
return [token for token in blacklisted_tokens if
getattr(token, 'signature') == str(given_token)]


blacklisted_tokens = set()
53 changes: 50 additions & 3 deletions app/api/v1/models/users.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@
This file contains the model for users data.
"""
from werkzeug.security import generate_password_hash, check_password_hash
import jwt
import datetime
from flask import current_app as app

from .abstract_model import AbstractModel

users = [] # Persists user objects
from .tokens import Token


class UserModel(AbstractModel):
Expand Down Expand Up @@ -65,6 +67,48 @@ def get_by_id(cls, usr_id):
def get_all_users(cls):
return [user.dictify() for user in users]

def encode_auth_token(self, user_name):
"""
Creates and returns an encoded authorization token.
It uses the UserModel username attribute as the token identifier.
"""

payload = {
"exp": datetime.datetime.utcnow() + datetime.timedelta(
days=app.config.get('AUTH_TOKEN_EXP_DAYS'),
seconds=app.config.get('AUTH_TOKEN_EXP_SECS')),
"iat": datetime.datetime.utcnow(),
"sub": user_name
}

return jwt.encode(
payload,
app.config['SECRET_KEY'],
algorithm='HS256')

@classmethod
def decode_auth_token(cls, encoded_token):
"""
Decodes the authorzation token to get the payload
the retrieves the username from 'sub' attribute
"""
try:
payload = jwt.decode(encoded_token,
app.config.get('SECRET_KEY'),
algorithms='HS256')
if Token.check_if_blacklisted(payload):
return {
"Status": 400,
"Message": "Token unsuable. Try signing in again"
}, 400
return payload['sub']

except (jwt.ExpiredSignatureError, jwt.InvalidTokenError) as er:
return {
"Status": 400,
"Message": "Token Invalid. Please log in again" + er
}, 400

def dictify(self):

return {
Expand All @@ -80,7 +124,10 @@ def dictify(self):
"id": self.id
}

# return self.__dict__
# return self.__dict__

def __repr__(self):
return '{Email} {Username}'.format(**self.dictify())


users = [] # Persist user objects
154 changes: 154 additions & 0 deletions app/api/v1/utils/auth.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
"""
This module contains validators that help with
Authorization cases.
"""

from functools import wraps
from flask import request

from ...v1.models.users import UserModel

current_user = None
raw_auth = None


def admin_required(f):
"""
Protects endpoints accessible to admin user only.
Ensures only an admin user can access this endpoint.
"""
@wraps(f)
def wrapper(*args, **kwargs):

# We never get here in real sense, consumed by missing Auth header
# Uncomment when testing manually
#
# Verify Logged in
"""if not current_user:
return {
"Status": 403,
"Error": "Please log in, okay?"
}, 403
user = UserModel.get_by_name(current_user)
if not user:
return {
"Status": 400,
"Error": "Identity unknown"
}
"""
current_user = get_auth_identity()

if not UserModel.get_by_name(current_user).isAdmin:
return {
"Status": 403,
"Message": "Oops! Only an admin can do that"
}, 403
return f(*args, **kwargs)
return wrapper


def current_user_only(f):
"""
Ensures the user changing the resource in a protected endpoint
is the one who created that resource.e.g Deleting a question
"""
@wraps(f)
def wrapper(*args, **kwars):
url_user_field = request.base_url.split('/')
user = url_user_field[-2]
this_user = get_auth_identity()

# Comment out if manually testing
# Handled by missing auth header error
"""
if not this_user:
return {
"Status": 403,
"Message": "You need to be logged in to do that"
}, 403
"""

try:
uid = int(user)
user = UserModel.get_by_id(uid)
if user:
user = user.username
except ValueError:
user = user
if this_user != user:
return {
"Status": 403,
"Error": "Denied. Not accessible to current user"
}, 403
return f(*args, **kwars)
return wrapper


def auth_required(f):
"""
Protects endpoints that require user authrorization for access
"""
@wraps(f)
def wrapper(*args, **kwargs):
if 'Authorization' not in request.headers:
return {
"Status": 400,
"Message": "Please provide a valid Authorization Header"
}, 400

auth_header = request.headers['Authorization']

try:
payload = auth_header.split(' ')[1]
except IndexError:

return {
"Status": 400,
"Message": "Please provide a valid Authorization Header"
}, 400

if not payload:
return {
"Status": 400,
"Message": "Token is empty. Please provide a valid token"
}, 400

try:
user_identity = UserModel.decode_auth_token(payload)

current_user = UserModel.get_by_name(user_identity).username
save_raw_payload(payload, current_user)

except Exception:
return {
"Status": 400,
"Message": "Invalid Token. Please provide a valid token"
}, 400

return f(current_user, *args, **kwargs)
return wrapper


def get_auth_identity():
"""
Returns the identity of the user accessing a protected
endpoint.
"""
return current_user


def save_raw_payload(undecoded, usr):
"""
Receives and saves current user identity and encoded token payload.
"""
global raw_auth, current_user
raw_auth = undecoded
current_user = usr


def get_raw_auth():
"""
Returns the identity of the payload that logged this session.
"""
return raw_auth
78 changes: 14 additions & 64 deletions app/api/v1/utils/helpers.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
from functools import wraps
"""
This module contains validators for user data passed in request
arguments.
"""
from flask import request
import re
from functools import wraps

from ...v1.models.users import UserModel
from ...v1.views.user import get_jwt_identity
name_pattern = re.compile(r'^[A-Za-z]+$')


def verify_pass(value):
Expand All @@ -11,65 +15,11 @@ def verify_pass(value):
return value


def admin_required(f):
@wraps(f)
def wrapper(*args, **kwargs):

# We never get here in real sense, consumed by missing Auth header
# Uncomment when testing manually
#
# Verify Logged in
"""if not get_jwt_identity():
return {
"Status": 403,
"Error": "Please log in, okay?"
}, 403
user = UserModel.get_by_name(get_jwt_identity())
if not user:
return {
"Status": 400,
"Error": "Identity unknown"
}
"""
if not UserModel.get_by_name(get_jwt_identity()).isAdmin:
return {
"Status": 403,
"Message": "Oops! Only an admin can do that"
}, 403
return f(*args, **kwargs)
return wrapper
def verify_name(value, item):
if ' ' in value:
raise ValueError(f'{value} has spaces. {item} should not have spaces')


def current_user_only(f):
@wraps(f)
def wrapper(*args, **kwars):
url_user_field = request.base_url.split('/')
user = url_user_field[-2]
this_user = get_jwt_identity()

# Comment out if manually testing
# Handled by missing auth header error
"""
if not this_user:
return {
"Status": 403,
"Message": "You need to be logged in to do that"
}, 403
"""

try:
uid = int(user)
user = UserModel.get_by_id(uid)
if user:
user = user.username
print(user)
except ValueError:
user = user
if this_user != user:
return {
"Status": 403,
"Error": "Denied. Not accessible to current user"
}, 403
return f(*args, **kwars)
return wrapper
elif not name_pattern.match(value):
raise ValueError(f'Oops! {value} has NUMBERS.' +
f' {item} should contain letters only')
return value
Loading

0 comments on commit a8936ec

Please sign in to comment.