Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions webservice/README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,47 @@ HTTP call returns by default the content of the response. A context
.. contents::
:local:

Configuration
=============

OAuth2 (Client Credentials)
---------------------------

For the *Backend Application (Client Credentials Grant)* flow, two extra
options control how the token is requested, so that endpoints which
deviate from the OAuth2 spec can still be used:

- **Token Request Method**: ``POST`` (default) or ``GET``. Most
providers expose the token endpoint as a POST; some require a GET.
- **Client Authentication**: how the client credentials are presented to
the token endpoint:

- *Client ID & Secret (HTTP Basic)* (default): the client id and
secret are sent as an
``Authorization: Basic base64(client_id:client_secret)`` header
(``client_secret_basic``).
- *Custom Authorization header*: a static header value is sent
verbatim. The **Client Auth Header** (default ``Authorization``) and
**Client Auth Header Value** are configured directly; the Client ID
/ Client Secret fields are not used in this case.

Example: custom Authorization header
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Some providers require the credentials in a non-standard Authorization
header (for instance Okta uses ``Authorization: SSWS <token>``). Such an
endpoint can be configured as:

::

Auth Type = OAuth2
OAuth2 Flow = Backend Application (Client Credentials Grant)
Token URL = https://provider.example.com/oauth2/token
Token Request Method = GET
Client Authentication = Custom Authorization header
Client Auth Header = Authorization
Client Auth Value = SSWS <token>

Bug Tracker
===========

Expand Down
41 changes: 34 additions & 7 deletions webservice/components/request_adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@

from odoo.addons.component.core import Component

from ..utils import sanitize_url_for_log
from ..utils import StaticHeaderAuth, sanitize_url_for_log

_logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -160,23 +160,50 @@ def _fetch_new_token(self, old_token):
# be used (and use it in that case)
oauth_params = self.collection.sudo().read(
[
"oauth2_client_auth_method",
"oauth2_clientid",
"oauth2_client_secret",
"oauth2_client_auth_header",
"oauth2_client_auth_value",
"oauth2_token_url",
"oauth2_token_method",
"oauth2_audience",
"redirect_url",
]
)[0]
client = self.get_client(oauth_params)
with OAuth2Session(client=client) as session:
token = session.fetch_token(
token_url=oauth_params["oauth2_token_url"],
client_id=oauth_params["oauth2_clientid"],
client_secret=oauth_params["oauth2_client_secret"],
audience=oauth_params.get("oauth2_audience") or "",
)
token = session.fetch_token(**self._token_fetch_kwargs(oauth_params))
return token

def _token_fetch_kwargs(self, oauth_params):
"""Build the ``OAuth2Session.fetch_token`` keyword arguments.

Both the HTTP method used for the token request and the way the client
credentials are presented to the token endpoint depend on the backend
configuration, so that non fully spec-compliant endpoints can be used.
"""
kwargs = {
"method": oauth_params["oauth2_token_method"].upper(),
"token_url": oauth_params["oauth2_token_url"],
"audience": oauth_params.get("oauth2_audience") or "",
}
match oauth_params["oauth2_client_auth_method"]:
case "client_secret_basic":
kwargs["client_id"] = oauth_params["oauth2_clientid"]
kwargs["client_secret"] = oauth_params["oauth2_client_secret"]
case "custom_header":
kwargs["auth"] = StaticHeaderAuth(
oauth_params["oauth2_client_auth_header"],
oauth_params["oauth2_client_auth_value"],
)
case _:
raise ValueError(
"Unsupported OAuth2 client authentication method: "
f"{oauth_params['oauth2_client_auth_method']!r}"
)
return kwargs

