From f506e3dfaad0462936740308c1ecd36e25d6616c Mon Sep 17 00:00:00 2001 From: esensar Date: Mon, 7 May 2018 17:17:19 +0200 Subject: [PATCH] Add token validation logic and protected routes --- app/accounts/__init__.py | 12 ++++++ app/accounts/models.py | 39 +++++++++++------ app/api/__init__.py | 43 ++++++++++++++++--- app/api/resources/account.py | 2 + .../swagger/create_account_spec.yaml | 1 + .../resources/swagger/create_token_spec.yaml | 3 +- app/swagger/template.yaml | 34 +++++++++++++++ config.py | 5 +++ 8 files changed, 117 insertions(+), 22 deletions(-) diff --git a/app/accounts/__init__.py b/app/accounts/__init__.py index 6d4b6b7..773d0cf 100644 --- a/app/accounts/__init__.py +++ b/app/accounts/__init__.py @@ -49,3 +49,15 @@ def create_token(username, password): raise ValueError("Invalid credentials") return account.create_auth_token() + + +def validate_token(token): + """ + Validates token and returns associated account + + :param token: auth token to validate + :type token: string + :returns created token + :rtype Account + """ + return Account.validate_token(token) diff --git a/app/accounts/models.py b/app/accounts/models.py index 0a97cb9..6ef058c 100644 --- a/app/accounts/models.py +++ b/app/accounts/models.py @@ -77,20 +77,31 @@ class Account(db.Model): Generates the Auth Token :return: string """ - try: - current_time = datetime.datetime.utcnow() - payload = { - 'exp': current_time + datetime.timedelta(days=0, hours=1), - 'iat': current_time, - 'sub': self.id - } - return jwt.encode( - payload, - app.config.get('SECRET_KEY'), - algorithm='HS256' - ).decode('utf-8') - except Exception as e: - return e + current_time = datetime.datetime.utcnow() + payload = { + 'exp': current_time + datetime.timedelta(days=0, hours=1), + 'iat': current_time, + 'sub': self.id + } + return jwt.encode( + payload, + app.config.get('SECRET_KEY'), + algorithm='HS256' + ).decode('utf-8') + + @staticmethod + def validate_token(token): + """ + Validates given Auth token + :rtype: Account + :return: Account associated with token + """ + payload = jwt.decode( + token, + app.config.get('SECRET_KEY'), + algorithms=['HS256'] + ) + return Account.get(id=payload['sub']) def __repr__(self): return '' % self.username, self.role diff --git a/app/api/__init__.py b/app/api/__init__.py index 6a74a8b..cf1a4ed 100644 --- a/app/api/__init__.py +++ b/app/api/__init__.py @@ -1,16 +1,45 @@ -from flask import Blueprint -from flask_restful import Api -from .resources.account import AccountResource -from .resources.token import TokenResource +from flask import Blueprint, request, g +from flask_restful import Api, Resource, abort +from functools import wraps from marshmallow import ValidationError +from app.accounts import validate_token api_bp = Blueprint('api', __name__) api = Api(api_bp) -# Add resources -api.add_resource(AccountResource, '/v1/accounts') -api.add_resource(TokenResource, '/v1/token') + +def protected(func): + @wraps(func) + def protected_function(*args, **kwargs): + try: + token = request.headers['Authorization'] or None + + if not token: + abort(401, message='Unauthorized', status='error') + + g.current_account = validate_token(token.replace("Bearer ", "")) + if not g.current_account: + abort(401, message='Unauthorized', status='error') + except Exception: + abort(401, message='Unauthorized', status='error') + + return protected_function + + +class ProtectedResource(Resource): + method_decorators = [protected] + + +def add_resources(): + from .resources.account import AccountResource + from .resources.token import TokenResource + + api.add_resource(AccountResource, '/v1/accounts') + api.add_resource(TokenResource, '/v1/token') + + +add_resources() @api_bp.errorhandler(ValidationError) diff --git a/app/api/resources/account.py b/app/api/resources/account.py index 3b7ae7a..6441a66 100644 --- a/app/api/resources/account.py +++ b/app/api/resources/account.py @@ -3,6 +3,7 @@ from webargs import fields from webargs.flaskparser import use_args from flasgger import swag_from import app.accounts as accounts +from app.api import protected class AccountResource(Resource): @@ -28,6 +29,7 @@ class AccountResource(Resource): except ValueError: abort(422, message='Account already exists', status='error') + @protected @swag_from('swagger/get_account_spec.yaml') def get(self): return '', 200 diff --git a/app/api/resources/swagger/create_account_spec.yaml b/app/api/resources/swagger/create_account_spec.yaml index 8ef8f8f..e610aa4 100644 --- a/app/api/resources/swagger/create_account_spec.yaml +++ b/app/api/resources/swagger/create_account_spec.yaml @@ -19,6 +19,7 @@ parameters: - password - email $ref: '#/definitions/User' +security: [] responses: 201: description: Successful creation diff --git a/app/api/resources/swagger/create_token_spec.yaml b/app/api/resources/swagger/create_token_spec.yaml index 6ef7c70..d2fe999 100644 --- a/app/api/resources/swagger/create_token_spec.yaml +++ b/app/api/resources/swagger/create_token_spec.yaml @@ -16,6 +16,7 @@ parameters: properties: user: $ref: '#/definitions/Credentials' +security: [] responses: 200: description: Successful creation @@ -33,4 +34,4 @@ responses: 401: description: Bad credentials schema: - $ref: '#/definitions/Error' + $ref: '#/definitions/UnauthorizedError' diff --git a/app/swagger/template.yaml b/app/swagger/template.yaml index 339d8b6..a1ccd66 100644 --- a/app/swagger/template.yaml +++ b/app/swagger/template.yaml @@ -57,6 +57,19 @@ definitions: email: $ref: '#/definitions/email' + UnauthorizedError: + type: object + required: + - status + - message + properties: + status: + $ref: '#/definitions/status' + default: error + message: + $ref: '#/definitions/message' + default: Unauthorized + Error: type: object required: @@ -70,6 +83,27 @@ definitions: $ref: '#/definitions/message' default: Error message +securityDefinitions: + Bearer: + type: apiKey + name: Authorization + in: header + description: | + For accessing the API a valid JWT token must be passed in all the queries in + the 'Authorization' header as Bearer token. + + + A valid JWT token is generated by the API and returned as answer of a call + to the route /login giving a valid user & password. + + + The following syntax must be used in the 'Authorization' header : + + Bearer xxxxxx.yyyyyyy.zzzzzz + +security: + - Bearer: [] + info: description: Python (Flask) backend for IoT sysyem made for master's degree final project title: IoT Backend diff --git a/config.py b/config.py index 8c57d30..3dc450e 100644 --- a/config.py +++ b/config.py @@ -33,3 +33,8 @@ MQTT_BROKER_PORT = 1883 MQTT_USERNAME = 'user' MQTT_PASSWORD = 'secret' MQTT_REFRESH_TIME = 1.0 # refresh time in seconds + +# Flassger config +SWAGGER = { + 'uiversion': 3 +}