diff --git a/auth_jwt/README.rst b/auth_jwt/README.rst index c63b8c4476..4316d4d8fe 100644 --- a/auth_jwt/README.rst +++ b/auth_jwt/README.rst @@ -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 ======== @@ -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 @@ -141,6 +137,7 @@ Contributors - Stéphane Bidoul - Mohamed Alkobrosli +- Don Kendall Maintainers ----------- diff --git a/auth_jwt/__manifest__.py b/auth_jwt/__manifest__.py index 8a311e5bdf..1acd7340a6 100644 --- a/auth_jwt/__manifest__.py +++ b/auth_jwt/__manifest__.py @@ -5,7 +5,7 @@ "name": "Auth JWT", "summary": """ JWT bearer token authentication.""", - "version": "18.0.1.0.0", + "version": "18.0.1.1.0", "license": "LGPL-3", "author": "ACSONE SA/NV,Odoo Community Association (OCA)", "maintainers": ["sbidoul"], diff --git a/auth_jwt/models/auth_jwt_validator.py b/auth_jwt/models/auth_jwt_validator.py index 13649adad2..bcc47fea8b 100644 --- a/auth_jwt/models/auth_jwt_validator.py +++ b/auth_jwt/models/auth_jwt_validator.py @@ -64,8 +64,19 @@ class AuthJwtValidator(models.Model): ], default="RS256", ) + audience_type = fields.Selection( + [ + ("aud", "Audience"), + ("group", "Group"), + ("scope", "Scope"), + ("custom", "Custom"), + ], + required=True, + default="aud", + ) + audience_type_custom = fields.Char(required=False, help="payload key to validate") audience = fields.Char( - required=True, help="Comma separated list of audiences, to validate aud." + required=True, help="Comma separated list of attribute needed." ) issuer = fields.Char(required=True, help="To validate iss.") user_id_strategy = fields.Selection( @@ -160,7 +171,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): @@ -194,20 +205,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 diff --git a/auth_jwt/readme/CONTRIBUTORS.md b/auth_jwt/readme/CONTRIBUTORS.md index d6260f557c..2a984e3e28 100644 --- a/auth_jwt/readme/CONTRIBUTORS.md +++ b/auth_jwt/readme/CONTRIBUTORS.md @@ -1,2 +1,3 @@ - Stéphane Bidoul \<\> - Mohamed Alkobrosli \<\> +- Don Kendall \<\> diff --git a/auth_jwt/static/description/index.html b/auth_jwt/static/description/index.html index 8813f94463..b046234278 100644 --- a/auth_jwt/static/description/index.html +++ b/auth_jwt/static/description/index.html @@ -3,7 +3,7 @@ -README.rst +Auth JWT -
+
+

Auth JWT

- - -Odoo Community Association - -
-

Auth JWT

-

Beta License: LGPL-3 OCA/server-auth Translate me on Weblate Try me on Runboat

+

Beta License: LGPL-3 OCA/server-auth Translate me on Weblate Try me on Runboat

JWT bearer token authentication.

Table of contents

@@ -391,11 +386,11 @@

Auth JWT

-

Installation

+

Installation

This module requires the pyjwt library to be installed.

-

Usage

+

Usage

This module lets developpers add a new jwt authentication method on Odoo controller routes.

To use it, you must:

@@ -459,7 +454,7 @@

Usage

with a different user by providing a new JWT token.

-

Bug Tracker

+

Bug Tracker

Bugs are tracked on GitHub Issues. 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 @@ -467,22 +462,23 @@

Bug Tracker

Do not contact contributors directly about support or help with technical issues.

-

Credits

+

Credits

-

Authors

+

Authors

  • ACSONE SA/NV
-

Contributors

+

Contributors

-

Maintainers

+

Maintainers

This module is maintained by the OCA.

Odoo Community Association @@ -497,6 +493,5 @@

Maintainers

-
diff --git a/auth_jwt/tests/test_auth_jwt.py b/auth_jwt/tests/test_auth_jwt.py index 6a87e87cbc..b3b973fef7 100644 --- a/auth_jwt/tests/test_auth_jwt.py +++ b/auth_jwt/tests/test_auth_jwt.py @@ -404,3 +404,46 @@ def test_public_or_jwt_valid_token(self): with self._mock_request(authorization=authorization) as request: self.env["ir.http"]._auth_method_public_or_jwt_validator() assert request.jwt_payload["aud"] == "me" + + def test_valid_token_with_audience_type_aud(self): + validator = self._create_validator("val1", audience="client1,client2") + validator.audience_type = "aud" + token = self._create_token(audience="client1") + payload = validator._decode(token) + self.assertEqual(payload["aud"], "client1") + + def test_invalid_audience_with_audience_type_aud(self): + validator = self._create_validator("val2", audience="client1,client2") + validator.audience_type = "aud" + token = self._create_token(audience="otherclient") + with self.assertRaises(UnauthorizedInvalidToken): + validator._decode(token) + + def test_valid_token_with_custom_audience_type(self): + validator = self._create_validator("val3", audience="read,write") + validator.audience_type = "custom" + validator.audience_type_custom = "scope" + payload = { + "iss": "http://the.issuer", + "exp": time.time() + 100, + "scope": "read write", # token claim space-separated + } + token = jwt.encode(payload, "thesecret", algorithm="HS256") + decoded = validator._decode(token) + self.assertIn("read", decoded["scope"].split(" ")) + + def test_invalid_custom_audience_type(self): + validator = self._create_validator("val4", audience="read") + validator.audience_type = "custom" + validator.audience_type_custom = "scope" + token = self._create_token() + # No scope claim in token + with self.assertRaises(UnauthorizedInvalidToken): + validator._decode(token) + + def test_invalid_signature_rejected(self): + validator = self._create_validator("val5", audience="client1") + validator.audience_type = "aud" + token = self._create_token(audience="client1", key="wrongsecret") + with self.assertRaises(UnauthorizedInvalidToken): + validator._decode(token) diff --git a/auth_jwt/views/auth_jwt_validator_views.xml b/auth_jwt/views/auth_jwt_validator_views.xml index 8aac0f500f..1a6f054ee2 100644 --- a/auth_jwt/views/auth_jwt_validator_views.xml +++ b/auth_jwt/views/auth_jwt_validator_views.xml @@ -12,8 +12,12 @@ + + -