mail_plugin/controllers/authenticate.py

112 lines
5.5 KiB
Python
Raw Normal View History

# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import base64
import datetime
import hmac
import json
import logging
import odoo
import werkzeug
from odoo import _, http
from odoo.http import request
from werkzeug.exceptions import NotFound
_logger = logging.getLogger(__name__)
class Authenticate(http.Controller):
@http.route(['/mail_client_extension/auth', '/mail_plugin/auth'], type='http', auth="user", methods=['GET'], website=True)
def auth(self, **values):
"""
Once authenticated this route renders the view that shows an app wants to access Odoo.
The user is invited to allow or deny the app. The form posts to `/mail_client_extension/auth/confirm`.
old route name "/mail_client_extension/auth is deprecated as of saas-14.3,it is not needed for newer
versions of the mail plugin but necessary for supporting older versions
"""
if not request.env.user._is_internal():
return request.render('mail_plugin.app_error', {'error': _('Access Error: Only Internal Users can link their inboxes to this database.')})
return request.render('mail_plugin.app_auth', values)
@http.route(['/mail_client_extension/auth/confirm', '/mail_plugin/auth/confirm'], type='http', auth="user", methods=['POST'])
def auth_confirm(self, scope, friendlyname, redirect, info=None, do=None, **kw):
"""
Called by the `app_auth` template. If the user decided to allow the app to access Odoo, a temporary auth code
is generated and they are redirected to `redirect` with this code in the URL. It should redirect to the app, and
the app should then exchange this auth code for an access token by calling
`/mail_client/auth/access_token`.
old route name "/mail_client_extension/auth/confirm is deprecated as of saas-14.3,it is not needed for newer
versions of the mail plugin but necessary for supporting older versions
"""
parsed_redirect = werkzeug.urls.url_parse(redirect)
params = parsed_redirect.decode_query()
if do:
name = friendlyname if not info else f'{friendlyname}: {info}'
auth_code = self._generate_auth_code(scope, name)
# params is a MultiDict which does not support .update() with kwargs
# the state attribute is needed for the gmail connector
params.update({'success': 1, 'auth_code': auth_code, 'state': kw.get('state', '')})
else:
params.update({'success': 0, 'state': kw.get('state', '')})
updated_redirect = parsed_redirect.replace(query=werkzeug.urls.url_encode(params))
return request.redirect(updated_redirect.to_url(), local=False)
# In this case, an exception will be thrown in case of preflight request if only POST is allowed.
@http.route(['/mail_client_extension/auth/access_token', '/mail_plugin/auth/access_token'], type='json', auth="none", cors="*",
methods=['POST', 'OPTIONS'])
def auth_access_token(self, auth_code='', **kw):
"""
Called by the external app to exchange an auth code, which is temporary and was passed in a URL, for an
access token, which is permanent, and can be used in the `Authorization` header to authorize subsequent requests
old route name "/mail_client_extension/auth/access_token is deprecated as of saas-14.3,it is not needed for newer
versions of the mail plugin but necessary for supporting older versions
"""
if not auth_code:
return {"error": "Invalid code"}
auth_message = self._get_auth_code_data(auth_code)
if not auth_message:
return {"error": "Invalid code"}
request.update_env(user=auth_message['uid'])
scope = 'odoo.plugin.' + auth_message.get('scope', '')
api_key = request.env['res.users.apikeys']._generate(scope, auth_message['name'])
return {'access_token': api_key}
def _get_auth_code_data(self, auth_code):
data, auth_code_signature = auth_code.split('.')
data = base64.b64decode(data)
auth_code_signature = base64.b64decode(auth_code_signature)
signature = odoo.tools.misc.hmac(request.env(su=True), 'mail_plugin', data).encode()
if not hmac.compare_digest(auth_code_signature, signature):
return None
auth_message = json.loads(data)
# Check the expiration
if datetime.datetime.utcnow() - datetime.datetime.fromtimestamp(auth_message['timestamp']) > datetime.timedelta(
minutes=3):
return None
return auth_message
# Using UTC explicitly in case of a distributed system where the generation and the signature verification do not
# necessarily happen on the same server
def _generate_auth_code(self, scope, name):
if not request.env.user._is_internal():
raise NotFound()
auth_dict = {
'scope': scope,
'name': name,
'timestamp': int(datetime.datetime.utcnow().timestamp()),
# <- elapsed time should be < 3 mins when verifying
'uid': request.uid,
}
auth_message = json.dumps(auth_dict, sort_keys=True).encode()
signature = odoo.tools.misc.hmac(request.env(su=True), 'mail_plugin', auth_message).encode()
auth_code = "%s.%s" % (base64.b64encode(auth_message).decode(), base64.b64encode(signature).decode())
_logger.info('Auth code created - user %s, scope %s', request.env.user, scope)
return auth_code