[FIX] google_gmail, fetchmail_gmail: use Odoo endpoint for authentication

Purpose
=======
The flow where we copy / paste the authorization code will be
depreciated. Because of that, we know use the newest authentication
system which use redirect URI.

Technical
=========
Now, the user is redirected to an Odoo endpoint "google_gmail/confirm"
and the access token / refresh token are automatically fetched.

Documentation
https://developers.google.com/identity/protocols/oauth2/native-app

Task-2852560

closes odoo/odoo#94476

X-original-commit: 69bd9bc6b36fdd55e8e859667bcddddad6db7d06
Signed-off-by: Thibault Delavallee (tde) <tde@openerp.com>
This commit is contained in:
std-odoo
2022-06-24 10:29:54 +02:00
parent f828c89648
commit dd87eb77f3
8 changed files with 326 additions and 66 deletions
@@ -15,6 +15,28 @@ msgstr ""
"Content-Transfer-Encoding: \n"
"Plural-Forms: \n"
#. module: fetchmail_gmail
#: model_terms:ir.ui.view,arch_db:fetchmail_gmail.fetchmail_server_view_form
msgid ""
"<i class=\"fa fa-arrow-right\"/>\n"
" Connect your Gmail account"
msgstr ""
#. module: fetchmail_gmail
#: model_terms:ir.ui.view,arch_db:fetchmail_gmail.fetchmail_server_view_form
msgid ""
"<i class=\"fa fa-cog\"/>\n"
" Edit Settings"
msgstr ""
#. module: fetchmail_gmail
#: model_terms:ir.ui.view,arch_db:fetchmail_gmail.fetchmail_server_view_form
msgid ""
"<span attrs=\"{'invisible': ['|', ('server_type', '!=', 'gmail'), ('google_gmail_refresh_token', '=', False)]}\" class=\"badge badge-success\">\n"
" Gmail Token Valid\n"
" </span>"
msgstr ""
#. module: fetchmail_gmail
#: model:ir.model.fields,field_description:fetchmail_gmail.field_fetchmail_server__google_gmail_access_token
msgid "Access Token"
@@ -27,15 +49,9 @@ msgstr ""
#. module: fetchmail_gmail
#: model:ir.model.fields,field_description:fetchmail_gmail.field_fetchmail_server__google_gmail_authorization_code
#: model_terms:ir.ui.view,arch_db:fetchmail_gmail.fetchmail_server_view_form
msgid "Authorization Code"
msgstr ""
#. module: fetchmail_gmail
#: model_terms:ir.ui.view,arch_db:fetchmail_gmail.fetchmail_server_view_form
msgid "Get an Authorization Code"
msgstr ""
#. module: fetchmail_gmail
#: model:ir.model.fields.selection,name:fetchmail_gmail.selection__fetchmail_server__server_type__gmail
msgid "Gmail OAuth Authentication"
@@ -58,7 +74,9 @@ msgstr ""
#. module: fetchmail_gmail
#: model_terms:ir.ui.view,arch_db:fetchmail_gmail.fetchmail_server_view_form
msgid "Setup your Gmail API credentials to link a Gmail account."
msgid ""
"Setup your Gmail API credentials in the general settings to link a Gmail "
"account."
msgstr ""
#. module: fetchmail_gmail
@@ -7,18 +7,30 @@
<field name="inherit_id" ref="fetchmail.view_email_server_form"/>
<field name="arch" type="xml">
<field name="user" position="after">
<field string="Authorization Code" name="google_gmail_authorization_code" password="True"
attrs="{'required': [('server_type', '=', 'gmail')], 'invisible': [('server_type', '!=', 'gmail')], 'readonly': [('state', '=', 'done')]}"
style="word-break: break-word;"/>
<field name="google_gmail_uri"
class="fa fa-arrow-right oe_edit_only"
widget="url"
text=" Get an Authorization Code"
attrs="{'invisible': ['|', ('server_type', '!=', 'gmail'), ('google_gmail_uri', '=', False)]}"
nolabel="1"/>
<div class="alert alert-warning" role="alert"
attrs="{'invisible': ['|', ('server_type', '!=', 'gmail'), ('google_gmail_uri', '!=', False)]}">
Setup your Gmail API credentials to link a Gmail account.
<field name="google_gmail_uri" invisible="1"/>
<field name="google_gmail_refresh_token" invisible="1"/>
<div></div>
<div attrs="{'invisible': [('server_type', '!=', 'gmail')]}">
<span attrs="{'invisible': ['|', ('server_type', '!=', 'gmail'), ('google_gmail_refresh_token', '=', False)]}"
class="badge badge-success">
Gmail Token Valid
</span>
<button type="object"
name="open_google_gmail_uri" class="btn-link px-0"
attrs="{'invisible': ['|', '|', ('google_gmail_uri', '=', False), ('server_type', '!=', 'gmail'), ('google_gmail_refresh_token', '!=', False)]}">
<i class="fa fa-arrow-right"/>
Connect your Gmail account
</button>
<button type="object"
name="open_google_gmail_uri" class="btn-link px-0"
attrs="{'invisible': ['|', '|', ('google_gmail_uri', '=', False), ('server_type', '!=', 'gmail'), ('google_gmail_refresh_token', '=', False)]}">
<i class="fa fa-cog"/>
Edit Settings
</button>
<div class="alert alert-warning" role="alert"
attrs="{'invisible': ['|', ('server_type', '!=', 'gmail'), ('google_gmail_uri', '!=', False)]}">
Setup your Gmail API credentials in the general settings to link a Gmail account.
</div>
</div>
</field>
<field name="password" position="attributes">
+1
View File
@@ -1,4 +1,5 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import controllers
from . import models
@@ -0,0 +1,4 @@
# -*- coding: utf-8 -*
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import main
+76
View File
@@ -0,0 +1,76 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import json
import logging
import werkzeug
from werkzeug.exceptions import Forbidden
from werkzeug.urls import url_encode
from odoo import _, http
from odoo.exceptions import UserError
from odoo.http import request
from odoo.tools import consteq
_logger = logging.getLogger(__name__)
class GoogleGmailController(http.Controller):
@http.route('/google_gmail/confirm', type='http', auth='user')
def google_gmail_callback(self, code=None, state=None, error=None, **kwargs):
"""Callback URL during the OAuth process.
Gmail redirects the user browser to this endpoint with the authorization code.
We will fetch the refresh token and the access token thanks to this authorization
code and save those values on the given mail server.
"""
if not request.env.user.has_group('base.group_system'):
_logger.error('Google Gmail: non-system user trying to link an Gmail account.')
raise Forbidden()
if error:
return _('An error occur during the authentication process: %s.', error)
try:
state = json.loads(state)
model_name = state['model']
rec_id = state['id']
csrf_token = state['csrf_token']
except Exception:
_logger.error('Google Gmail: Wrong state value %r.', state)
raise Forbidden()
model = request.env[model_name]
if not issubclass(type(model), request.env.registry['google.gmail.mixin']):
# The model must inherits from the "google.gmail.mixin" mixin
raise Forbidden()
record = model.browse(rec_id).exists()
if not record:
raise Forbidden()
if not csrf_token or not consteq(csrf_token, record._get_gmail_csrf_token()):
_logger.error('Google Gmail: Wrong CSRF token during Gmail authentication.')
raise Forbidden()
try:
refresh_token, access_token, expiration = record._fetch_gmail_refresh_token(code)
except UserError as e:
return _('An error occur during the authentication process: %s.', str(e.name))
record.write({
'google_gmail_access_token': access_token,
'google_gmail_access_token_expiration': expiration,
'google_gmail_authorization_code': code,
'google_gmail_refresh_token': refresh_token,
})
url_params = {
'id': rec_id,
'model': model_name,
'view_type': 'form'
}
url = '/web?#' + url_encode(url_params)
return werkzeug.utils.redirect(url, 303)
+50 -7
View File
@@ -15,6 +15,28 @@ msgstr ""
"Content-Transfer-Encoding: \n"
"Plural-Forms: \n"
#. module: google_gmail
#: model_terms:ir.ui.view,arch_db:google_gmail.ir_mail_server_view_form
msgid ""
"<i class=\"fa fa-arrow-right\"/>\n"
" Connect your Gmail account"
msgstr ""
#. module: google_gmail
#: model_terms:ir.ui.view,arch_db:google_gmail.ir_mail_server_view_form
msgid ""
"<i class=\"fa fa-cog\"/>\n"
" Edit Settings"
msgstr ""
#. module: google_gmail
#: model_terms:ir.ui.view,arch_db:google_gmail.ir_mail_server_view_form
msgid ""
"<span attrs=\"{'invisible': ['|', ('smtp_authentication', '!=', 'gmail'), ('google_gmail_refresh_token', '=', False)]}\" class=\"badge badge-success\">\n"
" Gmail Token Valid\n"
" </span>"
msgstr ""
#. module: google_gmail
#: model:ir.model.fields,field_description:google_gmail.field_google_gmail_mixin__google_gmail_access_token
#: model:ir.model.fields,field_description:google_gmail.field_ir_mail_server__google_gmail_access_token
@@ -27,6 +49,19 @@ msgstr ""
msgid "Access Token Expiration Timestamp"
msgstr ""
#. module: google_gmail
#: code:addons/google_gmail/controllers/main.py:0
#: code:addons/google_gmail/controllers/main.py:0
#, python-format
msgid "An error occur during the authentication process: %s."
msgstr ""
#. module: google_gmail
#: code:addons/google_gmail/models/google_gmail_mixin.py:0
#, python-format
msgid "An error occurred when fetching the access token."
msgstr ""
#. module: google_gmail
#: model:ir.model.fields,field_description:google_gmail.field_ir_mail_server__smtp_authentication
msgid "Authenticate with"
@@ -35,7 +70,6 @@ msgstr ""
#. module: google_gmail
#: model:ir.model.fields,field_description:google_gmail.field_google_gmail_mixin__google_gmail_authorization_code
#: model:ir.model.fields,field_description:google_gmail.field_ir_mail_server__google_gmail_authorization_code
#: model_terms:ir.ui.view,arch_db:google_gmail.ir_mail_server_view_form
msgid "Authorization Code"
msgstr ""
@@ -54,11 +88,6 @@ msgstr ""
msgid "Config Settings"
msgstr ""
#. module: google_gmail
#: model_terms:ir.ui.view,arch_db:google_gmail.ir_mail_server_view_form
msgid "Get an Authorization Code"
msgstr ""
#. module: google_gmail
#: model:ir.model.fields,field_description:google_gmail.field_res_config_settings__google_gmail_client_identifier
msgid "Gmail Client Id"
@@ -84,6 +113,18 @@ msgstr ""
msgid "Mail Server"
msgstr ""
#. module: google_gmail
#: code:addons/google_gmail/models/google_gmail_mixin.py:0
#, python-format
msgid "Only the administrator can link a Gmail mail server."
msgstr ""
#. module: google_gmail
#: code:addons/google_gmail/models/google_gmail_mixin.py:0
#, python-format
msgid "Please configure your Gmail credentials."
msgstr ""
#. module: google_gmail
#: model:ir.model.fields,field_description:google_gmail.field_google_gmail_mixin__google_gmail_refresh_token
#: model:ir.model.fields,field_description:google_gmail.field_ir_mail_server__google_gmail_refresh_token
@@ -92,7 +133,9 @@ msgstr ""
#. module: google_gmail
#: model_terms:ir.ui.view,arch_db:google_gmail.ir_mail_server_view_form
msgid "Setup your Gmail API credentials to link a Gmail account."
msgid ""
"Setup your Gmail API credentials in the general settings to link a Gmail "
"account."
msgstr ""
#. module: google_gmail
+122 -29
View File
@@ -1,10 +1,15 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import json
import logging
import time
import requests
from odoo import api, fields, models
from werkzeug.urls import url_encode, url_join
from odoo import _, api, fields, models, tools
from odoo.exceptions import AccessError, UserError
_logger = logging.getLogger(__name__)
@@ -27,38 +32,107 @@ class GoogleGmailMixin(models.AbstractModel):
Config = self.env['ir.config_parameter'].sudo()
google_gmail_client_id = Config.get_param('google_gmail_client_id')
google_gmail_client_secret = Config.get_param('google_gmail_client_secret')
base_url = self.get_base_url()
redirect_uri = url_join(base_url, '/google_gmail/confirm')
if not google_gmail_client_id or not google_gmail_client_secret:
self.google_gmail_uri = False
else:
google_gmail_uri = self.env['google.service']._get_google_token_uri('gmail', scope=self._SERVICE_SCOPE)
self.google_gmail_uri = google_gmail_uri
for record in self:
google_gmail_uri = 'https://accounts.google.com/o/oauth2/v2/auth?%s' % url_encode({
'client_id': google_gmail_client_id,
'redirect_uri': redirect_uri,
'response_type': 'code',
'scope': self._SERVICE_SCOPE,
# access_type and prompt needed to get a refresh token
'access_type': 'offline',
'prompt': 'consent',
'state': json.dumps({
'model': record._name,
'id': record.id or False,
'csrf_token': record._get_gmail_csrf_token() if record.id else False,
})
})
record.google_gmail_uri = google_gmail_uri
@api.model_create_multi
def create(self, vals_list):
for values in vals_list:
if values.get('google_gmail_authorization_code'):
# Generate the refresh token
values['google_gmail_refresh_token'] = self.env['google.service'].generate_refresh_token(
'gmail', values['google_gmail_authorization_code'])
values['google_gmail_access_token'] = False
values['google_gmail_access_token_expiration'] = False
def open_google_gmail_uri(self):
"""Open the URL to accept the Gmail permission.
return super(GoogleGmailMixin, self).create(vals_list)
This is done with an action, so we can force the user the save the form.
We need him to save the form so the current mail server record exist in DB, and
we can include the record ID in the URL.
"""
self.ensure_one()
def write(self, values):
authorization_code = values.get('google_gmail_authorization_code')
if (
authorization_code
and not all(authorization_code == code for code in self.mapped('google_gmail_authorization_code'))
):
# Update the refresh token
values['google_gmail_refresh_token'] = self.env['google.service'].generate_refresh_token(
'gmail', authorization_code)
values['google_gmail_access_token'] = False
values['google_gmail_access_token_expiration'] = False
if not self.env.user.has_group('base.group_system'):
raise AccessError(_('Only the administrator can link a Gmail mail server.'))
return super(GoogleGmailMixin, self).write(values)
if not self.google_gmail_uri:
raise UserError(_('Please configure your Gmail credentials.'))
return {
'type': 'ir.actions.act_url',
'url': self.google_gmail_uri,
}
def _fetch_gmail_refresh_token(self, authorization_code):
"""Request the refresh token and the initial access token from the authorization code.
:return:
refresh_token, access_token, access_token_expiration
"""
response = self._fetch_gmail_token('authorization_code', code=authorization_code)
return (
response['refresh_token'],
response['access_token'],
int(time.time()) + response['expires_in'],
)
def _fetch_gmail_access_token(self, refresh_token):
"""Refresh the access token thanks to the refresh token.
:return:
access_token, access_token_expiration
"""
response = self._fetch_gmail_token('refresh_token', refresh_token=refresh_token)
return (
response['access_token'],
int(time.time()) + response['expires_in'],
)
def _fetch_gmail_token(self, grant_type, **values):
"""Generic method to request an access token or a refresh token.
Return the JSON response of the GMail API and manage the errors which can occur.
:param grant_type: Depends the action we want to do (refresh_token or authorization_code)
:param values: Additional parameters that will be given to the GMail endpoint
"""
Config = self.env['ir.config_parameter'].sudo()
google_gmail_client_id = Config.get_param('google_gmail_client_id')
google_gmail_client_secret = Config.get_param('google_gmail_client_secret')
base_url = self.get_base_url()
redirect_uri = url_join(base_url, '/google_gmail/confirm')
response = requests.post(
'https://oauth2.googleapis.com/token',
data={
'client_id': google_gmail_client_id,
'client_secret': google_gmail_client_secret,
'grant_type': grant_type,
'redirect_uri': redirect_uri,
**values,
},
timeout=5,
)
if not response.ok:
raise UserError(_('An error occurred when fetching the access token.'))
return response.json()
def _generate_oauth2_string(self, user, refresh_token):
"""Generate a OAuth2 string which can be used for authentication.
@@ -73,14 +147,33 @@ class GoogleGmailMixin(models.AbstractModel):
if not self.google_gmail_access_token \
or not self.google_gmail_access_token_expiration \
or self.google_gmail_access_token_expiration < now_timestamp:
self.google_gmail_access_token, expires_in = self.env['google.service']._get_access_token(
refresh_token, 'gmail', self._SERVICE_SCOPE)
self.google_gmail_access_token_expiration = now_timestamp + expires_in
_logger.info('Google Gmail: fetch new access token. Expire in %i minutes', expires_in // 60)
access_token, expiration = self._fetch_gmail_access_token(self.google_gmail_refresh_token)
self.write({
'google_gmail_access_token': access_token,
'google_gmail_access_token_expiration': expiration,
})
_logger.info(
'Google Gmail: fetch new access token. Expires in %i minutes',
(self.google_gmail_access_token_expiration - now_timestamp) // 60)
else:
_logger.info(
'Google Gmail: reuse existing access token. Expire in %i minutes',
(self.google_gmail_access_token_expiration - now_timestamp) // 60)
return 'user=%s\1auth=Bearer %s\1\1' % (user, self.google_gmail_access_token)
def _get_gmail_csrf_token(self):
"""Generate a CSRF token that will be verified in `google_gmail_callback`.
This will prevent a malicious person to make an admin user disconnect the mail servers.
"""
self.ensure_one()
_logger.info('Google Gmail: generate CSRF token for %s #%i', self._name, self.id)
return tools.misc.hmac(
env=self.env(su=True),
scope='google_gmail_oauth',
message=(self._name, self.id),
)
@@ -6,17 +6,30 @@
<field name="inherit_id" ref="base.ir_mail_server_form"/>
<field name="arch" type="xml">
<field name="smtp_user" position="after">
<field string="Authorization Code" name="google_gmail_authorization_code" password="True"
attrs="{'required': [('smtp_authentication', '=', 'gmail')], 'invisible': [('smtp_authentication', '!=', 'gmail')]}"/>
<field name="google_gmail_uri"
class="fa fa-arrow-right oe_edit_only"
widget="url"
text=" Get an Authorization Code"
attrs="{'invisible': ['|', ('smtp_authentication', '!=', 'gmail'), ('google_gmail_uri', '=', False)]}"
nolabel="1"/>
<div class="alert alert-warning" role="alert"
attrs="{'invisible': ['|', ('smtp_authentication', '!=', 'gmail'), ('google_gmail_uri', '!=', False)]}">
Setup your Gmail API credentials to link a Gmail account.
<field name="google_gmail_uri" invisible="1"/>
<field name="google_gmail_refresh_token" invisible="1"/>
<div></div>
<div attrs="{'invisible': [('smtp_authentication', '!=', 'gmail')]}">
<span attrs="{'invisible': ['|', ('smtp_authentication', '!=', 'gmail'), ('google_gmail_refresh_token', '=', False)]}"
class="badge badge-success">
Gmail Token Valid
</span>
<button type="object"
name="open_google_gmail_uri" class="btn-link px-0"
attrs="{'invisible': ['|', '|', ('google_gmail_uri', '=', False), ('smtp_authentication', '!=', 'gmail'), ('google_gmail_refresh_token', '!=', False)]}">
<i class="fa fa-arrow-right"/>
Connect your Gmail account
</button>
<button type="object"
name="open_google_gmail_uri" class="btn-link px-0"
attrs="{'invisible': ['|', '|', ('google_gmail_uri', '=', False), ('smtp_authentication', '!=', 'gmail'), ('google_gmail_refresh_token', '=', False)]}">
<i class="fa fa-cog"/>
Edit Settings
</button>
<div class="alert alert-warning" role="alert"
attrs="{'invisible': ['|', ('smtp_authentication', '!=', 'gmail'), ('google_gmail_uri', '!=', False)]}">
Setup your Gmail API credentials in the general settings to link a Gmail account.
</div>
</div>
</field>
<field name="smtp_user" position="attributes">