authenticate.py 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110
  1. # -*- coding: utf-8 -*-
  2. # Part of Odoo. See LICENSE file for full copyright and licensing details.
  3. import base64
  4. import datetime
  5. import hmac
  6. import json
  7. import logging
  8. import odoo
  9. import werkzeug
  10. from odoo import http
  11. from odoo.http import request
  12. from werkzeug.exceptions import NotFound
  13. _logger = logging.getLogger(__name__)
  14. class Authenticate(http.Controller):
  15. @http.route(['/mail_client_extension/auth', '/mail_plugin/auth'], type='http', auth="user", methods=['GET'], website=True)
  16. def auth(self, **values):
  17. """
  18. Once authenticated this route renders the view that shows an app wants to access Odoo.
  19. The user is invited to allow or deny the app. The form posts to `/mail_client_extension/auth/confirm`.
  20. old route name "/mail_client_extension/auth is deprecated as of saas-14.3,it is not needed for newer
  21. versions of the mail plugin but necessary for supporting older versions
  22. """
  23. return request.render('mail_plugin.app_auth', values)
  24. @http.route(['/mail_client_extension/auth/confirm', '/mail_plugin/auth/confirm'], type='http', auth="user", methods=['POST'])
  25. def auth_confirm(self, scope, friendlyname, redirect, info=None, do=None, **kw):
  26. """
  27. Called by the `app_auth` template. If the user decided to allow the app to access Odoo, a temporary auth code
  28. is generated and they are redirected to `redirect` with this code in the URL. It should redirect to the app, and
  29. the app should then exchange this auth code for an access token by calling
  30. `/mail_client/auth/access_token`.
  31. old route name "/mail_client_extension/auth/confirm is deprecated as of saas-14.3,it is not needed for newer
  32. versions of the mail plugin but necessary for supporting older versions
  33. """
  34. parsed_redirect = werkzeug.urls.url_parse(redirect)
  35. params = parsed_redirect.decode_query()
  36. if do:
  37. name = friendlyname if not info else f'{friendlyname}: {info}'
  38. auth_code = self._generate_auth_code(scope, name)
  39. # params is a MultiDict which does not support .update() with kwargs
  40. # the state attribute is needed for the gmail connector
  41. params.update({'success': 1, 'auth_code': auth_code, 'state': kw.get('state', '')})
  42. else:
  43. params.update({'success': 0, 'state': kw.get('state', '')})
  44. updated_redirect = parsed_redirect.replace(query=werkzeug.urls.url_encode(params))
  45. return request.redirect(updated_redirect.to_url(), local=False)
  46. # In this case, an exception will be thrown in case of preflight request if only POST is allowed.
  47. @http.route(['/mail_client_extension/auth/access_token', '/mail_plugin/auth/access_token'], type='json', auth="none", cors="*",
  48. methods=['POST', 'OPTIONS'])
  49. def auth_access_token(self, auth_code='', **kw):
  50. """
  51. Called by the external app to exchange an auth code, which is temporary and was passed in a URL, for an
  52. access token, which is permanent, and can be used in the `Authorization` header to authorize subsequent requests
  53. old route name "/mail_client_extension/auth/access_token is deprecated as of saas-14.3,it is not needed for newer
  54. versions of the mail plugin but necessary for supporting older versions
  55. """
  56. if not auth_code:
  57. return {"error": "Invalid code"}
  58. auth_message = self._get_auth_code_data(auth_code)
  59. if not auth_message:
  60. return {"error": "Invalid code"}
  61. request.update_env(user=auth_message['uid'])
  62. scope = 'odoo.plugin.' + auth_message.get('scope', '')
  63. api_key = request.env['res.users.apikeys']._generate(scope, auth_message['name'])
  64. return {'access_token': api_key}
  65. def _get_auth_code_data(self, auth_code):
  66. data, auth_code_signature = auth_code.split('.')
  67. data = base64.b64decode(data)
  68. auth_code_signature = base64.b64decode(auth_code_signature)
  69. signature = odoo.tools.misc.hmac(request.env(su=True), 'mail_plugin', data).encode()
  70. if not hmac.compare_digest(auth_code_signature, signature):
  71. return None
  72. auth_message = json.loads(data)
  73. # Check the expiration
  74. if datetime.datetime.utcnow() - datetime.datetime.fromtimestamp(auth_message['timestamp']) > datetime.timedelta(
  75. minutes=3):
  76. return None
  77. return auth_message
  78. # Using UTC explicitly in case of a distributed system where the generation and the signature verification do not
  79. # necessarily happen on the same server
  80. def _generate_auth_code(self, scope, name):
  81. if not request.env.user._is_internal():
  82. raise NotFound()
  83. auth_dict = {
  84. 'scope': scope,
  85. 'name': name,
  86. 'timestamp': int(datetime.datetime.utcnow().timestamp()),
  87. # <- elapsed time should be < 3 mins when verifying
  88. 'uid': request.uid,
  89. }
  90. auth_message = json.dumps(auth_dict, sort_keys=True).encode()
  91. signature = odoo.tools.misc.hmac(request.env(su=True), 'mail_plugin', auth_message).encode()
  92. auth_code = "%s.%s" % (base64.b64encode(auth_message).decode(), base64.b64encode(signature).decode())
  93. _logger.info('Auth code created - user %s, scope %s', request.env.user, scope)
  94. return auth_code