PageRenderTime 22ms CodeModel.GetById 13ms RepoModel.GetById 0ms app.codeStats 0ms

/grumponado/basehandler.py

https://bitbucket.org/angryshortone/grump-o-nado
Python | 219 lines | 208 code | 4 blank | 7 comment | 0 complexity | 7ed53ef277ff3a7c5904dfbabcb42a6c MD5 | raw file
Possible License(s): MPL-2.0-no-copyleft-exception
  1. # This Source Code Form is subject to the terms of the Mozilla Public
  2. # License, v. 2.0. If a copy of the MPL was not distributed with this
  3. # file, You can obtain one at http://mozilla.org/MPL/2.0/.
  4. """
  5. The Basehandler
  6. ===============
  7. """
  8. import logging
  9. from datetime import timedelta
  10. from secrets import token_urlsafe
  11. import json
  12. import hashids
  13. import jwt
  14. from jwt import InvalidTokenError, ExpiredSignatureError
  15. from tornado.escape import json_decode
  16. import tornado.web
  17. from tornado.gen import coroutine
  18. from tornado_sqlalchemy import SessionMixin, as_future
  19. from .models import Tokens
  20. from .utils import now_datetime, get_timestamp_seconds
  21. from . import errors
  22. class Basehandler(tornado.web.RequestHandler, SessionMixin):
  23. """Basehandler"""
  24. def get_current_user(self):
  25. """Retrieve the currently active user"""
  26. cookie_secret = self.application.settings.get('cookie_secret')
  27. auth_header = self.request.headers.get("Authorization")
  28. auth_token = None
  29. try:
  30. if auth_header.startswith('Bearer '):
  31. auth_token = auth_header.split(' ')[1]
  32. verified_user = jwt.decode(auth_token, cookie_secret, algorithms='HS256')
  33. return {'id': verified_user['sub'],
  34. 'name': verified_user['name'],
  35. 'refresh': verified_user['refresh']}
  36. return None
  37. except ExpiredSignatureError:
  38. logging.debug('Expired token was received.')
  39. unverified_user = jwt.decode(auth_token, cookie_secret, algorithms='HS256', options={'verify_exp': False})
  40. try:
  41. return self._refresh_user_token(unverified_user['refresh'])
  42. except KeyError:
  43. logging.warning('User token does not contain a refresh token.')
  44. return None
  45. except InvalidTokenError:
  46. logging.warning('User has an invalid token.')
  47. return None
  48. except AttributeError:
  49. logging.debug('No token found, usually when logging in.')
  50. return None
  51. def _refresh_user_token(self, token):
  52. """
  53. Args:
  54. token:
  55. """
  56. with self.make_session() as session:
  57. db_token = session.query(Tokens).get(token)
  58. now_date = now_datetime()
  59. if db_token:
  60. if db_token.last_refresh + timedelta(days=14) < now_date:
  61. return None
  62. db_token.last_refresh = now_date
  63. session.add(db_token)
  64. session.commit()
  65. token_user = db_token.users
  66. if token_user:
  67. return {'id': token_user.id,
  68. 'name': token_user.user_name,
  69. 'refresh': token}
  70. return None
  71. @coroutine
  72. def _store_refresh_token(self, user_id, ip_address, user_agent):
  73. """Creates and stores a refresh token. The refresh token is used in
  74. subsequent request to refresh the authorization token. This allows users
  75. to stay logged in for more than a short time. Users only need to
  76. re-login every 2 weeks. Along with the token, the ip and the user agent
  77. are going to be saved.
  78. Args:
  79. user_id (int): the id of the user who is associated with the token
  80. ip_address (string): the IP address of the requesting user
  81. user_agent (string): the User agent
  82. Returns:
  83. string: an url save token
  84. """
  85. token = Tokens(user_id, token_urlsafe(48), ip_address, user_agent)
  86. with self.make_session() as session:
  87. as_future(session.add(token))
  88. as_future(session.commit())
  89. return token.token
  90. @property
  91. def current_user_token(self):
  92. """Access to the token of the currently active user"""
  93. if not hasattr(self, '_current_user_token'):
  94. self._current_user_token = self.get_current_user_token()
  95. return self._current_user_token
  96. @current_user_token.setter
  97. def current_user_token(self, value):
  98. """Set a token for the current user manually
  99. Args:
  100. value:
  101. """
  102. self._current_user_token = value
  103. def get_current_user_token(self):
  104. """If a user is correctly signed in a token is always available for said
  105. user. The token will be valid for the next five minutes.
  106. The new token is always returned along with each response. It is also
  107. signed, therefore be sure you have set the secret properly.
  108. Returns:
  109. token: a new, signed token, valid for the next five minutes.
  110. """
  111. if self.current_user:
  112. iat = get_timestamp_seconds()
  113. return jwt.encode({
  114. 'sub': self.current_user['id'],
  115. 'name': self.current_user['name'],
  116. 'iat': iat,
  117. 'exp': iat + 300,
  118. 'refresh': self.current_user['refresh']
  119. }, key=self.application.settings.get('cookie_secret'),
  120. algorithm='HS256').decode('utf-8')
  121. return None
  122. def _get_credentials(self):
  123. """A simplification to use both, the json body or the form encoded data.
  124. First it is tried to extract the data from a json request body. If that
  125. should fail, the data is fetched from the x-form encoded body.
  126. """
  127. try:
  128. logging.debug('Attempting to read the email and password from the request body')
  129. logging.debug('First try with json decode')
  130. request_data = json_decode(self.request.body)
  131. email = request_data.get('email')
  132. password = request_data.get('password')
  133. except json.JSONDecodeError:
  134. email = self.get_body_argument('email')
  135. password = self.get_body_argument('password')
  136. logging.debug('There was no json request in the body. Probably the data was transmitted as form '
  137. 'encode')
  138. if email and password:
  139. logging.debug('Password and email were correctly supplied')
  140. return {'email': email,
  141. 'password': password}
  142. elif not email:
  143. self.write_error(400, errors.USER_MISSING)
  144. else:
  145. self.write_error(400, errors.PASSWORD_MISSING)
  146. return None
  147. def _prepare_json(self, message):
  148. """Modifies a json response message by adding auth tokens
  149. Args:
  150. message (dict): the message, to which a token will be prepended
  151. Returns:
  152. dict: A new dictionary with an additional auth token
  153. """
  154. token = {'auth': self.get_current_user_token()}
  155. hashid_salt = self.application.settings.get('hashing_salt')
  156. hashid = hashids.Hashids(salt=hashid_salt)
  157. try:
  158. message['result']['id'] = hashid.encode(message['result']['id'])
  159. except KeyError:
  160. logging.debug('No need to encode any ids')
  161. except TypeError:
  162. logging.debug('None type - no need to encode any ids')
  163. if isinstance(message, dict):
  164. return {**message, **token}
  165. return {**{'msg': str(message)}, **token}
  166. def write(self, chunk):
  167. """
  168. Args:
  169. chunk:
  170. """
  171. super(Basehandler, self).write(chunk)
  172. self.finish()
  173. def write_error(self, status_code, error_message):
  174. """Writes an error message to the return buffer. It also provides an api
  175. to set the appropriate Http status code. Additionally it does modify the
  176. error message, for instance to add an authorisation token to the
  177. response.
  178. Args:
  179. status_code (int): a valid http status code.
  180. error_message (dict): a dictionary containing the message and any
  181. additional information
  182. """
  183. self.set_status(status_code)
  184. if not error_message:
  185. self.set_status(500)
  186. self.write('Error')
  187. self.write(self._prepare_json(error_message))
  188. def write_json_message(self, message):
  189. """Writes a message in the json format. Because we always need
  190. additional information with the actual message, the json message is
  191. prepared.
  192. Args:
  193. message (dict): the message to write to the buffer. This will be
  194. modified before it actually will be written.
  195. """
  196. self.write(self._prepare_json(message))