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
51 changes: 44 additions & 7 deletions auth_jwt/README.rst
Original file line number Diff line number Diff line change
@@ -1,7 +1,3 @@
.. image:: https://odoo-community.org/readme-banner-image
:target: https://odoo-community.org/get-involved?utm_source=readme
:alt: Odoo Community Association

========
Auth JWT
========
Expand All @@ -17,7 +13,7 @@ Auth JWT
.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png
:target: https://odoo-community.org/page/development-status
:alt: Beta
.. |badge2| image:: https://img.shields.io/badge/license-LGPL--3-blue.png
.. |badge2| image:: https://img.shields.io/badge/licence-LGPL--3-blue.png
:target: http://www.gnu.org/licenses/lgpl-3.0-standalone.html
:alt: License: LGPL-3
.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fserver--auth-lightgray.png?logo=github
Expand Down Expand Up @@ -65,14 +61,54 @@ The JWT validator can be configured with the following properties:

- ``name``: the validator name, to match the
``auth="jwt_{validator-name}"`` route property.
- ``audience``: a comma-separated list of allowed audiences, used to
validate the ``aud`` claim.
- ``audience``: a comma-separated list of values that must intersect
with the JWT claim selected by ``audience_type`` (by default the
standard ``aud`` claim — see "Audience type" below for matching
against other claims like ``groups`` or ``scope``).
- ``audience_type``: selects which JWT payload claim the ``audience``
list is matched against — ``Audience`` (default, validates ``aud``),
``Group``, ``Scope``, or ``Custom``. See "Audience type" below.
- ``audience_type_custom``: when ``audience_type`` is ``Custom``, the
JWT payload key to validate against the ``audience`` list (e.g.
``cognito:groups``, ``permissions``).
- ``issuer``: used to validate the ``iss`` claim.
- Signature type (secret or public key), algorithm, secret and JWK URI
are used to validate the token signature.

In addition, the ``exp`` claim is validated to reject expired tokens.

**Audience type — matching non-standard JWT claims.** The ``audience``
setting is matched against the standard JWT ``aud`` claim by default
(RFC 7519). Some identity providers — notably AWS Cognito and several
OAuth2-only IdPs — issue access tokens without an ``aud`` claim but
expose authorization information under other claims (``cognito:groups``,
``scope``, ``roles``). The ``audience_type`` field controls which claim
the ``audience`` list is matched against:

- **Audience** (default): standard ``aud`` claim validation; at least
one configured value must be present in the token's ``aud`` claim.
- **Group**: validates against the ``groups`` claim (array or
space-separated string).
- **Scope**: validates against the ``scope`` claim (space-separated per
OAuth2 RFC 6749 §3.3, or an array).
- **Custom**: validates against the arbitrary payload key configured in
*Custom Audience Type Key* (e.g. ``cognito:groups``, ``permissions``,
``https://example.com/claims/roles``).

For all non-``aud`` types the JWT library's built-in ``aud``
verification is skipped (the token has no ``aud``) and the match is a
set intersection: any one of the configured ``audience`` values
appearing in the token's claim authorizes the request.

**Example — AWS Cognito access token.** Cognito access tokens carry no
``aud`` claim but include ``cognito:groups`` (e.g.
``["odoo-admin", "odoo-portal"]``) and ``scope`` (e.g.
``"openid profile odoo/read"``). To restrict a route to clients in the
``odoo-admin`` Cognito group, configure the validator with
``audience_type = Custom``, ``audience_type_custom = cognito:groups``,
and ``audience = odoo-admin``. To restrict by OAuth scope instead,
configure ``audience_type = Scope`` and ``audience = odoo/read``.

If the ``Authorization`` HTTP header is missing, malformed, or contains
an invalid token, the request is rejected with a 401 (Unauthorized)
code, unless the cookie mode is enabled (see below).
Expand Down Expand Up @@ -141,6 +177,7 @@ Contributors

- Stéphane Bidoul <stephane.bidoul@acsone.eu>
- Mohamed Alkobrosli <malkobrosly@kencove.com>
- Don Kendall <kendall@donkendall.com>

Maintainers
-----------
Expand Down
2 changes: 1 addition & 1 deletion auth_jwt/__manifest__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"name": "Auth JWT",
"summary": """
JWT bearer token authentication.""",
"version": "18.0.1.0.2",
"version": "18.0.1.1.0",
"license": "LGPL-3",
"author": "ACSONE SA/NV,Odoo Community Association (OCA)",
"maintainers": ["sbidoul"],
Expand Down
61 changes: 56 additions & 5 deletions auth_jwt/models/auth_jwt_validator.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,44 @@ class AuthJwtValidator(models.Model):
],
default="RS256",
)
audience_type = fields.Selection(
[
("aud", "Audience"),
("group", "Group"),
("scope", "Scope"),
("custom", "Custom"),
],
required=True,
default="aud",
help=(
"Which JWT payload claim to validate the Audience list against:\n"
"- Audience (default): standard `aud` claim per RFC 7519.\n"
"- Group: matches against the token's `groups` claim. Useful "
"when the IdP exposes group membership but doesn't set `aud` "
"(typical for first-party OAuth2 access tokens).\n"
"- Scope: matches against the `scope` claim (space-separated "
"per OAuth2 RFC 6749 §3.3, or an array).\n"
"- Custom: matches against an arbitrary payload key specified "
"in Custom Audience Type Key (e.g. `cognito:groups`)."
),
)
audience_type_custom = fields.Char(
help=(
"JWT payload key to validate against the Audience list. Only "
"used when Audience Type is Custom. Example: `cognito:groups`, "
"`roles`, `permissions`, `https://example.com/claims/roles`."
),
)
audience = fields.Char(
required=True, help="Comma separated list of audiences, to validate aud."
required=True,
help=(
"Comma-separated values that must intersect with the JWT claim "
"selected by Audience Type. At least one value must be present "
"in the token for the request to be authorized. For Audience "
"type this validates the standard `aud` claim; for other types "
"this is a set-intersection check against the corresponding "
"payload field."
),
)
issuer = fields.Char(required=True, help="To validate iss.")
user_id_strategy = fields.Selection(
Expand Down Expand Up @@ -161,7 +197,7 @@ def _get_validator_by_name(self, validator_name):

@tools.ormcache("self.public_key_jwk_uri", "kid")
def _get_key(self, kid):
jwks_client = PyJWKClient(self.public_key_jwk_uri, cache_keys=False)
jwks_client = PyJWKClient(self.public_key_jwk_uri)
return jwks_client.get_signing_key(kid).key

def _encode(self, payload, secret, expire):
Expand Down Expand Up @@ -195,20 +231,35 @@ def _decode(self, token, secret=None):
raise UnauthorizedInvalidToken() from e
key = self._get_key(header.get("kid"))
algorithm = self.public_key_algorithm
aud = (self.audience or "").split(",") if self.audience_type == "aud" else None
try:
payload = jwt.decode(
token,
key=key,
algorithms=[algorithm],
options=dict(
require=["exp", "aud", "iss"],
require=["exp", "iss"],
verify_exp=True,
verify_aud=True,
verify_iss=True,
),
audience=self.audience.split(","),
audience=aud,
issuer=self.issuer,
)
payload_key = (
self.audience_type_custom
if self.audience_type == "custom"
else self.audience_type
)
if len((self.audience or "").split(",") or []) > 0:
for key_value in (self.audience or "").split(","):
payload_value = (
payload.get(payload_key)
if isinstance(payload.get(payload_key), list)
else (payload.get(payload_key) or "").split(" ")
)
if key_value in payload_value:
return payload
raise UnauthorizedInvalidToken()
except Exception as e:
_logger.info("Invalid token: %s", e)
raise UnauthorizedInvalidToken() from e
Expand Down
1 change: 1 addition & 0 deletions auth_jwt/readme/CONTRIBUTORS.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
- Stéphane Bidoul \<<stephane.bidoul@acsone.eu>\>
- Mohamed Alkobrosli \<<malkobrosly@kencove.com>\>
- Don Kendall \<<kendall@donkendall.com>\>
44 changes: 42 additions & 2 deletions auth_jwt/readme/USAGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,54 @@ The JWT validator can be configured with the following properties:

- `name`: the validator name, to match the `auth="jwt_{validator-name}"`
route property.
- `audience`: a comma-separated list of allowed audiences, used to
validate the `aud` claim.
- `audience`: a comma-separated list of values that must intersect with
the JWT claim selected by `audience_type` (by default the standard
`aud` claim — see "Audience type" below for matching against other
claims like `groups` or `scope`).
- `audience_type`: selects which JWT payload claim the `audience` list
is matched against — `Audience` (default, validates `aud`), `Group`,
`Scope`, or `Custom`. See "Audience type" below.
- `audience_type_custom`: when `audience_type` is `Custom`, the JWT
payload key to validate against the `audience` list (e.g.
`cognito:groups`, `permissions`).
- `issuer`: used to validate the `iss` claim.
- Signature type (secret or public key), algorithm, secret and JWK URI
are used to validate the token signature.

In addition, the `exp` claim is validated to reject expired tokens.

**Audience type — matching non-standard JWT claims.** The `audience`
setting is matched against the standard JWT `aud` claim by default
(RFC 7519). Some identity providers — notably AWS Cognito and several
OAuth2-only IdPs — issue access tokens without an `aud` claim but
expose authorization information under other claims (`cognito:groups`,
`scope`, `roles`). The `audience_type` field controls which claim the
`audience` list is matched against:

- **Audience** (default): standard `aud` claim validation; at least
one configured value must be present in the token's `aud` claim.
- **Group**: validates against the `groups` claim (array or
space-separated string).
- **Scope**: validates against the `scope` claim (space-separated per
OAuth2 RFC 6749 §3.3, or an array).
- **Custom**: validates against the arbitrary payload key configured
in *Custom Audience Type Key* (e.g. `cognito:groups`, `permissions`,
`https://example.com/claims/roles`).

For all non-`aud` types the JWT library's built-in `aud` verification
is skipped (the token has no `aud`) and the match is a set
intersection: any one of the configured `audience` values appearing in
the token's claim authorizes the request.

**Example — AWS Cognito access token.** Cognito access tokens carry
no `aud` claim but include `cognito:groups`
(e.g. `["odoo-admin", "odoo-portal"]`) and `scope`
(e.g. `"openid profile odoo/read"`). To restrict a route to clients
in the `odoo-admin` Cognito group, configure the validator with
`audience_type = Custom`, `audience_type_custom = cognito:groups`,
and `audience = odoo-admin`. To restrict by OAuth scope instead,
configure `audience_type = Scope` and `audience = odoo/read`.

If the `Authorization` HTTP header is missing, malformed, or contains an
invalid token, the request is rejected with a 401 (Unauthorized) code,
unless the cookie mode is enabled (see below).
Expand Down
Loading
Loading