def _request(self, method, url=None, url_params=None, **kwargs):
url = self._get_url(url=url, url_params=url_params)
content_only = kwargs.pop("content_only", True)
Expand Down
72 changes: 70 additions & 2 deletions webservice/models/webservice_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,37 @@ class WebserviceBackend(models.Model):
],
readonly=False,
)
oauth2_clientid = fields.Char(string="Client ID", auth_type="oauth2")
oauth2_client_secret = fields.Char(string="Client Secret", auth_type="oauth2")
oauth2_client_auth_method = fields.Selection(
[
("client_secret_basic", "Client ID & Secret (HTTP Basic)"),
("custom_header", "Custom Authorization header"),
],
default="client_secret_basic",
string="Client Authentication",
help="How the client credentials are presented to the token endpoint.",
)
oauth2_clientid = fields.Char(string="Client ID")
oauth2_client_secret = fields.Char(string="Client Secret")
oauth2_client_auth_header = fields.Char(
string="Client Auth Header",
default="Authorization",
help="Header name used to send the client credentials when the client "
"authentication method is a custom Authorization header.",
)
oauth2_client_auth_value = fields.Char(
string="Client Auth Header Value",
help="Full, static header value sent to the token endpoint when the "
"client authentication method is a custom Authorization header "
"(e.g. 'SSWS <token>').",
)
oauth2_token_url = fields.Char(string="Token URL", auth_type="oauth2")
oauth2_token_method = fields.Selection(
[("post", "POST"), ("get", "GET")],
default="post",
string="Token Request Method",
help="HTTP method used to request the token from the token endpoint. "
"Most providers use POST; some expose the token endpoint as GET.",
)
oauth2_authorization_url = fields.Char(string="Authorization URL")
oauth2_audience = fields.Char(
string="Audience"
Expand Down Expand Up @@ -83,6 +111,46 @@ def _check_auth_type(self):
if missing:
raise exceptions.UserError(rec._msg_missing_auth_param(missing))

@api.constrains(
"auth_type",
"oauth2_client_auth_method",
"oauth2_clientid",
"oauth2_client_secret",
)
def _check_oauth2_client_secret_basic(self):
for rec in self:
if rec.auth_type != "oauth2":
continue
if rec.oauth2_client_auth_method != "client_secret_basic":
continue
missing = [
rec._fields[fname]
for fname in ("oauth2_clientid", "oauth2_client_secret")
if not rec[fname]
]
if missing:
raise exceptions.UserError(rec._msg_missing_auth_param(missing))

@api.constrains(
"auth_type",
"oauth2_client_auth_method",
"oauth2_client_auth_header",
"oauth2_client_auth_value",
)
def _check_oauth2_custom_header(self):
for rec in self:
if rec.auth_type != "oauth2":
continue
if rec.oauth2_client_auth_method != "custom_header":
continue
missing = [
rec._fields[fname]
for fname in ("oauth2_client_auth_header", "oauth2_client_auth_value")
if not rec[fname]
]
if missing:
raise exceptions.UserError(rec._msg_missing_auth_param(missing))

def _msg_missing_auth_param(self, missing_fields):
def get_selection_value(fname):
return self._fields.get(fname).convert_to_export(self[fname], self)
Expand Down
31 changes: 31 additions & 0 deletions webservice/readme/CONFIGURE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
## OAuth2 (Client Credentials)

For the *Backend Application (Client Credentials Grant)* flow, two extra options
control how the token is requested, so that endpoints which deviate from the
OAuth2 spec can still be used:

- **Token Request Method**: `POST` (default) or `GET`. Most providers expose the
token endpoint as a POST; some require a GET.
- **Client Authentication**: how the client credentials are presented to the
token endpoint:
- *Client ID & Secret (HTTP Basic)* (default): the client id and secret are
sent as an `Authorization: Basic base64(client_id:client_secret)` header
(`client_secret_basic`).
- *Custom Authorization header*: a static header value is sent verbatim. The
**Client Auth Header** (default `Authorization`) and **Client Auth Header
Value** are configured directly; the Client ID / Client Secret fields are
not used in this case.

### Example: custom Authorization header

Some providers require the credentials in a non-standard Authorization header
(for instance Okta uses `Authorization: SSWS <token>`). Such an endpoint can be
configured as:

Auth Type = OAuth2
OAuth2 Flow = Backend Application (Client Credentials Grant)
Token URL = https://provider.example.com/oauth2/token
Token Request Method = GET
Client Authentication = Custom Authorization header
Client Auth Header = Authorization
Client Auth Value = SSWS <token>
67 changes: 57 additions & 10 deletions webservice/static/description/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -382,41 +382,88 @@ <h1>WebService</h1>
<p><strong>Table of contents</strong></p>
<div class="contents local topic" id="contents">
<ul class="simple">
<li><a class="reference internal" href="#bug-tracker" id="toc-entry-1">Bug Tracker</a></li>
<li><a class="reference internal" href="#credits" id="toc-entry-2">Credits</a><ul>
<li><a class="reference internal" href="#authors" id="toc-entry-3">Authors</a></li>
<li><a class="reference internal" href="#contributors" id="toc-entry-4">Contributors</a></li>
<li><a class="reference internal" href="#maintainers" id="toc-entry-5">Maintainers</a></li>
<li><a class="reference internal" href="#configuration" id="toc-entry-1">Configuration</a><ul>
<li><a class="reference internal" href="#oauth2-client-credentials" id="toc-entry-2">OAuth2 (Client Credentials)</a><ul>
<li><a class="reference internal" href="#example-custom-authorization-header" id="toc-entry-3">Example: custom Authorization header</a></li>
</ul>
</li>
</ul>
</li>
<li><a class="reference internal" href="#bug-tracker" id="toc-entry-4">Bug Tracker</a></li>
<li><a class="reference internal" href="#credits" id="toc-entry-5">Credits</a><ul>
<li><a class="reference internal" href="#authors" id="toc-entry-6">Authors</a></li>
<li><a class="reference internal" href="#contributors" id="toc-entry-7">Contributors</a></li>
<li><a class="reference internal" href="#maintainers" id="toc-entry-8">Maintainers</a></li>
</ul>
</li>
</ul>
</div>
<div class="section" id="configuration">
<h2><a class="toc-backref" href="#toc-entry-1">Configuration</a></h2>
<div class="section" id="oauth2-client-credentials">
<h3><a class="toc-backref" href="#toc-entry-2">OAuth2 (Client Credentials)</a></h3>
<p>For the <em>Backend Application (Client Credentials Grant)</em> flow, two extra
options control how the token is requested, so that endpoints which
deviate from the OAuth2 spec can still be used:</p>
<ul class="simple">
<li><strong>Token Request Method</strong>: <tt class="docutils literal">POST</tt> (default) or <tt class="docutils literal">GET</tt>. Most
providers expose the token endpoint as a POST; some require a GET.</li>
<li><strong>Client Authentication</strong>: how the client credentials are presented to
the token endpoint:<ul>
<li><em>Client ID &amp; Secret (HTTP Basic)</em> (default): the client id and
secret are sent as an
<tt class="docutils literal">Authorization: Basic base64(client_id:client_secret)</tt> header
(<tt class="docutils literal">client_secret_basic</tt>).</li>
<li><em>Custom Authorization header</em>: a static header value is sent
verbatim. The <strong>Client Auth Header</strong> (default <tt class="docutils literal">Authorization</tt>) and
<strong>Client Auth Header Value</strong> are configured directly; the Client ID
/ Client Secret fields are not used in this case.</li>
</ul>
</li>
</ul>
<div class="section" id="example-custom-authorization-header">
<h4><a class="toc-backref" href="#toc-entry-3">Example: custom Authorization header</a></h4>
<p>Some providers require the credentials in a non-standard Authorization
header (for instance Okta uses <tt class="docutils literal">Authorization: SSWS &lt;token&gt;</tt>). Such an
endpoint can be configured as:</p>
<pre class="literal-block">
Auth Type = OAuth2
OAuth2 Flow = Backend Application (Client Credentials Grant)
Token URL = https://provider.example.com/oauth2/token
Token Request Method = GET
Client Authentication = Custom Authorization header
Client Auth Header = Authorization
Client Auth Value = SSWS &lt;token&gt;
</pre>
</div>
</div>
</div>
<div class="section" id="bug-tracker">
<h2><a class="toc-backref" href="#toc-entry-1">Bug Tracker</a></h2>
<h2><a class="toc-backref" href="#toc-entry-4">Bug Tracker</a></h2>
<p>Bugs are tracked on <a class="reference external" href="https://github.com/OCA/web-api/issues">GitHub Issues</a>.
In case of trouble, please check there if your issue has already been reported.
If you spotted it first, help us to smash it by providing a detailed and welcomed
<a class="reference external" href="https://github.com/OCA/web-api/issues/new?body=module:%20webservice%0Aversion:%2019.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**">feedback</a>.</p>
<p>Do not contact contributors directly about support or help with technical issues.</p>
</div>
<div class="section" id="credits">
<h2><a class="toc-backref" href="#toc-entry-2">Credits</a></h2>
<h2><a class="toc-backref" href="#toc-entry-5">Credits</a></h2>
<div class="section" id="authors">
<h3><a class="toc-backref" href="#toc-entry-3">Authors</a></h3>
<h3><a class="toc-backref" href="#toc-entry-6">Authors</a></h3>
<ul class="simple">
<li>Creu Blanca</li>
<li>Camptocamp</li>
</ul>
</div>
<div class="section" id="contributors">
<h3><a class="toc-backref" href="#toc-entry-4">Contributors</a></h3>
<h3><a class="toc-backref" href="#toc-entry-7">Contributors</a></h3>
<ul class="simple">
<li>Enric Tobella &lt;<a class="reference external" href="mailto:etobella&#64;creublanca.es">etobella&#64;creublanca.es</a>&gt;</li>
<li>Alexandre Fayolle &lt;<a class="reference external" href="mailto:alexandre.fayolle&#64;camptocamp.com">alexandre.fayolle&#64;camptocamp.com</a>&gt;</li>
</ul>
</div>
<div class="section" id="maintainers">
<h3><a class="toc-backref" href="#toc-entry-5">Maintainers</a></h3>
<h3><a class="toc-backref" href="#toc-entry-8">Maintainers</a></h3>
<p>This module is maintained by the OCA.</p>
<a class="reference external image-reference" href="https://odoo-community.org">
<img alt="Odoo Community Association" src="https://odoo-community.org/logo.png" />
Expand Down
Loading
Loading