/grumponado/basehandler.py
Python | 219 lines | 208 code | 4 blank | 7 comment | 0 complexity | 7ed53ef277ff3a7c5904dfbabcb42a6c MD5 | raw file
Possible License(s): MPL-2.0-no-copyleft-exception
- # This Source Code Form is subject to the terms of the Mozilla Public
- # License, v. 2.0. If a copy of the MPL was not distributed with this
- # file, You can obtain one at http://mozilla.org/MPL/2.0/.
- """
- The Basehandler
- ===============
- """
- import logging
- from datetime import timedelta
- from secrets import token_urlsafe
- import json
- import hashids
- import jwt
- from jwt import InvalidTokenError, ExpiredSignatureError
- from tornado.escape import json_decode
- import tornado.web
- from tornado.gen import coroutine
- from tornado_sqlalchemy import SessionMixin, as_future
- from .models import Tokens
- from .utils import now_datetime, get_timestamp_seconds
- from . import errors
- class Basehandler(tornado.web.RequestHandler, SessionMixin):
- """Basehandler"""
- def get_current_user(self):
- """Retrieve the currently active user"""
- cookie_secret = self.application.settings.get('cookie_secret')
- auth_header = self.request.headers.get("Authorization")
- auth_token = None
- try:
- if auth_header.startswith('Bearer '):
- auth_token = auth_header.split(' ')[1]
- verified_user = jwt.decode(auth_token, cookie_secret, algorithms='HS256')
- return {'id': verified_user['sub'],
- 'name': verified_user['name'],
- 'refresh': verified_user['refresh']}
- return None
- except ExpiredSignatureError:
- logging.debug('Expired token was received.')
- unverified_user = jwt.decode(auth_token, cookie_secret, algorithms='HS256', options={'verify_exp': False})
- try:
- return self._refresh_user_token(unverified_user['refresh'])
- except KeyError:
- logging.warning('User token does not contain a refresh token.')
- return None
- except InvalidTokenError:
- logging.warning('User has an invalid token.')
- return None
- except AttributeError:
- logging.debug('No token found, usually when logging in.')
- return None
- def _refresh_user_token(self, token):
- """
- Args:
- token:
- """
- with self.make_session() as session:
- db_token = session.query(Tokens).get(token)
- now_date = now_datetime()
- if db_token:
- if db_token.last_refresh + timedelta(days=14) < now_date:
- return None
- db_token.last_refresh = now_date
- session.add(db_token)
- session.commit()
- token_user = db_token.users
- if token_user:
- return {'id': token_user.id,
- 'name': token_user.user_name,
- 'refresh': token}
- return None
- @coroutine
- def _store_refresh_token(self, user_id, ip_address, user_agent):
- """Creates and stores a refresh token. The refresh token is used in
- subsequent request to refresh the authorization token. This allows users
- to stay logged in for more than a short time. Users only need to
- re-login every 2 weeks. Along with the token, the ip and the user agent
- are going to be saved.
- Args:
- user_id (int): the id of the user who is associated with the token
- ip_address (string): the IP address of the requesting user
- user_agent (string): the User agent
- Returns:
- string: an url save token
- """
- token = Tokens(user_id, token_urlsafe(48), ip_address, user_agent)
- with self.make_session() as session:
- as_future(session.add(token))
- as_future(session.commit())
- return token.token
- @property
- def current_user_token(self):
- """Access to the token of the currently active user"""
- if not hasattr(self, '_current_user_token'):
- self._current_user_token = self.get_current_user_token()
- return self._current_user_token
- @current_user_token.setter
- def current_user_token(self, value):
- """Set a token for the current user manually
- Args:
- value:
- """
- self._current_user_token = value
- def get_current_user_token(self):
- """If a user is correctly signed in a token is always available for said
- user. The token will be valid for the next five minutes.
- The new token is always returned along with each response. It is also
- signed, therefore be sure you have set the secret properly.
- Returns:
- token: a new, signed token, valid for the next five minutes.
- """
- if self.current_user:
- iat = get_timestamp_seconds()
- return jwt.encode({
- 'sub': self.current_user['id'],
- 'name': self.current_user['name'],
- 'iat': iat,
- 'exp': iat + 300,
- 'refresh': self.current_user['refresh']
- }, key=self.application.settings.get('cookie_secret'),
- algorithm='HS256').decode('utf-8')
- return None
- def _get_credentials(self):
- """A simplification to use both, the json body or the form encoded data.
- First it is tried to extract the data from a json request body. If that
- should fail, the data is fetched from the x-form encoded body.
- """
- try:
- logging.debug('Attempting to read the email and password from the request body')
- logging.debug('First try with json decode')
- request_data = json_decode(self.request.body)
- email = request_data.get('email')
- password = request_data.get('password')
- except json.JSONDecodeError:
- email = self.get_body_argument('email')
- password = self.get_body_argument('password')
- logging.debug('There was no json request in the body. Probably the data was transmitted as form '
- 'encode')
- if email and password:
- logging.debug('Password and email were correctly supplied')
- return {'email': email,
- 'password': password}
- elif not email:
- self.write_error(400, errors.USER_MISSING)
- else:
- self.write_error(400, errors.PASSWORD_MISSING)
- return None
- def _prepare_json(self, message):
- """Modifies a json response message by adding auth tokens
- Args:
- message (dict): the message, to which a token will be prepended
- Returns:
- dict: A new dictionary with an additional auth token
- """
- token = {'auth': self.get_current_user_token()}
- hashid_salt = self.application.settings.get('hashing_salt')
- hashid = hashids.Hashids(salt=hashid_salt)
- try:
- message['result']['id'] = hashid.encode(message['result']['id'])
- except KeyError:
- logging.debug('No need to encode any ids')
- except TypeError:
- logging.debug('None type - no need to encode any ids')
- if isinstance(message, dict):
- return {**message, **token}
- return {**{'msg': str(message)}, **token}
- def write(self, chunk):
- """
- Args:
- chunk:
- """
- super(Basehandler, self).write(chunk)
- self.finish()
- def write_error(self, status_code, error_message):
- """Writes an error message to the return buffer. It also provides an api
- to set the appropriate Http status code. Additionally it does modify the
- error message, for instance to add an authorisation token to the
- response.
- Args:
- status_code (int): a valid http status code.
- error_message (dict): a dictionary containing the message and any
- additional information
- """
- self.set_status(status_code)
- if not error_message:
- self.set_status(500)
- self.write('Error')
- self.write(self._prepare_json(error_message))
- def write_json_message(self, message):
- """Writes a message in the json format. Because we always need
- additional information with the actual message, the json message is
- prepared.
- Args:
- message (dict): the message to write to the buffer. This will be
- modified before it actually will be written.
- """
- self.write(self._prepare_json(message))