From 2a4292391de143e49882f73a6d27e8d00f74f06e Mon Sep 17 00:00:00 2001 From: SilvioC2C Date: Tue, 26 Nov 2024 23:07:22 +0100 Subject: [PATCH] [IMP] webservice: use refresh token --- webservice/components/request_adapter.py | 52 ++++++++++++++---- webservice/models/webservice_backend.py | 67 ++++++++++++++++++++++++ webservice/views/webservice_backend.xml | 25 +++++++++ 3 files changed, 135 insertions(+), 9 deletions(-) diff --git a/webservice/components/request_adapter.py b/webservice/components/request_adapter.py index 24e18e90..813ebd17 100644 --- a/webservice/components/request_adapter.py +++ b/webservice/components/request_adapter.py @@ -4,16 +4,16 @@ # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). import json -import logging import time import requests -from oauthlib.oauth2 import BackendApplicationClient, WebApplicationClient +import urllib3 +from oauthlib.oauth2 import BackendApplicationClient, WebApplicationClient, rfc6749 from requests_oauthlib import OAuth2Session -from odoo.addons.component.core import Component +from odoo.tools.safe_eval import safe_eval -_logger = logging.getLogger(__name__) +from odoo.addons.component.core import Component class BaseRestRequestsAdapter(Component): @@ -133,17 +133,32 @@ def token(self): if self._is_token_valid(token): self._token = token else: - new_token = self._fetch_new_token(old_token=token) + new_token = {} + if backend.oauth2_use_refresh_token: + # pylint: disable=except-pass + try: + new_token = self._fetch_refresh_token(old_token=token) + except self._get_refresh_token_ignorable_exceptions(): + pass # Do nothing and let Odoo try to fetch a new token + if not new_token: + new_token = self._fetch_new_token(old_token=token) + json_token = json.dumps(new_token) cr.execute( - "UPDATE webservice_backend " "SET oauth2_token=%s " "WHERE id=%s", - (json.dumps(new_token), backend.id), + "UPDATE webservice_backend SET oauth2_token = %s WHERE id = %s", + (json_token, backend.id), ) + backend._compute_oauth2_token_expiration_datetime() self._token = new_token return self._token + def _get_refresh_token_ignorable_exceptions(self): + return ( + rfc6749.errors.OAuth2Error, + requests.exceptions.RequestException, + urllib3.exceptions.HTTPError, + ) + def _fetch_new_token(self, old_token): - # TODO: check if the old token has a refresh_token that can - # be used (and use it in that case) oauth_params = self.collection.sudo().read( [ "oauth2_clientid", @@ -163,6 +178,25 @@ def _fetch_new_token(self, old_token): ) return token + def _fetch_refresh_token(self, old_token): + backend = self.collection.sudo() + with OAuth2Session(client_id=backend.oauth2_clientid) as session: + # NB: the following section partially mimics ``OAuth2Session.request()``, + # but allows more flexibility to make this work with different providers + session.token = {} + res = session.request( + **safe_eval( + backend.oauth2_refresh_token_params.strip(), + {"webservice": backend, "adapter": self, "old_token": old_token}, + ) + ) + for hook in session.compliance_hook["access_token_response"]: + res = hook(res) + client = session._client + client.parse_request_body_response(res.text, scope=session.scope) + session.token = client.token + return session.token + def _request(self, method, url=None, url_params=None, **kwargs): url = self._get_url(url=url, url_params=url_params) new_kwargs = kwargs.copy() diff --git a/webservice/models/webservice_backend.py b/webservice/models/webservice_backend.py index a9121121..9a28d5bb 100644 --- a/webservice/models/webservice_backend.py +++ b/webservice/models/webservice_backend.py @@ -3,7 +3,11 @@ # @author Simone Orsi # @author Alexandre Fayolle # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +import json import logging +from datetime import datetime + +from requests_oauthlib import OAuth2Session from odoo import _, api, exceptions, fields, models @@ -57,6 +61,19 @@ class WebserviceBackend(models.Model): help="random key generated when authorization flow starts " "to ensure that no CSRF attack happen" ) + oauth2_token_expiration_datetime = fields.Datetime( + string="Token Expiration", + compute="_compute_oauth2_token_expiration_datetime", + compute_sudo=True, + store=True, + ) + oauth2_use_refresh_token = fields.Boolean(string="Use Refresh Token") + oauth2_refresh_token_params = fields.Text( + string="Refresh Token Params", + help="Parameters used by ``OAuth2Session.request()`` method to fetch a new" + " token through refresh of the old one", + default=lambda self: self._get_default_oauth2_refresh_token_params(), + ) content_type = fields.Selection( [ ("application/json", "JSON"), @@ -67,6 +84,31 @@ class WebserviceBackend(models.Model): ) company_id = fields.Many2one("res.company", string="Company") + @api.model + def _get_default_oauth2_refresh_token_params(self): + code = OAuth2Session.request.__code__ + varnames = [f"\n# - {v}" for v in code.co_varnames[1 : code.co_argcount + 1]] + txt = """ +# Define a dictionary with keyword arguments to pass to ``OAuth2Session.request()`` +# +# ``OAuth2Session.request()`` arguments:%s +# +# Available variables to use for dict definition (see example below): +# - webservice: current record +# - adapter: webservice's request adapter +# - old_token: pre-existing token (as dictionary) + +{ + 'method': 'POST', + 'url': webservice.oauth2_token_url, + 'data': { + 'key1': adapter.get_key1(), + 'key2': old_token['key2'], + }, +} +""" + return (txt % "".join(varnames)).lstrip() + @api.constrains("auth_type") def _check_auth_type(self): valid_fields = { @@ -175,3 +217,28 @@ def _compute_server_env(self): res = super()._compute_server_env() self.filtered(lambda r: r.auth_type != "oauth2").oauth2_flow = None return res + + @api.depends("oauth2_token") + def _compute_oauth2_token_expiration_datetime(self): + from_ts = datetime.fromtimestamp + for ws in self: + token_str = ws.oauth2_token + try: + token = json.loads(token_str or "{}") or {} + ws.oauth2_token_expiration_datetime = from_ts(token.get("expires_at")) + except Exception as e: + if token_str: + _logger.warning( + "Could not determine token expiration date from %s," + " got error: %s: %s", + token_str, + e.__class__.__name__, + str(e), + ) + ws.oauth2_token_expiration_datetime = None + + def button_refresh_token(self): + self.ensure_one() + adapter = self._get_adapter() + new_token = adapter._fetch_refresh_token(json.loads(self.oauth2_token)) + self.oauth2_token = json.dumps(new_token) diff --git a/webservice/views/webservice_backend.xml b/webservice/views/webservice_backend.xml index 87156b3f..8274cb70 100644 --- a/webservice/views/webservice_backend.xml +++ b/webservice/views/webservice_backend.xml @@ -8,6 +8,7 @@ webservice.backend
+
@@ -91,11 +98,29 @@ invisible="auth_type != 'oauth2'" required="auth_type == 'oauth2'" /> + + + + +