From a0ca0a04124bbfab53af71929df783b8a3071984 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= Date: Wed, 28 Apr 2021 11:29:55 +0200 Subject: [PATCH 01/43] [ADD] auth_jwt --- auth_jwt/README.rst | 129 +++++++++++ auth_jwt/__init__.py | 1 + auth_jwt/__manifest__.py | 17 ++ auth_jwt/exceptions.py | 32 +++ auth_jwt/models/__init__.py | 2 + auth_jwt/models/auth_jwt_validator.py | 186 ++++++++++++++++ auth_jwt/models/ir_http.py | 80 +++++++ auth_jwt/readme/CONTRIBUTORS.rst | 1 + auth_jwt/readme/DESCRIPTION.rst | 1 + auth_jwt/readme/INSTALL.rst | 1 + auth_jwt/readme/USAGE.rst | 39 ++++ auth_jwt/security/ir.model.access.csv | 2 + auth_jwt/static/description/icon.png | Bin 0 -> 9455 bytes auth_jwt/tests/__init__.py | 1 + auth_jwt/tests/test_auth_jwt.py | 229 ++++++++++++++++++++ auth_jwt/views/auth_jwt_validator_views.xml | 88 ++++++++ 16 files changed, 809 insertions(+) create mode 100644 auth_jwt/README.rst create mode 100644 auth_jwt/__init__.py create mode 100644 auth_jwt/__manifest__.py create mode 100644 auth_jwt/exceptions.py create mode 100644 auth_jwt/models/__init__.py create mode 100644 auth_jwt/models/auth_jwt_validator.py create mode 100644 auth_jwt/models/ir_http.py create mode 100644 auth_jwt/readme/CONTRIBUTORS.rst create mode 100644 auth_jwt/readme/DESCRIPTION.rst create mode 100644 auth_jwt/readme/INSTALL.rst create mode 100644 auth_jwt/readme/USAGE.rst create mode 100644 auth_jwt/security/ir.model.access.csv create mode 100644 auth_jwt/static/description/icon.png create mode 100644 auth_jwt/tests/__init__.py create mode 100644 auth_jwt/tests/test_auth_jwt.py create mode 100644 auth_jwt/views/auth_jwt_validator_views.xml diff --git a/auth_jwt/README.rst b/auth_jwt/README.rst new file mode 100644 index 0000000000..cf670acff7 --- /dev/null +++ b/auth_jwt/README.rst @@ -0,0 +1,129 @@ +======== +Auth JWT +======== + +.. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |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/licence-AGPL--3-blue.png + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fserver--auth-lightgray.png?logo=github + :target: https://github.com/OCA/server-auth/tree/13.0/auth_jwt + :alt: OCA/server-auth +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/server-auth-13-0/server-auth-13-0-auth_jwt + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runbot-Try%20me-875A7B.png + :target: https://runbot.odoo-community.org/runbot/251/13.0 + :alt: Try me on Runbot + +|badge1| |badge2| |badge3| |badge4| |badge5| + +JWT bearer token authentication. + +**Table of contents** + +.. contents:: + :local: + +Installation +============ + +This module requires the ``python-jose`` library to be installed. + +Usage +===== + +This module lets developpers add a new ``jwt`` authentication method on Odoo +controller routes. + +To use it, you must: + +* Create an ``auth.jwt.validator`` record to configure how the JWT token will + be validated. +* Add an ``auth="jwt_{validator-name}"`` attribute to the routes + you want to protect where ``{validator-name}`` corresponds to the name + attribute of the JWT validator record. + +The ``auth_jwt_test`` module provides examples. + +The JWT validator can be configured with the following properties: + +* ``name``: the validator name, to match the ``auth="jwt_{validator-name}"`` + route property. +* ``audience``: used to validate the ``aud`` claim. +* ``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. + +If the ``Authorization`` HTTP header is missing, malformed, or contains +an invalid token, the request is rejected with a 401 (Unauthorized) code. + +If the token is valid, the request executes with the configured user id. By +default the user id selection strategy is ``static`` (i.e. the same for all +requests) and the selected user is configured on the JWT validator. Additional +strategies can be provided by overriding the ``_get_uid()`` method and +extending the ``user_id_strategy`` selection field.. + +Additionally, if a ``partner_id_strategy`` is configured, a partner is searched +and if found, its id is stored in the ``request.partner_id`` attribute. If +``partner_id_required`` is set, a 401 (Unauthorized) is returned if no partner +was found. Otherwise ``request.partner_id`` is left falsy. Additional +strategies can be provided by overriding the ``_get_partner_id()`` method +and extending the ``partner_id_strategy`` selection field. + +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 smashing it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +~~~~~~~ + +* ACSONE SA/NV + +Contributors +~~~~~~~~~~~~ + +* Stéphane Bidoul + +Maintainers +~~~~~~~~~~~ + +This module is maintained by the OCA. + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use. + +.. |maintainer-sbidoul| image:: https://github.com/sbidoul.png?size=40px + :target: https://github.com/sbidoul + :alt: sbidoul + +Current `maintainer `__: + +|maintainer-sbidoul| + +This module is part of the `OCA/server-auth `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/auth_jwt/__init__.py b/auth_jwt/__init__.py new file mode 100644 index 0000000000..0650744f6b --- /dev/null +++ b/auth_jwt/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/auth_jwt/__manifest__.py b/auth_jwt/__manifest__.py new file mode 100644 index 0000000000..e632e255c8 --- /dev/null +++ b/auth_jwt/__manifest__.py @@ -0,0 +1,17 @@ +# Copyright 2021 ACSONE SA/NV +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +{ + "name": "Auth JWT", + "summary": """ + JWT bearer token authentication.""", + "version": "13.0.1.0.0", + "license": "AGPL-3", + "author": "ACSONE SA/NV,Odoo Community Association (OCA)", + "maintainers": ["sbidoul"], + "website": "https://github.com/OCA/server-auth", + "depends": [], + "external_dependencies": {"python": ["python-jose", "cryptography"]}, + "data": ["security/ir.model.access.csv", "views/auth_jwt_validator_views.xml"], + "demo": [], +} diff --git a/auth_jwt/exceptions.py b/auth_jwt/exceptions.py new file mode 100644 index 0000000000..dbebaff04d --- /dev/null +++ b/auth_jwt/exceptions.py @@ -0,0 +1,32 @@ +# Copyright 2021 ACSONE SA/NV +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from werkzeug.exceptions import InternalServerError, Unauthorized + + +class UnauthorizedMissingAuthorizationHeader(Unauthorized): + pass + + +class UnauthorizedMalformedAuthorizationHeader(Unauthorized): + pass + + +class UnauthorizedSessionMismatch(Unauthorized): + pass + + +class AmbiguousJwtValidator(InternalServerError): + pass + + +class JwtValidatorNotFound(InternalServerError): + pass + + +class UnauthorizedInvalidToken(Unauthorized): + pass + + +class UnauthorizedPartnerNotFound(Unauthorized): + pass diff --git a/auth_jwt/models/__init__.py b/auth_jwt/models/__init__.py new file mode 100644 index 0000000000..49b44a2b20 --- /dev/null +++ b/auth_jwt/models/__init__.py @@ -0,0 +1,2 @@ +from . import auth_jwt_validator +from . import ir_http diff --git a/auth_jwt/models/auth_jwt_validator.py b/auth_jwt/models/auth_jwt_validator.py new file mode 100644 index 0000000000..5490356ccc --- /dev/null +++ b/auth_jwt/models/auth_jwt_validator.py @@ -0,0 +1,186 @@ +# Copyright 2021 ACSONE SA/NV +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +import logging +from functools import partial + +import requests +from jose import jwt # pylint: disable=missing-manifest-dependency +from werkzeug.exceptions import InternalServerError + +from odoo import _, api, fields, models, tools +from odoo.exceptions import ValidationError + +from ..exceptions import ( + AmbiguousJwtValidator, + JwtValidatorNotFound, + UnauthorizedInvalidToken, + UnauthorizedPartnerNotFound, +) + +_logger = logging.getLogger(__name__) + + +class AuthJwtValidator(models.Model): + _name = "auth.jwt.validator" + _description = "JWT Validator Configuration" + + name = fields.Char(required=True) + signature_type = fields.Selection( + [("secret", "Secret"), ("public_key", "Public key")], required=True + ) + secret_key = fields.Char() + secret_algorithm = fields.Selection([("HS256", "HS256")], default="HS256") # TODO + public_key_jwk_uri = fields.Char() + public_key_algorithm = fields.Selection( + [("RS256", "RS256")], default="RS256" + ) # TODO + audience = fields.Char(required=True, help="To validate aud.") + issuer = fields.Char(required=True, help="To validate iss.") + user_id_strategy = fields.Selection( + [("static", "Static")], required=True, default="static" + ) + static_user_id = fields.Many2one("res.users", default=1) + partner_id_strategy = fields.Selection([("email", "From email claim")]) + partner_id_required = fields.Boolean() + + _sql_constraints = [ + ("name_uniq", "unique(name)", "JWT validator names must be unique !"), + ] + + @api.constrains("name") + def _check_name(self): + for rec in self: + if not rec.name.isidentifier(): + raise ValidationError( + _("Name %r is not a valid python identifier.") % (rec.name,) + ) + + @api.model + def _get_validator_by_name_domain(self, validator_name): + if validator_name: + return [("name", "=", validator_name)] + return [] + + @api.model + def _get_validator_by_name(self, validator_name): + domain = self._get_validator_by_name_domain(validator_name) + validator = self.search(domain) + if not validator: + _logger.error("JWT validator not found for name %r", validator_name) + raise JwtValidatorNotFound() + if len(validator) != 1: + _logger.error( + "More than one JWT validator found for name %r", validator_name + ) + raise AmbiguousJwtValidator() + return validator + + @tools.ormcache("self.public_key_jwk_uri", "kid") + def _get_key(self, kid): + r = requests.get(self.public_key_jwk_uri) + r.raise_for_status() + response = r.json() + for key in response["keys"]: + if key["kid"] == kid: + return key + return {} + + def _decode(self, token): + """Validate and decode a JWT token, return the payload.""" + if self.signature_type == "secret": + key = self.secret_key + algorithm = self.secret_algorithm + else: + try: + header = jwt.get_unverified_header(token) + except jwt.JWTError as e: + _logger.info("Invalid token: %s", e) + raise UnauthorizedInvalidToken() + key = self._get_key(header.get("kid")) + algorithm = self.public_key_algorithm + try: + payload = jwt.decode( + token, + key=key, + algorithms=[algorithm], + options=dict( + require_exp=True, verify_exp=True, verify_aud=True, verify_iss=True + ), + audience=self.audience, + issuer=self.issuer, + ) + except jwt.JWTError as e: + _logger.info("Invalid token: %s", e) + raise UnauthorizedInvalidToken() + return payload + + def _get_uid(self, payload): + # override for additional strategies + if self.user_id_strategy == "static": + return self.static_user_id.id + + def _get_and_check_uid(self, payload): + uid = self._get_uid(payload) + if not uid: + _logger.error("_get_uid did not return a user id") + raise InternalServerError() + return uid + + def _get_partner_id(self, payload): + # override for additional strategies + if self.partner_id_strategy == "email": + email = payload.get("email") + if not email: + _logger.debug("JWT payload does not have an email claim") + return + partner = self.env["res.partner"].search([("email", "=", email)]) + if len(partner) != 1: + _logger.debug("%d partners found for email %s", len(partner), email) + return + return partner.id + + def _get_and_check_partner_id(self, payload): + partner_id = self._get_partner_id(payload) + if not partner_id and self.partner_id_required: + raise UnauthorizedPartnerNotFound() + return partner_id + + def _register_hook(self): + res = super()._register_hook() + self.search([])._register_auth_method() + return res + + def _register_auth_method(self): + IrHttp = self.env["ir.http"] + for rec in self: + setattr( + IrHttp.__class__, + f"_auth_method_jwt_{rec.name}", + partial(IrHttp.__class__._auth_method_jwt, validator_name=rec.name), + ) + + def _unregister_auth_method(self): + IrHttp = self.env["ir.http"] + for rec in self: + try: + delattr(IrHttp.__class__, f"_auth_method_jwt_{rec.name}") + except AttributeError: + pass + + @api.model_create_multi + def create(self, vals): + rec = super().create(vals) + rec._register_auth_method() + return rec + + def write(self, vals): + if "name" in vals: + self._unregister_auth_method() + res = super().write(vals) + self._register_auth_method() + return res + + def unlink(self): + self._unregister_auth_method() + return super().unlink() diff --git a/auth_jwt/models/ir_http.py b/auth_jwt/models/ir_http.py new file mode 100644 index 0000000000..5100fdfdac --- /dev/null +++ b/auth_jwt/models/ir_http.py @@ -0,0 +1,80 @@ +# Copyright 2021 ACSONE SA/NV +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +import logging +import re + +from odoo import SUPERUSER_ID, api, models, registry as registry_get +from odoo.http import request + +from ..exceptions import ( + UnauthorizedMalformedAuthorizationHeader, + UnauthorizedMissingAuthorizationHeader, + UnauthorizedSessionMismatch, +) + +_logger = logging.getLogger(__name__) + + +AUTHORIZATION_RE = re.compile(r"^Bearer ([^ ]+)$") + + +class IrHttpJwt(models.AbstractModel): + + _inherit = "ir.http" + + @classmethod + def _authenticate(cls, auth_method="user"): + """Protect the _authenticate method. + + This is to ensure that the _authenticate method is called + in the correct conditions to invoke _auth_method_jwt below. + When migrating, review this method carefully by reading the original + _authenticate method and make sure the conditions have not changed. + """ + if auth_method == "jwt" or auth_method.startswith("jwt_"): + if request.session.uid: + _logger.warning( + 'A route with auth="jwt" must not be used within a user session.' + ) + raise UnauthorizedSessionMismatch() + if request.uid: + _logger.error( + 'A route with auth="jwt" should not have a request.uid here.' + ) + raise UnauthorizedSessionMismatch() + return super()._authenticate(auth_method) + + @classmethod + def _auth_method_jwt(cls, validator_name=None): + assert request.db + assert not request.uid + assert not request.session.uid + token = cls._get_bearer_token() + assert token + registry = registry_get(request.db) + with registry.cursor() as cr: + env = api.Environment(cr, SUPERUSER_ID, {}) + validator = env["auth.jwt.validator"]._get_validator_by_name(validator_name) + assert len(validator) == 1 + payload = validator._decode(token) + uid = validator._get_and_check_uid(payload) + assert uid + partner_id = validator._get_and_check_partner_id(payload) + request.uid = uid # this resets request.env + request.jwt_payload = payload + request.jwt_partner_id = partner_id + + @classmethod + def _get_bearer_token(cls): + # https://tools.ietf.org/html/rfc2617#section-3.2.2 + authorization = request.httprequest.environ.get("HTTP_AUTHORIZATION") + if not authorization: + _logger.info("Missing Authorization header.") + raise UnauthorizedMissingAuthorizationHeader() + # https://tools.ietf.org/html/rfc6750#section-2.1 + mo = AUTHORIZATION_RE.match(authorization) + if not mo: + _logger.info("Malformed Authorization header.") + raise UnauthorizedMalformedAuthorizationHeader() + return mo.group(1) diff --git a/auth_jwt/readme/CONTRIBUTORS.rst b/auth_jwt/readme/CONTRIBUTORS.rst new file mode 100644 index 0000000000..f323b44ab0 --- /dev/null +++ b/auth_jwt/readme/CONTRIBUTORS.rst @@ -0,0 +1 @@ +* Stéphane Bidoul diff --git a/auth_jwt/readme/DESCRIPTION.rst b/auth_jwt/readme/DESCRIPTION.rst new file mode 100644 index 0000000000..9322c82e13 --- /dev/null +++ b/auth_jwt/readme/DESCRIPTION.rst @@ -0,0 +1 @@ +JWT bearer token authentication. diff --git a/auth_jwt/readme/INSTALL.rst b/auth_jwt/readme/INSTALL.rst new file mode 100644 index 0000000000..62a3d4fc43 --- /dev/null +++ b/auth_jwt/readme/INSTALL.rst @@ -0,0 +1 @@ +This module requires the ``python-jose`` library to be installed. diff --git a/auth_jwt/readme/USAGE.rst b/auth_jwt/readme/USAGE.rst new file mode 100644 index 0000000000..d8bd837037 --- /dev/null +++ b/auth_jwt/readme/USAGE.rst @@ -0,0 +1,39 @@ +This module lets developpers add a new ``jwt`` authentication method on Odoo +controller routes. + +To use it, you must: + +* Create an ``auth.jwt.validator`` record to configure how the JWT token will + be validated. +* Add an ``auth="jwt_{validator-name}"`` attribute to the routes + you want to protect where ``{validator-name}`` corresponds to the name + attribute of the JWT validator record. + +The ``auth_jwt_test`` module provides examples. + +The JWT validator can be configured with the following properties: + +* ``name``: the validator name, to match the ``auth="jwt_{validator-name}"`` + route property. +* ``audience``: used to validate the ``aud`` claim. +* ``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. + +If the ``Authorization`` HTTP header is missing, malformed, or contains +an invalid token, the request is rejected with a 401 (Unauthorized) code. + +If the token is valid, the request executes with the configured user id. By +default the user id selection strategy is ``static`` (i.e. the same for all +requests) and the selected user is configured on the JWT validator. Additional +strategies can be provided by overriding the ``_get_uid()`` method and +extending the ``user_id_strategy`` selection field.. + +Additionally, if a ``partner_id_strategy`` is configured, a partner is searched +and if found, its id is stored in the ``request.partner_id`` attribute. If +``partner_id_required`` is set, a 401 (Unauthorized) is returned if no partner +was found. Otherwise ``request.partner_id`` is left falsy. Additional +strategies can be provided by overriding the ``_get_partner_id()`` method +and extending the ``partner_id_strategy`` selection field. diff --git a/auth_jwt/security/ir.model.access.csv b/auth_jwt/security/ir.model.access.csv new file mode 100644 index 0000000000..3935420e6e --- /dev/null +++ b/auth_jwt/security/ir.model.access.csv @@ -0,0 +1,2 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_auth_jwt_validator_admin,auth_jwt_validator admin,model_auth_jwt_validator,base.group_system,1,1,1,1 diff --git a/auth_jwt/static/description/icon.png b/auth_jwt/static/description/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..3a0328b516c4980e8e44cdb63fd945757ddd132d GIT binary patch literal 9455 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 z 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 Xviia!U7SGha1wx#SCgwmn*{w2TRX*I literal 0 HcmV?d00001 diff --git a/auth_jwt/tests/__init__.py b/auth_jwt/tests/__init__.py new file mode 100644 index 0000000000..3a4e62d18f --- /dev/null +++ b/auth_jwt/tests/__init__.py @@ -0,0 +1 @@ +from . import test_auth_jwt diff --git a/auth_jwt/tests/test_auth_jwt.py b/auth_jwt/tests/test_auth_jwt.py new file mode 100644 index 0000000000..5d8c660f84 --- /dev/null +++ b/auth_jwt/tests/test_auth_jwt.py @@ -0,0 +1,229 @@ +# Copyright 2021 ACSONE SA/NV +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +import contextlib +import time +from unittest.mock import Mock + +from jose import jwt + +import odoo.http +from odoo.exceptions import ValidationError +from odoo.tests.common import TransactionCase +from odoo.tools import mute_logger +from odoo.tools.misc import DotDict + +from ..exceptions import ( + AmbiguousJwtValidator, + JwtValidatorNotFound, + UnauthorizedInvalidToken, + UnauthorizedMalformedAuthorizationHeader, + UnauthorizedMissingAuthorizationHeader, + UnauthorizedPartnerNotFound, +) + + +class TestAuthMethod(TransactionCase): + @contextlib.contextmanager + def _mock_request(self, authorization): + request = Mock( + context={}, + db=self.env.cr.dbname, + uid=None, + httprequest=Mock(environ={"HTTP_AUTHORIZATION": authorization}), + session=DotDict(), + ) + + with contextlib.ExitStack() as s: + odoo.http._request_stack.push(request) + s.callback(odoo.http._request_stack.pop) + yield request + + def _create_token( + self, + key="thesecret", + audience="me", + issuer="http://the.issuer", + exp_delta=100, + email=None, + ): + payload = dict(aud=audience, iss=issuer, exp=time.time() + exp_delta) + if email: + payload["email"] = email + return jwt.encode(payload, key=key, algorithm="HS256") + + def _create_validator(self, name, audience="me", partner_id_required=False): + return self.env["auth.jwt.validator"].create( + dict( + name=name, + signature_type="secret", + secret_algorithm="HS256", + secret_key="thesecret", + audience=audience, + issuer="http://the.issuer", + user_id_strategy="static", + partner_id_strategy="email", + partner_id_required=partner_id_required, + ) + ) + + @contextlib.contextmanager + def _commit_validator(self, name, audience="me", partner_id_required=False): + validator = self._create_validator( + name=name, audience=audience, partner_id_required=partner_id_required + ) + # commit because IrHttp._auth_method_jwt will look for validator in another tx + self.env.cr.commit() # pylint: disable=invalid-commit + try: + yield validator + finally: + validator.unlink() + self.env.cr.commit() # pylint: disable=invalid-commit + + def test_missing_authorization_header(self): + with self._mock_request(authorization=None): + with self.assertRaises(UnauthorizedMissingAuthorizationHeader): + self.env["ir.http"]._auth_method_jwt() + + def test_malformed_authorization_header(self): + for authorization in ( + "a", + "Bearer", + "Bearer ", + "Bearer x y", + "Bearer token ", + "bearer token", + ): + with self._mock_request(authorization=authorization): + with self.assertRaises(UnauthorizedMalformedAuthorizationHeader): + self.env["ir.http"]._auth_method_jwt() + + def test_auth_method_valid_token(self): + with self._commit_validator("validator"): + authorization = "Bearer " + self._create_token() + with self._mock_request(authorization=authorization): + self.env["ir.http"]._auth_method_jwt_validator() + + def test_auth_method_valid_token_two_validators(self): + with self._commit_validator( + "validator2", audience="bad" + ), self._commit_validator("validator3"): + authorization = "Bearer " + self._create_token() + with self._mock_request(authorization=authorization): + # first validator rejects the token because of invalid audience + with self.assertRaises(UnauthorizedInvalidToken): + self.env["ir.http"]._auth_method_jwt_validator2() + # second validator accepts the token + self.env["ir.http"]._auth_method_jwt_validator3() + + def test_auth_method_invalid_token(self): + # Test invalid token via _auth_method_jwt + # Other types of invalid tokens are unit tested elswhere. + with self._commit_validator("validator4"): + authorization = "Bearer " + self._create_token(audience="bad") + with self._mock_request(authorization=authorization): + with self.assertRaises(UnauthorizedInvalidToken): + self.env["ir.http"]._auth_method_jwt_validator4() + + def test_user_id_strategy(self): + with self._commit_validator("validator5") as validator: + authorization = "Bearer " + self._create_token() + with self._mock_request(authorization=authorization) as request: + self.env["ir.http"]._auth_method_jwt_validator5() + self.assertEqual(request.uid, validator.static_user_id.id) + + def test_partner_id_strategy_email_found(self): + partner = self.env["res.partner"].search([("email", "!=", False)])[0] + with self._commit_validator("validator6"): + authorization = "Bearer " + self._create_token(email=partner.email) + with self._mock_request(authorization=authorization) as request: + self.env["ir.http"]._auth_method_jwt_validator6() + self.assertEqual(request.jwt_partner_id, partner.id) + + def test_partner_id_strategy_email_not_found(self): + with self._commit_validator("validator6"): + authorization = "Bearer " + self._create_token( + email="notanemail@example.com" + ) + with self._mock_request(authorization=authorization) as request: + self.env["ir.http"]._auth_method_jwt_validator6() + self.assertFalse(request.jwt_partner_id) + + def test_partner_id_strategy_email_not_found_partner_required(self): + with self._commit_validator("validator6", partner_id_required=True): + authorization = "Bearer " + self._create_token( + email="notanemail@example.com" + ) + with self._mock_request(authorization=authorization): + with self.assertRaises(UnauthorizedPartnerNotFound): + self.env["ir.http"]._auth_method_jwt_validator6() + + def test_get_validator(self): + AuthJwtValidator = self.env["auth.jwt.validator"] + AuthJwtValidator.search([]).unlink() + with self.assertRaises(JwtValidatorNotFound), mute_logger( + "odoo.addons.auth_jwt.models.auth_jwt_validator" + ): + AuthJwtValidator._get_validator_by_name(None) + with self.assertRaises(JwtValidatorNotFound), mute_logger( + "odoo.addons.auth_jwt.models.auth_jwt_validator" + ): + AuthJwtValidator._get_validator_by_name("notavalidator") + validator1 = self._create_validator(name="validator1") + with self.assertRaises(JwtValidatorNotFound), mute_logger( + "odoo.addons.auth_jwt.models.auth_jwt_validator" + ): + AuthJwtValidator._get_validator_by_name("notavalidator") + self.assertEqual(AuthJwtValidator._get_validator_by_name(None), validator1) + self.assertEqual( + AuthJwtValidator._get_validator_by_name("validator1"), validator1 + ) + # create a second validator + validator2 = self._create_validator(name="validator2") + with self.assertRaises(AmbiguousJwtValidator), mute_logger( + "odoo.addons.auth_jwt.models.auth_jwt_validator" + ): + AuthJwtValidator._get_validator_by_name(None) + self.assertEqual( + AuthJwtValidator._get_validator_by_name("validator2"), validator2 + ) + + def test_bad_tokens(self): + validator = self._create_validator("validator") + token = self._create_token(key="badsecret") + with self.assertRaises(UnauthorizedInvalidToken): + validator._decode(token) + token = self._create_token(audience="badaudience") + with self.assertRaises(UnauthorizedInvalidToken): + validator._decode(token) + token = self._create_token(issuer="badissuer") + with self.assertRaises(UnauthorizedInvalidToken): + validator._decode(token) + token = self._create_token(exp_delta=-100) + with self.assertRaises(UnauthorizedInvalidToken): + validator._decode(token) + + def test_auth_method_registration_on_create(self): + IrHttp = self.env["ir.http"] + self.assertFalse(hasattr(IrHttp.__class__, "_auth_method_jwt_validator1")) + self._create_validator("validator1") + self.assertTrue(hasattr(IrHttp.__class__, "_auth_method_jwt_validator1")) + + def test_auth_method_unregistration_on_unlink(self): + IrHttp = self.env["ir.http"] + validator = self._create_validator("validator1") + self.assertTrue(hasattr(IrHttp.__class__, "_auth_method_jwt_validator1")) + validator.unlink() + self.assertFalse(hasattr(IrHttp.__class__, "_auth_method_jwt_validator1")) + + def test_auth_method_registration_on_rename(self): + IrHttp = self.env["ir.http"] + validator = self._create_validator("validator1") + self.assertTrue(hasattr(IrHttp.__class__, "_auth_method_jwt_validator1")) + validator.name = "validator2" + self.assertFalse(hasattr(IrHttp.__class__, "_auth_method_jwt_validator1")) + self.assertTrue(hasattr(IrHttp.__class__, "_auth_method_jwt_validator2")) + + def test_name_check(self): + with self.assertRaises(ValidationError): + self._create_validator(name="not an identifier") diff --git a/auth_jwt/views/auth_jwt_validator_views.xml b/auth_jwt/views/auth_jwt_validator_views.xml new file mode 100644 index 0000000000..11c9c42e75 --- /dev/null +++ b/auth_jwt/views/auth_jwt_validator_views.xml @@ -0,0 +1,88 @@ + + + + auth.jwt.validator.form + auth.jwt.validator + +
+ + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ + auth.jwt.validator.tree + auth.jwt.validator + + + + + + + + + + + + + + JWT Validators + auth.jwt.validator + tree,form + + +
From da4dfd00c2cccceea41a392aa3c87024e3c4a5aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= Date: Wed, 28 Apr 2021 15:01:08 +0200 Subject: [PATCH 02/43] auth_jwt: use PyJWT instead of python-jose Because it allows validating with a list of audiences. --- auth_jwt/README.rst | 2 +- auth_jwt/__manifest__.py | 2 +- auth_jwt/models/auth_jwt_validator.py | 13 ++++++++----- auth_jwt/readme/INSTALL.rst | 2 +- auth_jwt/tests/test_auth_jwt.py | 2 +- 5 files changed, 12 insertions(+), 9 deletions(-) diff --git a/auth_jwt/README.rst b/auth_jwt/README.rst index cf670acff7..cf7ec02b57 100644 --- a/auth_jwt/README.rst +++ b/auth_jwt/README.rst @@ -35,7 +35,7 @@ JWT bearer token authentication. Installation ============ -This module requires the ``python-jose`` library to be installed. +This module requires the ``pyjwt`` library to be installed. Usage ===== diff --git a/auth_jwt/__manifest__.py b/auth_jwt/__manifest__.py index e632e255c8..af3a772047 100644 --- a/auth_jwt/__manifest__.py +++ b/auth_jwt/__manifest__.py @@ -11,7 +11,7 @@ "maintainers": ["sbidoul"], "website": "https://github.com/OCA/server-auth", "depends": [], - "external_dependencies": {"python": ["python-jose", "cryptography"]}, + "external_dependencies": {"python": ["pyjwt", "cryptography"]}, "data": ["security/ir.model.access.csv", "views/auth_jwt_validator_views.xml"], "demo": [], } diff --git a/auth_jwt/models/auth_jwt_validator.py b/auth_jwt/models/auth_jwt_validator.py index 5490356ccc..b25ace08a0 100644 --- a/auth_jwt/models/auth_jwt_validator.py +++ b/auth_jwt/models/auth_jwt_validator.py @@ -4,8 +4,8 @@ import logging from functools import partial +import jwt # pylint: disable=missing-manifest-dependency import requests -from jose import jwt # pylint: disable=missing-manifest-dependency from werkzeug.exceptions import InternalServerError from odoo import _, api, fields, models, tools @@ -94,7 +94,7 @@ def _decode(self, token): else: try: header = jwt.get_unverified_header(token) - except jwt.JWTError as e: + except Exception as e: _logger.info("Invalid token: %s", e) raise UnauthorizedInvalidToken() key = self._get_key(header.get("kid")) @@ -105,12 +105,15 @@ def _decode(self, token): key=key, algorithms=[algorithm], options=dict( - require_exp=True, verify_exp=True, verify_aud=True, verify_iss=True + require=["exp", "aud", "iss"], + verify_exp=True, + verify_aud=True, + verify_iss=True, ), - audience=self.audience, + audience=[self.audience], issuer=self.issuer, ) - except jwt.JWTError as e: + except Exception as e: _logger.info("Invalid token: %s", e) raise UnauthorizedInvalidToken() return payload diff --git a/auth_jwt/readme/INSTALL.rst b/auth_jwt/readme/INSTALL.rst index 62a3d4fc43..9d8ccacf56 100644 --- a/auth_jwt/readme/INSTALL.rst +++ b/auth_jwt/readme/INSTALL.rst @@ -1 +1 @@ -This module requires the ``python-jose`` library to be installed. +This module requires the ``pyjwt`` library to be installed. diff --git a/auth_jwt/tests/test_auth_jwt.py b/auth_jwt/tests/test_auth_jwt.py index 5d8c660f84..e30b078233 100644 --- a/auth_jwt/tests/test_auth_jwt.py +++ b/auth_jwt/tests/test_auth_jwt.py @@ -5,7 +5,7 @@ import time from unittest.mock import Mock -from jose import jwt +import jwt import odoo.http from odoo.exceptions import ValidationError From 22e8c80637e6b8b53193b62fc93bc0a3d10932a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= Date: Wed, 28 Apr 2021 15:08:31 +0200 Subject: [PATCH 03/43] auth_jwt: add signature algorithms --- auth_jwt/models/auth_jwt_validator.py | 27 ++++++++++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/auth_jwt/models/auth_jwt_validator.py b/auth_jwt/models/auth_jwt_validator.py index b25ace08a0..855c49c08d 100644 --- a/auth_jwt/models/auth_jwt_validator.py +++ b/auth_jwt/models/auth_jwt_validator.py @@ -30,11 +30,32 @@ class AuthJwtValidator(models.Model): [("secret", "Secret"), ("public_key", "Public key")], required=True ) secret_key = fields.Char() - secret_algorithm = fields.Selection([("HS256", "HS256")], default="HS256") # TODO + secret_algorithm = fields.Selection( + [ + # https://pyjwt.readthedocs.io/en/stable/algorithms.html + ("HS256", "HS256 - HMAC using SHA-256 hash algorithm"), + ("HS384", "HS384 - HMAC using SHA-384 hash algorithm"), + ("HS512", "HS512 - HMAC using SHA-512 hash algorithm"), + ], + default="HS256", + ) public_key_jwk_uri = fields.Char() public_key_algorithm = fields.Selection( - [("RS256", "RS256")], default="RS256" - ) # TODO + [ + # https://pyjwt.readthedocs.io/en/stable/algorithms.html + ("ES256", "ES256 - ECDSA using SHA-256"), + ("ES256K", "ES256K - ECDSA with secp256k1 curve using SHA-256"), + ("ES384", "ES384 - ECDSA using SHA-384"), + ("ES512", "ES512 - ECDSA using SHA-512"), + ("RS256", "RS256 - RSASSA-PKCS1-v1_5 using SHA-256"), + ("RS384", "RS384 - RSASSA-PKCS1-v1_5 using SHA-384"), + ("RS512", "RS512 - RSASSA-PKCS1-v1_5 using SHA-512"), + ("PS256", "PS256 - RSASSA-PSS using SHA-256 and MGF1 padding with SHA-256"), + ("PS384", "PS384 - RSASSA-PSS using SHA-384 and MGF1 padding with SHA-384"), + ("PS512", "PS512 - RSASSA-PSS using SHA-512 and MGF1 padding with SHA-512"), + ], + default="RS256", + ) audience = fields.Char(required=True, help="To validate aud.") issuer = fields.Char(required=True, help="To validate iss.") user_id_strategy = fields.Selection( From 910a716044f8a70fe1109c2983f92f8b0317f592 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= Date: Wed, 28 Apr 2021 15:17:53 +0200 Subject: [PATCH 04/43] auth_jwt: support multiple audiences --- auth_jwt/models/auth_jwt_validator.py | 6 ++++-- auth_jwt/readme/USAGE.rst | 3 ++- auth_jwt/tests/test_auth_jwt.py | 10 ++++++++++ 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/auth_jwt/models/auth_jwt_validator.py b/auth_jwt/models/auth_jwt_validator.py index 855c49c08d..c8a6f397ab 100644 --- a/auth_jwt/models/auth_jwt_validator.py +++ b/auth_jwt/models/auth_jwt_validator.py @@ -56,7 +56,9 @@ class AuthJwtValidator(models.Model): ], default="RS256", ) - audience = fields.Char(required=True, help="To validate aud.") + audience = fields.Char( + required=True, help="Comma separated list of audiences, to validate aud." + ) issuer = fields.Char(required=True, help="To validate iss.") user_id_strategy = fields.Selection( [("static", "Static")], required=True, default="static" @@ -131,7 +133,7 @@ def _decode(self, token): verify_aud=True, verify_iss=True, ), - audience=[self.audience], + audience=self.audience.split(","), issuer=self.issuer, ) except Exception as e: diff --git a/auth_jwt/readme/USAGE.rst b/auth_jwt/readme/USAGE.rst index d8bd837037..749615fa1f 100644 --- a/auth_jwt/readme/USAGE.rst +++ b/auth_jwt/readme/USAGE.rst @@ -15,7 +15,8 @@ The JWT validator can be configured with the following properties: * ``name``: the validator name, to match the ``auth="jwt_{validator-name}"`` route property. -* ``audience``: used to validate the ``aud`` claim. +* ``audience``: a comma-separated list of allowed audiences, used to validate + the ``aud`` claim. * ``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. diff --git a/auth_jwt/tests/test_auth_jwt.py b/auth_jwt/tests/test_auth_jwt.py index e30b078233..fd115ec246 100644 --- a/auth_jwt/tests/test_auth_jwt.py +++ b/auth_jwt/tests/test_auth_jwt.py @@ -203,6 +203,16 @@ def test_bad_tokens(self): with self.assertRaises(UnauthorizedInvalidToken): validator._decode(token) + def test_multiple_aud(self): + validator = self._create_validator("validator", audience="a1,a2") + token = self._create_token(audience="a1") + validator._decode(token) + token = self._create_token(audience="a2") + validator._decode(token) + token = self._create_token(audience="a3") + with self.assertRaises(UnauthorizedInvalidToken): + validator._decode(token) + def test_auth_method_registration_on_create(self): IrHttp = self.env["ir.http"] self.assertFalse(hasattr(IrHttp.__class__, "_auth_method_jwt_validator1")) From 40aedcca5f7ae37d942a5a9bd246a076da7c6c32 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= Date: Wed, 28 Apr 2021 15:28:26 +0200 Subject: [PATCH 05/43] auth_jwt: add nbf validation test --- auth_jwt/tests/test_auth_jwt.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/auth_jwt/tests/test_auth_jwt.py b/auth_jwt/tests/test_auth_jwt.py index fd115ec246..c38f522daa 100644 --- a/auth_jwt/tests/test_auth_jwt.py +++ b/auth_jwt/tests/test_auth_jwt.py @@ -45,11 +45,14 @@ def _create_token( audience="me", issuer="http://the.issuer", exp_delta=100, + nbf=None, email=None, ): payload = dict(aud=audience, iss=issuer, exp=time.time() + exp_delta) if email: payload["email"] = email + if nbf: + payload["nbf"] = nbf return jwt.encode(payload, key=key, algorithm="HS256") def _create_validator(self, name, audience="me", partner_id_required=False): @@ -213,6 +216,14 @@ def test_multiple_aud(self): with self.assertRaises(UnauthorizedInvalidToken): validator._decode(token) + def test_nbf(self): + validator = self._create_validator("validator") + token = self._create_token(nbf=time.time() - 60) + validator._decode(token) + token = self._create_token(nbf=time.time() + 60) + with self.assertRaises(UnauthorizedInvalidToken): + validator._decode(token) + def test_auth_method_registration_on_create(self): IrHttp = self.env["ir.http"] self.assertFalse(hasattr(IrHttp.__class__, "_auth_method_jwt_validator1")) From 06897a44667a1e64f7d54a04afe7389ea90f52f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= Date: Thu, 29 Apr 2021 14:37:15 +0200 Subject: [PATCH 06/43] auth_jwt: docs clarification and fixes --- auth_jwt/readme/USAGE.rst | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/auth_jwt/readme/USAGE.rst b/auth_jwt/readme/USAGE.rst index 749615fa1f..03390e3cf0 100644 --- a/auth_jwt/readme/USAGE.rst +++ b/auth_jwt/readme/USAGE.rst @@ -30,11 +30,18 @@ If the token is valid, the request executes with the configured user id. By default the user id selection strategy is ``static`` (i.e. the same for all requests) and the selected user is configured on the JWT validator. Additional strategies can be provided by overriding the ``_get_uid()`` method and -extending the ``user_id_strategy`` selection field.. +extending the ``user_id_strategy`` selection field. + +The selected user is *not* stored in the session. It is only available in +``request.uid`` (and thus it is the one used in ``request.env``). To avoid any +confusion and mismatches between the bearer token and the session, this module +rejects requests made with an authenticated user session. Additionally, if a ``partner_id_strategy`` is configured, a partner is searched -and if found, its id is stored in the ``request.partner_id`` attribute. If +and if found, its id is stored in the ``request.jwt_partner_id`` attribute. If ``partner_id_required`` is set, a 401 (Unauthorized) is returned if no partner -was found. Otherwise ``request.partner_id`` is left falsy. Additional +was found. Otherwise ``request.jwt_partner_id`` is left falsy. Additional strategies can be provided by overriding the ``_get_partner_id()`` method and extending the ``partner_id_strategy`` selection field. + +The decoded JWT payload is stored in ``request.jwt_payload``. From 05a10d5d166c89700abae543dced8ed3a6ea4445 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= Date: Fri, 25 Jun 2021 19:37:16 +0200 Subject: [PATCH 07/43] auth_jwt: fix jwks URI support Make it work with pyjwt. --- auth_jwt/models/auth_jwt_validator.py | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/auth_jwt/models/auth_jwt_validator.py b/auth_jwt/models/auth_jwt_validator.py index c8a6f397ab..e5813f193d 100644 --- a/auth_jwt/models/auth_jwt_validator.py +++ b/auth_jwt/models/auth_jwt_validator.py @@ -5,7 +5,7 @@ from functools import partial import jwt # pylint: disable=missing-manifest-dependency -import requests +from jwt import PyJWKClient from werkzeug.exceptions import InternalServerError from odoo import _, api, fields, models, tools @@ -101,13 +101,8 @@ def _get_validator_by_name(self, validator_name): @tools.ormcache("self.public_key_jwk_uri", "kid") def _get_key(self, kid): - r = requests.get(self.public_key_jwk_uri) - r.raise_for_status() - response = r.json() - for key in response["keys"]: - if key["kid"] == kid: - return key - return {} + jwks_client = PyJWKClient(self.public_key_jwk_uri, cache_keys=False) + return jwks_client.get_signing_key(kid).key def _decode(self, token): """Validate and decode a JWT token, return the payload.""" From 89dd810f80a9fb2c48df4969ae9c682aadfcbf37 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= Date: Sun, 25 Jul 2021 17:17:16 +0200 Subject: [PATCH 08/43] auth_jwt: mock instead of committing in tests --- auth_jwt/tests/test_auth_jwt.py | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/auth_jwt/tests/test_auth_jwt.py b/auth_jwt/tests/test_auth_jwt.py index c38f522daa..7901f23c95 100644 --- a/auth_jwt/tests/test_auth_jwt.py +++ b/auth_jwt/tests/test_auth_jwt.py @@ -75,13 +75,24 @@ def _commit_validator(self, name, audience="me", partner_id_required=False): validator = self._create_validator( name=name, audience=audience, partner_id_required=partner_id_required ) - # commit because IrHttp._auth_method_jwt will look for validator in another tx - self.env.cr.commit() # pylint: disable=invalid-commit + + def _mocked_get_validator_by_name(self, validator_name): + if validator_name == name: + return validator + return self.env["auth.jwt.validator"]._get_validator_by_name.origin( + self, validator_name + ) + try: + # Patch _get_validator_by_name because IrHttp._auth_method_jwt + # will look for the validator in another transaction, + # where the validator we created above would not be visible. + self.env["auth.jwt.validator"]._patch_method( + "_get_validator_by_name", _mocked_get_validator_by_name + ) yield validator finally: - validator.unlink() - self.env.cr.commit() # pylint: disable=invalid-commit + self.env["auth.jwt.validator"]._revert_method("_get_validator_by_name") def test_missing_authorization_header(self): with self._mock_request(authorization=None): From 16bc96b3db92e44e833079891361477d57af18df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= Date: Mon, 26 Jul 2021 10:48:46 +0200 Subject: [PATCH 09/43] auth_jwt: more precise precondition check --- auth_jwt/models/ir_http.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/auth_jwt/models/ir_http.py b/auth_jwt/models/ir_http.py index 5100fdfdac..5d0a02e027 100644 --- a/auth_jwt/models/ir_http.py +++ b/auth_jwt/models/ir_http.py @@ -38,7 +38,10 @@ def _authenticate(cls, auth_method="user"): 'A route with auth="jwt" must not be used within a user session.' ) raise UnauthorizedSessionMismatch() - if request.uid: + # Odoo calls _authenticate more than once (in v14? why?), so + # on the second call we have a request uid and that is not an error + # because _authenticate will not call _auth_method_jwt a second time. + if request.uid and not hasattr(request, "jwt_payload"): _logger.error( 'A route with auth="jwt" should not have a request.uid here.' ) From 030164bd56513b69285161821227af5cbf1964c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= Date: Mon, 26 Jul 2021 13:09:37 +0200 Subject: [PATCH 10/43] Rename auth_jwt_test to auth_jwt_demo --- auth_jwt/README.rst | 2 +- auth_jwt/readme/USAGE.rst | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/auth_jwt/README.rst b/auth_jwt/README.rst index cf7ec02b57..f84ef84b14 100644 --- a/auth_jwt/README.rst +++ b/auth_jwt/README.rst @@ -51,7 +51,7 @@ To use it, you must: you want to protect where ``{validator-name}`` corresponds to the name attribute of the JWT validator record. -The ``auth_jwt_test`` module provides examples. +The ``auth_jwt_demo`` module provides examples. The JWT validator can be configured with the following properties: diff --git a/auth_jwt/readme/USAGE.rst b/auth_jwt/readme/USAGE.rst index 03390e3cf0..40b52af6ac 100644 --- a/auth_jwt/readme/USAGE.rst +++ b/auth_jwt/readme/USAGE.rst @@ -9,7 +9,7 @@ To use it, you must: you want to protect where ``{validator-name}`` corresponds to the name attribute of the JWT validator record. -The ``auth_jwt_test`` module provides examples. +The ``auth_jwt_demo`` module provides examples. The JWT validator can be configured with the following properties: From e6c93e2aae0e6caaddf1a30cfba5810c56371437 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= Date: Sun, 27 Jun 2021 17:04:28 +0200 Subject: [PATCH 11/43] [MIG] auth_jwt --- auth_jwt/__manifest__.py | 2 +- auth_jwt/models/ir_http.py | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/auth_jwt/__manifest__.py b/auth_jwt/__manifest__.py index af3a772047..bd7adbd946 100644 --- a/auth_jwt/__manifest__.py +++ b/auth_jwt/__manifest__.py @@ -5,7 +5,7 @@ "name": "Auth JWT", "summary": """ JWT bearer token authentication.""", - "version": "13.0.1.0.0", + "version": "14.0.1.0.0", "license": "AGPL-3", "author": "ACSONE SA/NV,Odoo Community Association (OCA)", "maintainers": ["sbidoul"], diff --git a/auth_jwt/models/ir_http.py b/auth_jwt/models/ir_http.py index 5d0a02e027..2b555d38c6 100644 --- a/auth_jwt/models/ir_http.py +++ b/auth_jwt/models/ir_http.py @@ -24,7 +24,7 @@ class IrHttpJwt(models.AbstractModel): _inherit = "ir.http" @classmethod - def _authenticate(cls, auth_method="user"): + def _authenticate(cls, endpoint): """Protect the _authenticate method. This is to ensure that the _authenticate method is called @@ -32,6 +32,7 @@ def _authenticate(cls, auth_method="user"): When migrating, review this method carefully by reading the original _authenticate method and make sure the conditions have not changed. """ + auth_method = endpoint.routing["auth"] if auth_method == "jwt" or auth_method.startswith("jwt_"): if request.session.uid: _logger.warning( @@ -46,7 +47,7 @@ def _authenticate(cls, auth_method="user"): 'A route with auth="jwt" should not have a request.uid here.' ) raise UnauthorizedSessionMismatch() - return super()._authenticate(auth_method) + return super()._authenticate(endpoint) @classmethod def _auth_method_jwt(cls, validator_name=None): From 2d167fdc0cb9778969e9673916cea21ec2c0e864 Mon Sep 17 00:00:00 2001 From: oca-travis Date: Wed, 28 Jul 2021 17:59:00 +0000 Subject: [PATCH 12/43] [UPD] Update auth_jwt.pot --- auth_jwt/i18n/auth_jwt.pot | 255 +++++++++++++++++++++++++++++++++++++ 1 file changed, 255 insertions(+) create mode 100644 auth_jwt/i18n/auth_jwt.pot diff --git a/auth_jwt/i18n/auth_jwt.pot b/auth_jwt/i18n/auth_jwt.pot new file mode 100644 index 0000000000..5d22a8cac2 --- /dev/null +++ b/auth_jwt/i18n/auth_jwt.pot @@ -0,0 +1,255 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * auth_jwt +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 14.0\n" +"Report-Msgid-Bugs-To: \n" +"Last-Translator: \n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: auth_jwt +#: model_terms:ir.ui.view,arch_db:auth_jwt.view_auth_jwt_validator_form +msgid "Algorithm" +msgstr "" + +#. module: auth_jwt +#: model:ir.model.fields,field_description:auth_jwt.field_auth_jwt_validator__audience +msgid "Audience" +msgstr "" + +#. module: auth_jwt +#: model:ir.model.fields,help:auth_jwt.field_auth_jwt_validator__audience +msgid "Comma separated list of audiences, to validate aud." +msgstr "" + +#. module: auth_jwt +#: model:ir.model.fields,field_description:auth_jwt.field_auth_jwt_validator__create_uid +msgid "Created by" +msgstr "" + +#. module: auth_jwt +#: model:ir.model.fields,field_description:auth_jwt.field_auth_jwt_validator__create_date +msgid "Created on" +msgstr "" + +#. module: auth_jwt +#: model:ir.model.fields,field_description:auth_jwt.field_auth_jwt_validator__display_name +#: model:ir.model.fields,field_description:auth_jwt.field_ir_http__display_name +msgid "Display Name" +msgstr "" + +#. module: auth_jwt +#: model:ir.model.fields.selection,name:auth_jwt.selection__auth_jwt_validator__public_key_algorithm__es256 +msgid "ES256 - ECDSA using SHA-256" +msgstr "" + +#. module: auth_jwt +#: model:ir.model.fields.selection,name:auth_jwt.selection__auth_jwt_validator__public_key_algorithm__es256k +msgid "ES256K - ECDSA with secp256k1 curve using SHA-256" +msgstr "" + +#. module: auth_jwt +#: model:ir.model.fields.selection,name:auth_jwt.selection__auth_jwt_validator__public_key_algorithm__es384 +msgid "ES384 - ECDSA using SHA-384" +msgstr "" + +#. module: auth_jwt +#: model:ir.model.fields.selection,name:auth_jwt.selection__auth_jwt_validator__public_key_algorithm__es512 +msgid "ES512 - ECDSA using SHA-512" +msgstr "" + +#. module: auth_jwt +#: model:ir.model.fields.selection,name:auth_jwt.selection__auth_jwt_validator__partner_id_strategy__email +msgid "From email claim" +msgstr "" + +#. module: auth_jwt +#: model:ir.model.fields.selection,name:auth_jwt.selection__auth_jwt_validator__secret_algorithm__hs256 +msgid "HS256 - HMAC using SHA-256 hash algorithm" +msgstr "" + +#. module: auth_jwt +#: model:ir.model.fields.selection,name:auth_jwt.selection__auth_jwt_validator__secret_algorithm__hs384 +msgid "HS384 - HMAC using SHA-384 hash algorithm" +msgstr "" + +#. module: auth_jwt +#: model:ir.model.fields.selection,name:auth_jwt.selection__auth_jwt_validator__secret_algorithm__hs512 +msgid "HS512 - HMAC using SHA-512 hash algorithm" +msgstr "" + +#. module: auth_jwt +#: model:ir.model,name:auth_jwt.model_ir_http +msgid "HTTP Routing" +msgstr "" + +#. module: auth_jwt +#: model:ir.model.fields,field_description:auth_jwt.field_auth_jwt_validator__id +#: model:ir.model.fields,field_description:auth_jwt.field_ir_http__id +msgid "ID" +msgstr "" + +#. module: auth_jwt +#: model:ir.model.fields,field_description:auth_jwt.field_auth_jwt_validator__issuer +msgid "Issuer" +msgstr "" + +#. module: auth_jwt +#: model_terms:ir.ui.view,arch_db:auth_jwt.view_auth_jwt_validator_form +msgid "JWK URI" +msgstr "" + +#. module: auth_jwt +#: model:ir.model,name:auth_jwt.model_auth_jwt_validator +msgid "JWT Validator Configuration" +msgstr "" + +#. module: auth_jwt +#: model:ir.actions.act_window,name:auth_jwt.action_auth_jwt_validator +#: model:ir.ui.menu,name:auth_jwt.menu_auth_jwt_validator +msgid "JWT Validators" +msgstr "" + +#. module: auth_jwt +#: model:ir.model.constraint,message:auth_jwt.constraint_auth_jwt_validator_name_uniq +msgid "JWT validator names must be unique !" +msgstr "" + +#. module: auth_jwt +#: model_terms:ir.ui.view,arch_db:auth_jwt.view_auth_jwt_validator_form +msgid "Key" +msgstr "" + +#. module: auth_jwt +#: model:ir.model.fields,field_description:auth_jwt.field_auth_jwt_validator____last_update +#: model:ir.model.fields,field_description:auth_jwt.field_ir_http____last_update +msgid "Last Modified on" +msgstr "" + +#. module: auth_jwt +#: model:ir.model.fields,field_description:auth_jwt.field_auth_jwt_validator__write_uid +msgid "Last Updated by" +msgstr "" + +#. module: auth_jwt +#: model:ir.model.fields,field_description:auth_jwt.field_auth_jwt_validator__write_date +msgid "Last Updated on" +msgstr "" + +#. module: auth_jwt +#: model:ir.model.fields,field_description:auth_jwt.field_auth_jwt_validator__name +msgid "Name" +msgstr "" + +#. module: auth_jwt +#: code:addons/auth_jwt/models/auth_jwt_validator.py:0 +#, python-format +msgid "Name %r is not a valid python identifier." +msgstr "" + +#. module: auth_jwt +#: model:ir.model.fields.selection,name:auth_jwt.selection__auth_jwt_validator__public_key_algorithm__ps256 +msgid "PS256 - RSASSA-PSS using SHA-256 and MGF1 padding with SHA-256" +msgstr "" + +#. module: auth_jwt +#: model:ir.model.fields.selection,name:auth_jwt.selection__auth_jwt_validator__public_key_algorithm__ps384 +msgid "PS384 - RSASSA-PSS using SHA-384 and MGF1 padding with SHA-384" +msgstr "" + +#. module: auth_jwt +#: model:ir.model.fields.selection,name:auth_jwt.selection__auth_jwt_validator__public_key_algorithm__ps512 +msgid "PS512 - RSASSA-PSS using SHA-512 and MGF1 padding with SHA-512" +msgstr "" + +#. module: auth_jwt +#: model:ir.model.fields,field_description:auth_jwt.field_auth_jwt_validator__partner_id_required +msgid "Partner Id Required" +msgstr "" + +#. module: auth_jwt +#: model:ir.model.fields,field_description:auth_jwt.field_auth_jwt_validator__partner_id_strategy +msgid "Partner Id Strategy" +msgstr "" + +#. module: auth_jwt +#: model:ir.model.fields,field_description:auth_jwt.field_auth_jwt_validator__public_key_algorithm +msgid "Public Key Algorithm" +msgstr "" + +#. module: auth_jwt +#: model:ir.model.fields,field_description:auth_jwt.field_auth_jwt_validator__public_key_jwk_uri +msgid "Public Key Jwk Uri" +msgstr "" + +#. module: auth_jwt +#: model:ir.model.fields.selection,name:auth_jwt.selection__auth_jwt_validator__signature_type__public_key +msgid "Public key" +msgstr "" + +#. module: auth_jwt +#: model:ir.model.fields.selection,name:auth_jwt.selection__auth_jwt_validator__public_key_algorithm__rs256 +msgid "RS256 - RSASSA-PKCS1-v1_5 using SHA-256" +msgstr "" + +#. module: auth_jwt +#: model:ir.model.fields.selection,name:auth_jwt.selection__auth_jwt_validator__public_key_algorithm__rs384 +msgid "RS384 - RSASSA-PKCS1-v1_5 using SHA-384" +msgstr "" + +#. module: auth_jwt +#: model:ir.model.fields.selection,name:auth_jwt.selection__auth_jwt_validator__public_key_algorithm__rs512 +msgid "RS512 - RSASSA-PKCS1-v1_5 using SHA-512" +msgstr "" + +#. module: auth_jwt +#: model:ir.model.fields.selection,name:auth_jwt.selection__auth_jwt_validator__signature_type__secret +msgid "Secret" +msgstr "" + +#. module: auth_jwt +#: model:ir.model.fields,field_description:auth_jwt.field_auth_jwt_validator__secret_algorithm +msgid "Secret Algorithm" +msgstr "" + +#. module: auth_jwt +#: model:ir.model.fields,field_description:auth_jwt.field_auth_jwt_validator__secret_key +msgid "Secret Key" +msgstr "" + +#. module: auth_jwt +#: model:ir.model.fields,field_description:auth_jwt.field_auth_jwt_validator__signature_type +msgid "Signature Type" +msgstr "" + +#. module: auth_jwt +#: model:ir.model.fields.selection,name:auth_jwt.selection__auth_jwt_validator__user_id_strategy__static +msgid "Static" +msgstr "" + +#. module: auth_jwt +#: model:ir.model.fields,field_description:auth_jwt.field_auth_jwt_validator__static_user_id +msgid "Static User" +msgstr "" + +#. module: auth_jwt +#: model:ir.model.fields,help:auth_jwt.field_auth_jwt_validator__issuer +msgid "To validate iss." +msgstr "" + +#. module: auth_jwt +#: model:ir.model.fields,field_description:auth_jwt.field_auth_jwt_validator__user_id_strategy +msgid "User Id Strategy" +msgstr "" + +#. module: auth_jwt +#: model_terms:ir.ui.view,arch_db:auth_jwt.view_auth_jwt_validator_form +#: model_terms:ir.ui.view,arch_db:auth_jwt.view_auth_jwt_validator_tree +msgid "arch" +msgstr "" From 6bc609b27d730153a0afb699ff306b5ecd02760d Mon Sep 17 00:00:00 2001 From: OCA-git-bot Date: Wed, 28 Jul 2021 19:49:00 +0000 Subject: [PATCH 13/43] [UPD] README.rst --- auth_jwt/README.rst | 26 +- auth_jwt/static/description/index.html | 470 +++++++++++++++++++++++++ 2 files changed, 487 insertions(+), 9 deletions(-) create mode 100644 auth_jwt/static/description/index.html diff --git a/auth_jwt/README.rst b/auth_jwt/README.rst index f84ef84b14..be0c8385b3 100644 --- a/auth_jwt/README.rst +++ b/auth_jwt/README.rst @@ -14,13 +14,13 @@ Auth JWT :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html :alt: License: AGPL-3 .. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fserver--auth-lightgray.png?logo=github - :target: https://github.com/OCA/server-auth/tree/13.0/auth_jwt + :target: https://github.com/OCA/server-auth/tree/14.0/auth_jwt :alt: OCA/server-auth .. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png - :target: https://translation.odoo-community.org/projects/server-auth-13-0/server-auth-13-0-auth_jwt + :target: https://translation.odoo-community.org/projects/server-auth-14-0/server-auth-14-0-auth_jwt :alt: Translate me on Weblate .. |badge5| image:: https://img.shields.io/badge/runbot-Try%20me-875A7B.png - :target: https://runbot.odoo-community.org/runbot/251/13.0 + :target: https://runbot.odoo-community.org/runbot/251/14.0 :alt: Try me on Runbot |badge1| |badge2| |badge3| |badge4| |badge5| @@ -57,7 +57,8 @@ The JWT validator can be configured with the following properties: * ``name``: the validator name, to match the ``auth="jwt_{validator-name}"`` route property. -* ``audience``: used to validate the ``aud`` claim. +* ``audience``: a comma-separated list of allowed audiences, used to validate + the ``aud`` claim. * ``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. @@ -71,22 +72,29 @@ If the token is valid, the request executes with the configured user id. By default the user id selection strategy is ``static`` (i.e. the same for all requests) and the selected user is configured on the JWT validator. Additional strategies can be provided by overriding the ``_get_uid()`` method and -extending the ``user_id_strategy`` selection field.. +extending the ``user_id_strategy`` selection field. + +The selected user is *not* stored in the session. It is only available in +``request.uid`` (and thus it is the one used in ``request.env``). To avoid any +confusion and mismatches between the bearer token and the session, this module +rejects requests made with an authenticated user session. Additionally, if a ``partner_id_strategy`` is configured, a partner is searched -and if found, its id is stored in the ``request.partner_id`` attribute. If +and if found, its id is stored in the ``request.jwt_partner_id`` attribute. If ``partner_id_required`` is set, a 401 (Unauthorized) is returned if no partner -was found. Otherwise ``request.partner_id`` is left falsy. Additional +was found. Otherwise ``request.jwt_partner_id`` is left falsy. Additional strategies can be provided by overriding the ``_get_partner_id()`` method and extending the ``partner_id_strategy`` selection field. +The decoded JWT payload is stored in ``request.jwt_payload``. + 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 smashing it by providing a detailed and welcomed -`feedback `_. +`feedback `_. Do not contact contributors directly about support or help with technical issues. @@ -124,6 +132,6 @@ Current `maintainer `__: |maintainer-sbidoul| -This module is part of the `OCA/server-auth `_ project on GitHub. +This module is part of the `OCA/server-auth `_ project on GitHub. You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/auth_jwt/static/description/index.html b/auth_jwt/static/description/index.html new file mode 100644 index 0000000000..07fea2065a --- /dev/null +++ b/auth_jwt/static/description/index.html @@ -0,0 +1,470 @@ + + + + + + +Auth JWT + + + +
+

Auth JWT

+ + +

Beta License: AGPL-3 OCA/server-auth Translate me on Weblate Try me on Runbot

+

JWT bearer token authentication.

+

Table of contents

+ +
+

Installation

+

This module requires the pyjwt library to be installed.

+
+
+

Usage

+

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

+

To use it, you must:

+
    +
  • Create an auth.jwt.validator record to configure how the JWT token will +be validated.
  • +
  • Add an auth="jwt_{validator-name}" attribute to the routes +you want to protect where {validator-name} corresponds to the name +attribute of the JWT validator record.
  • +
+

The auth_jwt_demo module provides examples.

+

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.
  • +
  • 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.

+

If the Authorization HTTP header is missing, malformed, or contains +an invalid token, the request is rejected with a 401 (Unauthorized) code.

+

If the token is valid, the request executes with the configured user id. By +default the user id selection strategy is static (i.e. the same for all +requests) and the selected user is configured on the JWT validator. Additional +strategies can be provided by overriding the _get_uid() method and +extending the user_id_strategy selection field.

+

The selected user is not stored in the session. It is only available in +request.uid (and thus it is the one used in request.env). To avoid any +confusion and mismatches between the bearer token and the session, this module +rejects requests made with an authenticated user session.

+

Additionally, if a partner_id_strategy is configured, a partner is searched +and if found, its id is stored in the request.jwt_partner_id attribute. If +partner_id_required is set, a 401 (Unauthorized) is returned if no partner +was found. Otherwise request.jwt_partner_id is left falsy. Additional +strategies can be provided by overriding the _get_partner_id() method +and extending the partner_id_strategy selection field.

+

The decoded JWT payload is stored in request.jwt_payload.

+
+
+

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 smashing it by providing a detailed and welcomed +feedback.

+

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

+
+
+

Credits

+
+

Authors

+
    +
  • ACSONE SA/NV
  • +
+
+
+

Contributors

+ +
+
+

Maintainers

+

This module is maintained by the OCA.

+Odoo Community Association +

OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use.

+

Current maintainer:

+

sbidoul

+

This module is part of the OCA/server-auth project on GitHub.

+

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

+
+
+
+ + From b430a28c0e4e057cb4076a16b11c0801b5a9d3b7 Mon Sep 17 00:00:00 2001 From: OCA-git-bot Date: Wed, 28 Jul 2021 19:49:01 +0000 Subject: [PATCH 14/43] auth_jwt 14.0.1.0.1 --- auth_jwt/__manifest__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/auth_jwt/__manifest__.py b/auth_jwt/__manifest__.py index bd7adbd946..6888130721 100644 --- a/auth_jwt/__manifest__.py +++ b/auth_jwt/__manifest__.py @@ -5,7 +5,7 @@ "name": "Auth JWT", "summary": """ JWT bearer token authentication.""", - "version": "14.0.1.0.0", + "version": "14.0.1.0.1", "license": "AGPL-3", "author": "ACSONE SA/NV,Odoo Community Association (OCA)", "maintainers": ["sbidoul"], From 3c074d6e507e97fffcebe42bce38bb33669c156f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= Date: Tue, 5 Oct 2021 09:44:53 +0200 Subject: [PATCH 15/43] [IMP] auth_jwt: add public_or_jwt auth method This method is useful for public endpoints that need to work for anonymous user, but can be enhanced when an authenticated user is know. A typical use case is a "add to cart" enpoint that can work for anonymous users, but can be enhanced by binding the cart to a known customer when the authenticated user is known. --- auth_jwt/models/auth_jwt_validator.py | 8 +++++ auth_jwt/models/ir_http.py | 14 +++++++-- auth_jwt/readme/USAGE.rst | 14 +++++++-- auth_jwt/tests/test_auth_jwt.py | 44 ++++++++++++++++++++++++++- 4 files changed, 74 insertions(+), 6 deletions(-) diff --git a/auth_jwt/models/auth_jwt_validator.py b/auth_jwt/models/auth_jwt_validator.py index e5813f193d..8fd37bf219 100644 --- a/auth_jwt/models/auth_jwt_validator.py +++ b/auth_jwt/models/auth_jwt_validator.py @@ -180,12 +180,20 @@ def _register_auth_method(self): f"_auth_method_jwt_{rec.name}", partial(IrHttp.__class__._auth_method_jwt, validator_name=rec.name), ) + setattr( + IrHttp.__class__, + f"_auth_method_public_or_jwt_{rec.name}", + partial( + IrHttp.__class__._auth_method_public_or_jwt, validator_name=rec.name + ), + ) def _unregister_auth_method(self): IrHttp = self.env["ir.http"] for rec in self: try: delattr(IrHttp.__class__, f"_auth_method_jwt_{rec.name}") + delattr(IrHttp.__class__, f"_auth_method_public_or_jwt_{rec.name}") except AttributeError: pass diff --git a/auth_jwt/models/ir_http.py b/auth_jwt/models/ir_http.py index 2b555d38c6..aad11b9854 100644 --- a/auth_jwt/models/ir_http.py +++ b/auth_jwt/models/ir_http.py @@ -33,7 +33,11 @@ def _authenticate(cls, endpoint): _authenticate method and make sure the conditions have not changed. """ auth_method = endpoint.routing["auth"] - if auth_method == "jwt" or auth_method.startswith("jwt_"): + if ( + auth_method in ("jwt", "public_or_jwt") + or auth_method.startswith("jwt_") + or auth_method.startswith("public_or_jwt_") + ): if request.session.uid: _logger.warning( 'A route with auth="jwt" must not be used within a user session.' @@ -44,7 +48,7 @@ def _authenticate(cls, endpoint): # because _authenticate will not call _auth_method_jwt a second time. if request.uid and not hasattr(request, "jwt_payload"): _logger.error( - 'A route with auth="jwt" should not have a request.uid here.' + "A route with auth='jwt' should not have a request.uid here." ) raise UnauthorizedSessionMismatch() return super()._authenticate(endpoint) @@ -69,6 +73,12 @@ def _auth_method_jwt(cls, validator_name=None): request.jwt_payload = payload request.jwt_partner_id = partner_id + @classmethod + def _auth_method_public_or_jwt(cls, validator_name=None): + if "HTTP_AUTHORIZATION" not in request.httprequest.environ: + return cls._auth_method_public() + return cls._auth_method_jwt(validator_name) + @classmethod def _get_bearer_token(cls): # https://tools.ietf.org/html/rfc2617#section-3.2.2 diff --git a/auth_jwt/readme/USAGE.rst b/auth_jwt/readme/USAGE.rst index 40b52af6ac..be48400b6e 100644 --- a/auth_jwt/readme/USAGE.rst +++ b/auth_jwt/readme/USAGE.rst @@ -5,9 +5,9 @@ To use it, you must: * Create an ``auth.jwt.validator`` record to configure how the JWT token will be validated. -* Add an ``auth="jwt_{validator-name}"`` attribute to the routes - you want to protect where ``{validator-name}`` corresponds to the name - attribute of the JWT validator record. +* Add an ``auth="jwt_{validator-name}"`` or ``auth="public_or_jwt_{validator-name}"`` + attribute to the routes you want to protect where ``{validator-name}`` corresponds to + the name attribute of the JWT validator record. The ``auth_jwt_demo`` module provides examples. @@ -45,3 +45,11 @@ strategies can be provided by overriding the ``_get_partner_id()`` method and extending the ``partner_id_strategy`` selection field. The decoded JWT payload is stored in ``request.jwt_payload``. + +The ``public_auth_jwt`` method delegates authentication to the standard Odoo ``public`` +method when the Authorization header is not set. If it is set, the regular JWT +authentication is performed as described above. This method is useful for public +endpoints that need to work for anonymous users, but can be enhanced when an +authenticated user is know. A typical use case is a "add to cart" endpoint that can work +for anonymous users, but can be enhanced by binding the cart to a known customer when +the authenticated user is known. diff --git a/auth_jwt/tests/test_auth_jwt.py b/auth_jwt/tests/test_auth_jwt.py index 7901f23c95..097312072b 100644 --- a/auth_jwt/tests/test_auth_jwt.py +++ b/auth_jwt/tests/test_auth_jwt.py @@ -26,13 +26,21 @@ class TestAuthMethod(TransactionCase): @contextlib.contextmanager def _mock_request(self, authorization): + environ = {} + if authorization: + environ["HTTP_AUTHORIZATION"] = authorization request = Mock( context={}, db=self.env.cr.dbname, uid=None, - httprequest=Mock(environ={"HTTP_AUTHORIZATION": authorization}), + httprequest=Mock(environ=environ), session=DotDict(), + env=self.env, ) + # These attributes are added upon successful auth, so make sure + # calling hasattr on the mock when they are not yet set returns False. + del request.jwt_payload + del request.jwt_partner_id with contextlib.ExitStack() as s: odoo.http._request_stack.push(request) @@ -238,24 +246,58 @@ def test_nbf(self): def test_auth_method_registration_on_create(self): IrHttp = self.env["ir.http"] self.assertFalse(hasattr(IrHttp.__class__, "_auth_method_jwt_validator1")) + self.assertFalse( + hasattr(IrHttp.__class__, "_auth_method_public_or_jwt_validator1") + ) self._create_validator("validator1") self.assertTrue(hasattr(IrHttp.__class__, "_auth_method_jwt_validator1")) + self.assertTrue( + hasattr(IrHttp.__class__, "_auth_method_public_or_jwt_validator1") + ) def test_auth_method_unregistration_on_unlink(self): IrHttp = self.env["ir.http"] validator = self._create_validator("validator1") self.assertTrue(hasattr(IrHttp.__class__, "_auth_method_jwt_validator1")) + self.assertTrue( + hasattr(IrHttp.__class__, "_auth_method_public_or_jwt_validator1") + ) validator.unlink() self.assertFalse(hasattr(IrHttp.__class__, "_auth_method_jwt_validator1")) + self.assertFalse( + hasattr(IrHttp.__class__, "_auth_method_public_or_jwt_validator1") + ) def test_auth_method_registration_on_rename(self): IrHttp = self.env["ir.http"] validator = self._create_validator("validator1") self.assertTrue(hasattr(IrHttp.__class__, "_auth_method_jwt_validator1")) + self.assertTrue( + hasattr(IrHttp.__class__, "_auth_method_public_or_jwt_validator1") + ) validator.name = "validator2" self.assertFalse(hasattr(IrHttp.__class__, "_auth_method_jwt_validator1")) + self.assertFalse( + hasattr(IrHttp.__class__, "_auth_method_public_or_jwt_validator1") + ) self.assertTrue(hasattr(IrHttp.__class__, "_auth_method_jwt_validator2")) + self.assertTrue( + hasattr(IrHttp.__class__, "_auth_method_public_or_jwt_validator2") + ) def test_name_check(self): with self.assertRaises(ValidationError): self._create_validator(name="not an identifier") + + def test_public_or_jwt_no_token(self): + with self._mock_request(authorization=None) as request: + self.env["ir.http"]._auth_method_public_or_jwt() + assert request.uid == self.env.ref("base.public_user").id + assert not hasattr(request, "jwt_payload") + + def test_public_or_jwt_valid_token(self): + with self._commit_validator("validator"): + authorization = "Bearer " + self._create_token() + with self._mock_request(authorization=authorization) as request: + self.env["ir.http"]._auth_method_public_or_jwt_validator() + assert request.jwt_payload["aud"] == "me" From 51365a8db528bf075f545bdabf321675c84abc0b Mon Sep 17 00:00:00 2001 From: OCA-git-bot Date: Wed, 6 Oct 2021 11:53:45 +0000 Subject: [PATCH 16/43] [UPD] README.rst --- auth_jwt/README.rst | 14 +++++++++++--- auth_jwt/static/description/index.html | 13 ++++++++++--- 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/auth_jwt/README.rst b/auth_jwt/README.rst index be0c8385b3..871af3c4a2 100644 --- a/auth_jwt/README.rst +++ b/auth_jwt/README.rst @@ -47,9 +47,9 @@ To use it, you must: * Create an ``auth.jwt.validator`` record to configure how the JWT token will be validated. -* Add an ``auth="jwt_{validator-name}"`` attribute to the routes - you want to protect where ``{validator-name}`` corresponds to the name - attribute of the JWT validator record. +* Add an ``auth="jwt_{validator-name}"`` or ``auth="public_or_jwt_{validator-name}"`` + attribute to the routes you want to protect where ``{validator-name}`` corresponds to + the name attribute of the JWT validator record. The ``auth_jwt_demo`` module provides examples. @@ -88,6 +88,14 @@ and extending the ``partner_id_strategy`` selection field. The decoded JWT payload is stored in ``request.jwt_payload``. +The ``public_auth_jwt`` method delegates authentication to the standard Odoo ``public`` +method when the Authorization header is not set. If it is set, the regular JWT +authentication is performed as described above. This method is useful for public +endpoints that need to work for anonymous users, but can be enhanced when an +authenticated user is know. A typical use case is a "add to cart" endpoint that can work +for anonymous users, but can be enhanced by binding the cart to a known customer when +the authenticated user is known. + Bug Tracker =========== diff --git a/auth_jwt/static/description/index.html b/auth_jwt/static/description/index.html index 07fea2065a..d5564eb61b 100644 --- a/auth_jwt/static/description/index.html +++ b/auth_jwt/static/description/index.html @@ -395,9 +395,9 @@

Usage

  • Create an auth.jwt.validator record to configure how the JWT token will be validated.
  • -
  • Add an auth="jwt_{validator-name}" attribute to the routes -you want to protect where {validator-name} corresponds to the name -attribute of the JWT validator record.
  • +
  • Add an auth="jwt_{validator-name}" or auth="public_or_jwt_{validator-name}" +attribute to the routes you want to protect where {validator-name} corresponds to +the name attribute of the JWT validator record.

The auth_jwt_demo module provides examples.

The JWT validator can be configured with the following properties:

@@ -429,6 +429,13 @@

Usage

strategies can be provided by overriding the _get_partner_id() method and extending the partner_id_strategy selection field.

The decoded JWT payload is stored in request.jwt_payload.

+

The public_auth_jwt method delegates authentication to the standard Odoo public +method when the Authorization header is not set. If it is set, the regular JWT +authentication is performed as described above. This method is useful for public +endpoints that need to work for anonymous users, but can be enhanced when an +authenticated user is know. A typical use case is a “add to cart” endpoint that can work +for anonymous users, but can be enhanced by binding the cart to a known customer when +the authenticated user is known.

Bug Tracker

From 8dd53c6b01a38b15c2d004d4e19a71a7960b97db Mon Sep 17 00:00:00 2001 From: OCA-git-bot Date: Wed, 6 Oct 2021 11:53:46 +0000 Subject: [PATCH 17/43] auth_jwt 14.0.1.1.0 --- auth_jwt/__manifest__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/auth_jwt/__manifest__.py b/auth_jwt/__manifest__.py index 6888130721..7e0209f0c0 100644 --- a/auth_jwt/__manifest__.py +++ b/auth_jwt/__manifest__.py @@ -5,7 +5,7 @@ "name": "Auth JWT", "summary": """ JWT bearer token authentication.""", - "version": "14.0.1.0.1", + "version": "14.0.1.1.0", "license": "AGPL-3", "author": "ACSONE SA/NV,Odoo Community Association (OCA)", "maintainers": ["sbidoul"], From e8c12c3993bb5966d95d57bd757148aad3e60330 Mon Sep 17 00:00:00 2001 From: Maksym Yankin Date: Wed, 29 Dec 2021 10:37:59 +0200 Subject: [PATCH 18/43] auth_jwt: Relicence under LGPL --- auth_jwt/README.rst | 6 +++--- auth_jwt/__manifest__.py | 4 ++-- auth_jwt/exceptions.py | 2 +- auth_jwt/models/auth_jwt_validator.py | 2 +- auth_jwt/models/ir_http.py | 2 +- auth_jwt/static/description/index.html | 2 +- auth_jwt/tests/test_auth_jwt.py | 2 +- 7 files changed, 10 insertions(+), 10 deletions(-) diff --git a/auth_jwt/README.rst b/auth_jwt/README.rst index 871af3c4a2..002a5092bf 100644 --- a/auth_jwt/README.rst +++ b/auth_jwt/README.rst @@ -10,9 +10,9 @@ 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/licence-AGPL--3-blue.png - :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html - :alt: License: AGPL-3 +.. |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 :target: https://github.com/OCA/server-auth/tree/14.0/auth_jwt :alt: OCA/server-auth diff --git a/auth_jwt/__manifest__.py b/auth_jwt/__manifest__.py index 7e0209f0c0..b6daf34c17 100644 --- a/auth_jwt/__manifest__.py +++ b/auth_jwt/__manifest__.py @@ -1,12 +1,12 @@ # Copyright 2021 ACSONE SA/NV -# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). { "name": "Auth JWT", "summary": """ JWT bearer token authentication.""", "version": "14.0.1.1.0", - "license": "AGPL-3", + "license": "LGPL-3", "author": "ACSONE SA/NV,Odoo Community Association (OCA)", "maintainers": ["sbidoul"], "website": "https://github.com/OCA/server-auth", diff --git a/auth_jwt/exceptions.py b/auth_jwt/exceptions.py index dbebaff04d..d1b5a80d0d 100644 --- a/auth_jwt/exceptions.py +++ b/auth_jwt/exceptions.py @@ -1,5 +1,5 @@ # Copyright 2021 ACSONE SA/NV -# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl) from werkzeug.exceptions import InternalServerError, Unauthorized diff --git a/auth_jwt/models/auth_jwt_validator.py b/auth_jwt/models/auth_jwt_validator.py index 8fd37bf219..5d841888be 100644 --- a/auth_jwt/models/auth_jwt_validator.py +++ b/auth_jwt/models/auth_jwt_validator.py @@ -1,5 +1,5 @@ # Copyright 2021 ACSONE SA/NV -# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). import logging from functools import partial diff --git a/auth_jwt/models/ir_http.py b/auth_jwt/models/ir_http.py index aad11b9854..f90b5824c2 100644 --- a/auth_jwt/models/ir_http.py +++ b/auth_jwt/models/ir_http.py @@ -1,5 +1,5 @@ # Copyright 2021 ACSONE SA/NV -# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). import logging import re diff --git a/auth_jwt/static/description/index.html b/auth_jwt/static/description/index.html index d5564eb61b..bd621ae44d 100644 --- a/auth_jwt/static/description/index.html +++ b/auth_jwt/static/description/index.html @@ -367,7 +367,7 @@

Auth JWT

!! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! --> -

Beta License: AGPL-3 OCA/server-auth Translate me on Weblate Try me on Runbot

+

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

JWT bearer token authentication.

Table of contents

diff --git a/auth_jwt/tests/test_auth_jwt.py b/auth_jwt/tests/test_auth_jwt.py index 097312072b..937778c521 100644 --- a/auth_jwt/tests/test_auth_jwt.py +++ b/auth_jwt/tests/test_auth_jwt.py @@ -1,5 +1,5 @@ # Copyright 2021 ACSONE SA/NV -# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). import contextlib import time From d00e0a7528eb186033015a79694dedc409962f86 Mon Sep 17 00:00:00 2001 From: OCA-git-bot Date: Wed, 29 Dec 2021 11:07:22 +0000 Subject: [PATCH 19/43] auth_jwt 14.0.1.2.0 --- auth_jwt/__manifest__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/auth_jwt/__manifest__.py b/auth_jwt/__manifest__.py index b6daf34c17..2e9ae85bd5 100644 --- a/auth_jwt/__manifest__.py +++ b/auth_jwt/__manifest__.py @@ -5,7 +5,7 @@ "name": "Auth JWT", "summary": """ JWT bearer token authentication.""", - "version": "14.0.1.1.0", + "version": "14.0.1.2.0", "license": "LGPL-3", "author": "ACSONE SA/NV,Odoo Community Association (OCA)", "maintainers": ["sbidoul"], From 6b17cc674792a1465662eb8f3fd0e3d2c4a56478 Mon Sep 17 00:00:00 2001 From: Florian Mounier Date: Thu, 17 Feb 2022 15:44:55 +0100 Subject: [PATCH 20/43] [IMP] auth_jwt: Add validator.next_validator_id to allow validator chaining --- auth_jwt/exceptions.py | 14 ++ auth_jwt/models/auth_jwt_validator.py | 22 ++ auth_jwt/models/ir_http.py | 36 ++- auth_jwt/tests/test_auth_jwt.py | 259 ++++++++++++++------ auth_jwt/views/auth_jwt_validator_views.xml | 2 + 5 files changed, 246 insertions(+), 87 deletions(-) diff --git a/auth_jwt/exceptions.py b/auth_jwt/exceptions.py index d1b5a80d0d..bc81b3ae12 100644 --- a/auth_jwt/exceptions.py +++ b/auth_jwt/exceptions.py @@ -30,3 +30,17 @@ class UnauthorizedInvalidToken(Unauthorized): class UnauthorizedPartnerNotFound(Unauthorized): pass + + +class CompositeJwtError(Unauthorized): + """Indicate that multiple errors occurred during JWT chain validation.""" + + def __init__(self, errors): + self.errors = errors + super().__init__( + "Multiple errors occurred during JWT chain validation:\n" + + "\n".join( + "{}: {}".format(validator_name, error) + for validator_name, error in self.errors.items() + ) + ) diff --git a/auth_jwt/models/auth_jwt_validator.py b/auth_jwt/models/auth_jwt_validator.py index 5d841888be..3d3c9b4d7c 100644 --- a/auth_jwt/models/auth_jwt_validator.py +++ b/auth_jwt/models/auth_jwt_validator.py @@ -67,6 +67,12 @@ class AuthJwtValidator(models.Model): partner_id_strategy = fields.Selection([("email", "From email claim")]) partner_id_required = fields.Boolean() + next_validator_id = fields.Many2one( + "auth.jwt.validator", + domain="[('id', '!=', id)]", + help="Next validator to try if this one fails", + ) + _sql_constraints = [ ("name_uniq", "unique(name)", "JWT validator names must be unique !"), ] @@ -79,6 +85,22 @@ def _check_name(self): _("Name %r is not a valid python identifier.") % (rec.name,) ) + @api.constrains("next_validator_id") + def _check_next_validator_id(self): + # Prevent circular references + for rec in self: + validator = rec + chain = [validator.name] + while validator: + validator = validator.next_validator_id + chain.append(validator.name) + if rec == validator: + raise ValidationError( + _("Validators mustn't make a closed chain: {}.").format( + " -> ".join(chain) + ) + ) + @api.model def _get_validator_by_name_domain(self, validator_name): if validator_name: diff --git a/auth_jwt/models/ir_http.py b/auth_jwt/models/ir_http.py index f90b5824c2..89bfec6cde 100644 --- a/auth_jwt/models/ir_http.py +++ b/auth_jwt/models/ir_http.py @@ -4,10 +4,11 @@ import logging import re -from odoo import SUPERUSER_ID, api, models, registry as registry_get +from odoo import SUPERUSER_ID, api, models from odoo.http import request from ..exceptions import ( + CompositeJwtError, UnauthorizedMalformedAuthorizationHeader, UnauthorizedMissingAuthorizationHeader, UnauthorizedSessionMismatch, @@ -55,20 +56,33 @@ def _authenticate(cls, endpoint): @classmethod def _auth_method_jwt(cls, validator_name=None): - assert request.db assert not request.uid assert not request.session.uid token = cls._get_bearer_token() assert token - registry = registry_get(request.db) - with registry.cursor() as cr: - env = api.Environment(cr, SUPERUSER_ID, {}) - validator = env["auth.jwt.validator"]._get_validator_by_name(validator_name) - assert len(validator) == 1 - payload = validator._decode(token) - uid = validator._get_and_check_uid(payload) - assert uid - partner_id = validator._get_and_check_partner_id(payload) + # # Use request cursor to allow partner creation strategy in validator + env = api.Environment(request.cr, SUPERUSER_ID, {}) + validator = env["auth.jwt.validator"]._get_validator_by_name(validator_name) + assert len(validator) == 1 + + payload = None + exceptions = {} + while validator: + try: + payload = validator._decode(token) + break + except Exception as e: + exceptions[validator.name] = e + validator = validator.next_validator_id + + if not payload: + if len(exceptions) == 1: + raise list(exceptions.values())[0] + raise CompositeJwtError(exceptions) + + uid = validator._get_and_check_uid(payload) + assert uid + partner_id = validator._get_and_check_partner_id(payload) request.uid = uid # this resets request.env request.jwt_payload = payload request.jwt_partner_id = partner_id diff --git a/auth_jwt/tests/test_auth_jwt.py b/auth_jwt/tests/test_auth_jwt.py index 937778c521..c1e00c660c 100644 --- a/auth_jwt/tests/test_auth_jwt.py +++ b/auth_jwt/tests/test_auth_jwt.py @@ -15,6 +15,7 @@ from ..exceptions import ( AmbiguousJwtValidator, + CompositeJwtError, JwtValidatorNotFound, UnauthorizedInvalidToken, UnauthorizedMalformedAuthorizationHeader, @@ -36,6 +37,7 @@ def _mock_request(self, authorization): httprequest=Mock(environ=environ), session=DotDict(), env=self.env, + cr=self.env.cr, ) # These attributes are added upon successful auth, so make sure # calling hasattr on the mock when they are not yet set returns False. @@ -63,45 +65,28 @@ def _create_token( payload["nbf"] = nbf return jwt.encode(payload, key=key, algorithm="HS256") - def _create_validator(self, name, audience="me", partner_id_required=False): + def _create_validator( + self, + name, + audience="me", + issuer="http://the.issuer", + secret_key="thesecret", + partner_id_required=False, + ): return self.env["auth.jwt.validator"].create( dict( name=name, signature_type="secret", secret_algorithm="HS256", - secret_key="thesecret", + secret_key=secret_key, audience=audience, - issuer="http://the.issuer", + issuer=issuer, user_id_strategy="static", partner_id_strategy="email", partner_id_required=partner_id_required, ) ) - @contextlib.contextmanager - def _commit_validator(self, name, audience="me", partner_id_required=False): - validator = self._create_validator( - name=name, audience=audience, partner_id_required=partner_id_required - ) - - def _mocked_get_validator_by_name(self, validator_name): - if validator_name == name: - return validator - return self.env["auth.jwt.validator"]._get_validator_by_name.origin( - self, validator_name - ) - - try: - # Patch _get_validator_by_name because IrHttp._auth_method_jwt - # will look for the validator in another transaction, - # where the validator we created above would not be visible. - self.env["auth.jwt.validator"]._patch_method( - "_get_validator_by_name", _mocked_get_validator_by_name - ) - yield validator - finally: - self.env["auth.jwt.validator"]._revert_method("_get_validator_by_name") - def test_missing_authorization_header(self): with self._mock_request(authorization=None): with self.assertRaises(UnauthorizedMissingAuthorizationHeader): @@ -121,64 +106,186 @@ def test_malformed_authorization_header(self): self.env["ir.http"]._auth_method_jwt() def test_auth_method_valid_token(self): - with self._commit_validator("validator"): - authorization = "Bearer " + self._create_token() - with self._mock_request(authorization=authorization): - self.env["ir.http"]._auth_method_jwt_validator() + self._create_validator("validator") + authorization = "Bearer " + self._create_token() + with self._mock_request(authorization=authorization): + self.env["ir.http"]._auth_method_jwt_validator() - def test_auth_method_valid_token_two_validators(self): - with self._commit_validator( - "validator2", audience="bad" - ), self._commit_validator("validator3"): - authorization = "Bearer " + self._create_token() - with self._mock_request(authorization=authorization): - # first validator rejects the token because of invalid audience - with self.assertRaises(UnauthorizedInvalidToken): - self.env["ir.http"]._auth_method_jwt_validator2() - # second validator accepts the token - self.env["ir.http"]._auth_method_jwt_validator3() + def test_auth_method_valid_token_two_validators_one_bad_issuer(self): + self._create_validator("validator2", issuer="http://other.issuer") + self._create_validator("validator3") + + authorization = "Bearer " + self._create_token() + with self._mock_request(authorization=authorization): + # first validator rejects the token because of invalid audience + with self.assertRaises(UnauthorizedInvalidToken): + self.env["ir.http"]._auth_method_jwt_validator2() + # second validator accepts the token + self.env["ir.http"]._auth_method_jwt_validator3() + + def test_auth_method_valid_token_two_validators_one_bad_issuer_chained(self): + validator2 = self._create_validator("validator2", issuer="http://other.issuer") + validator3 = self._create_validator("validator3") + validator2.next_validator_id = validator3 + + authorization = "Bearer " + self._create_token() + with self._mock_request(authorization=authorization): + # Validator2 rejects the token because of invalid issuer but chain + # on validator3 which accepts it + self.env["ir.http"]._auth_method_jwt_validator2() + + def test_auth_method_valid_token_two_validators_one_bad_audience(self): + self._create_validator("validator2", audience="bad") + self._create_validator("validator3") + + authorization = "Bearer " + self._create_token() + with self._mock_request(authorization=authorization): + # first validator rejects the token because of invalid audience + with self.assertRaises(UnauthorizedInvalidToken): + self.env["ir.http"]._auth_method_jwt_validator2() + # second validator accepts the token + self.env["ir.http"]._auth_method_jwt_validator3() + + def test_auth_method_valid_token_two_validators_one_bad_audience_chained(self): + validator2 = self._create_validator("validator2", audience="bad") + validator3 = self._create_validator("validator3") + + validator2.next_validator_id = validator3 + authorization = "Bearer " + self._create_token() + with self._mock_request(authorization=authorization): + self.env["ir.http"]._auth_method_jwt_validator2() def test_auth_method_invalid_token(self): # Test invalid token via _auth_method_jwt # Other types of invalid tokens are unit tested elswhere. - with self._commit_validator("validator4"): - authorization = "Bearer " + self._create_token(audience="bad") - with self._mock_request(authorization=authorization): - with self.assertRaises(UnauthorizedInvalidToken): - self.env["ir.http"]._auth_method_jwt_validator4() + self._create_validator("validator4") + authorization = "Bearer " + self._create_token(audience="bad") + with self._mock_request(authorization=authorization): + with self.assertRaises(UnauthorizedInvalidToken): + self.env["ir.http"]._auth_method_jwt_validator4() + + def test_auth_method_invalid_token_on_chain(self): + validator1 = self._create_validator("validator", issuer="http://other.issuer") + validator2 = self._create_validator("validator2", audience="bad audience") + validator3 = self._create_validator("validator3", secret_key="bad key") + validator4 = self._create_validator( + "validator4", issuer="http://other.issuer", audience="bad audience" + ) + validator5 = self._create_validator( + "validator5", issuer="http://other.issuer", secret_key="bad key" + ) + validator6 = self._create_validator( + "validator6", audience="bad audience", secret_key="bad key" + ) + validator7 = self._create_validator( + "validator7", + issuer="http://other.issuer", + audience="bad audience", + secret_key="bad key", + ) + validator1.next_validator_id = validator2 + validator2.next_validator_id = validator3 + validator3.next_validator_id = validator4 + validator4.next_validator_id = validator5 + validator5.next_validator_id = validator6 + validator6.next_validator_id = validator7 + + authorization = "Bearer " + self._create_token() + with self._mock_request(authorization=authorization): + with self.assertRaises(CompositeJwtError) as composite_error: + self.env["ir.http"]._auth_method_jwt_validator() + self.assertEqual( + str(composite_error.exception), + "401 Unauthorized: Multiple errors occurred during JWT chain validation:\n" + "validator: 401 Unauthorized: " + "The server could not verify that you are authorized to " + "access the URL requested. You either supplied the wrong " + "credentials (e.g. a bad password), or your browser doesn't " + "understand how to supply the credentials required.\n" + "validator2: 401 Unauthorized: " + "The server could not verify that you are authorized to " + "access the URL requested. You either supplied the wrong " + "credentials (e.g. a bad password), or your browser doesn't " + "understand how to supply the credentials required.\n" + "validator3: 401 Unauthorized: " + "The server could not verify that you are authorized to " + "access the URL requested. You either supplied the wrong " + "credentials (e.g. a bad password), or your browser doesn't " + "understand how to supply the credentials required.\n" + "validator4: 401 Unauthorized: " + "The server could not verify that you are authorized to " + "access the URL requested. You either supplied the wrong " + "credentials (e.g. a bad password), or your browser doesn't " + "understand how to supply the credentials required.\n" + "validator5: 401 Unauthorized: " + "The server could not verify that you are authorized to " + "access the URL requested. You either supplied the wrong " + "credentials (e.g. a bad password), or your browser doesn't " + "understand how to supply the credentials required.\n" + "validator6: 401 Unauthorized: " + "The server could not verify that you are authorized to " + "access the URL requested. You either supplied the wrong " + "credentials (e.g. a bad password), or your browser doesn't " + "understand how to supply the credentials required.\n" + "validator7: 401 Unauthorized: " + "The server could not verify that you are authorized to " + "access the URL requested. You either supplied the wrong " + "credentials (e.g. a bad password), or your browser doesn't " + "understand how to supply the credentials required.", + ) + + def test_invalid_validation_chain(self): + validator1 = self._create_validator("validator") + validator2 = self._create_validator("validator2") + validator3 = self._create_validator("validator3") + + validator1.next_validator_id = validator2 + validator2.next_validator_id = validator3 + with self.assertRaises(ValidationError) as error: + validator3.next_validator_id = validator1 + self.assertEqual( + str(error.exception), + "Validators mustn't make a closed chain: " + "validator3 -> validator -> validator2 -> validator3.", + ) + + def test_invalid_validation_auto_chain(self): + validator = self._create_validator("validator") + with self.assertRaises(ValidationError) as error: + validator.next_validator_id = validator + self.assertEqual( + str(error.exception), + "Validators mustn't make a closed chain: " "validator -> validator.", + ) def test_user_id_strategy(self): - with self._commit_validator("validator5") as validator: - authorization = "Bearer " + self._create_token() - with self._mock_request(authorization=authorization) as request: - self.env["ir.http"]._auth_method_jwt_validator5() - self.assertEqual(request.uid, validator.static_user_id.id) + validator = self._create_validator("validator5") + authorization = "Bearer " + self._create_token() + with self._mock_request(authorization=authorization) as request: + self.env["ir.http"]._auth_method_jwt_validator5() + self.assertEqual(request.uid, validator.static_user_id.id) def test_partner_id_strategy_email_found(self): partner = self.env["res.partner"].search([("email", "!=", False)])[0] - with self._commit_validator("validator6"): - authorization = "Bearer " + self._create_token(email=partner.email) - with self._mock_request(authorization=authorization) as request: - self.env["ir.http"]._auth_method_jwt_validator6() - self.assertEqual(request.jwt_partner_id, partner.id) + self._create_validator("validator6") + authorization = "Bearer " + self._create_token(email=partner.email) + with self._mock_request(authorization=authorization) as request: + self.env["ir.http"]._auth_method_jwt_validator6() + self.assertEqual(request.jwt_partner_id, partner.id) def test_partner_id_strategy_email_not_found(self): - with self._commit_validator("validator6"): - authorization = "Bearer " + self._create_token( - email="notanemail@example.com" - ) - with self._mock_request(authorization=authorization) as request: - self.env["ir.http"]._auth_method_jwt_validator6() - self.assertFalse(request.jwt_partner_id) + self._create_validator("validator6") + authorization = "Bearer " + self._create_token(email="notanemail@example.com") + with self._mock_request(authorization=authorization) as request: + self.env["ir.http"]._auth_method_jwt_validator6() + self.assertFalse(request.jwt_partner_id) def test_partner_id_strategy_email_not_found_partner_required(self): - with self._commit_validator("validator6", partner_id_required=True): - authorization = "Bearer " + self._create_token( - email="notanemail@example.com" - ) - with self._mock_request(authorization=authorization): - with self.assertRaises(UnauthorizedPartnerNotFound): - self.env["ir.http"]._auth_method_jwt_validator6() + self._create_validator("validator6", partner_id_required=True) + authorization = "Bearer " + self._create_token(email="notanemail@example.com") + with self._mock_request(authorization=authorization): + with self.assertRaises(UnauthorizedPartnerNotFound): + self.env["ir.http"]._auth_method_jwt_validator6() def test_get_validator(self): AuthJwtValidator = self.env["auth.jwt.validator"] @@ -296,8 +403,8 @@ def test_public_or_jwt_no_token(self): assert not hasattr(request, "jwt_payload") def test_public_or_jwt_valid_token(self): - with self._commit_validator("validator"): - authorization = "Bearer " + self._create_token() - with self._mock_request(authorization=authorization) as request: - self.env["ir.http"]._auth_method_public_or_jwt_validator() - assert request.jwt_payload["aud"] == "me" + self._create_validator("validator") + authorization = "Bearer " + self._create_token() + with self._mock_request(authorization=authorization) as request: + self.env["ir.http"]._auth_method_public_or_jwt_validator() + assert request.jwt_payload["aud"] == "me" diff --git a/auth_jwt/views/auth_jwt_validator_views.xml b/auth_jwt/views/auth_jwt_validator_views.xml index 11c9c42e75..6bafe97978 100644 --- a/auth_jwt/views/auth_jwt_validator_views.xml +++ b/auth_jwt/views/auth_jwt_validator_views.xml @@ -10,6 +10,7 @@ + @@ -69,6 +70,7 @@ + From f486b8a2986a2edc4dc6a222b6b7d1f93c0b85a2 Mon Sep 17 00:00:00 2001 From: oca-ci Date: Tue, 14 Jun 2022 12:18:56 +0000 Subject: [PATCH 21/43] [UPD] Update auth_jwt.pot --- auth_jwt/i18n/auth_jwt.pot | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/auth_jwt/i18n/auth_jwt.pot b/auth_jwt/i18n/auth_jwt.pot index 5d22a8cac2..3c056f6751 100644 --- a/auth_jwt/i18n/auth_jwt.pot +++ b/auth_jwt/i18n/auth_jwt.pot @@ -153,6 +153,16 @@ msgstr "" msgid "Name %r is not a valid python identifier." msgstr "" +#. module: auth_jwt +#: model:ir.model.fields,field_description:auth_jwt.field_auth_jwt_validator__next_validator_id +msgid "Next Validator" +msgstr "" + +#. module: auth_jwt +#: model:ir.model.fields,help:auth_jwt.field_auth_jwt_validator__next_validator_id +msgid "Next validator to try if this one fails" +msgstr "" + #. module: auth_jwt #: model:ir.model.fields.selection,name:auth_jwt.selection__auth_jwt_validator__public_key_algorithm__ps256 msgid "PS256 - RSASSA-PSS using SHA-256 and MGF1 padding with SHA-256" @@ -248,6 +258,12 @@ msgstr "" msgid "User Id Strategy" msgstr "" +#. module: auth_jwt +#: code:addons/auth_jwt/models/auth_jwt_validator.py:0 +#, python-format +msgid "Validators mustn't make a closed chain: {}." +msgstr "" + #. module: auth_jwt #: model_terms:ir.ui.view,arch_db:auth_jwt.view_auth_jwt_validator_form #: model_terms:ir.ui.view,arch_db:auth_jwt.view_auth_jwt_validator_tree From e82d36fb5911d02d6727f1ee608e0d94047a6946 Mon Sep 17 00:00:00 2001 From: OCA-git-bot Date: Tue, 14 Jun 2022 12:24:54 +0000 Subject: [PATCH 22/43] auth_jwt 14.0.2.0.0 --- auth_jwt/__manifest__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/auth_jwt/__manifest__.py b/auth_jwt/__manifest__.py index 2e9ae85bd5..a8411ffd3f 100644 --- a/auth_jwt/__manifest__.py +++ b/auth_jwt/__manifest__.py @@ -5,7 +5,7 @@ "name": "Auth JWT", "summary": """ JWT bearer token authentication.""", - "version": "14.0.1.2.0", + "version": "14.0.2.0.0", "license": "LGPL-3", "author": "ACSONE SA/NV,Odoo Community Association (OCA)", "maintainers": ["sbidoul"], From e884ba22a008a267802ec4c159171a242d06df04 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= Date: Tue, 6 Jun 2023 12:18:24 +0200 Subject: [PATCH 23/43] [MIG] auth_jwt from 14 to 16 --- auth_jwt/__manifest__.py | 2 +- auth_jwt/models/auth_jwt_validator.py | 6 +++--- auth_jwt/models/ir_http.py | 2 +- auth_jwt/tests/test_auth_jwt.py | 6 ++++-- auth_jwt/views/auth_jwt_validator_views.xml | 2 +- 5 files changed, 10 insertions(+), 8 deletions(-) diff --git a/auth_jwt/__manifest__.py b/auth_jwt/__manifest__.py index a8411ffd3f..903eb0d11b 100644 --- a/auth_jwt/__manifest__.py +++ b/auth_jwt/__manifest__.py @@ -5,7 +5,7 @@ "name": "Auth JWT", "summary": """ JWT bearer token authentication.""", - "version": "14.0.2.0.0", + "version": "16.0.1.0.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 3d3c9b4d7c..9d0daec563 100644 --- a/auth_jwt/models/auth_jwt_validator.py +++ b/auth_jwt/models/auth_jwt_validator.py @@ -136,7 +136,7 @@ def _decode(self, token): header = jwt.get_unverified_header(token) except Exception as e: _logger.info("Invalid token: %s", e) - raise UnauthorizedInvalidToken() + raise UnauthorizedInvalidToken() from e key = self._get_key(header.get("kid")) algorithm = self.public_key_algorithm try: @@ -155,7 +155,7 @@ def _decode(self, token): ) except Exception as e: _logger.info("Invalid token: %s", e) - raise UnauthorizedInvalidToken() + raise UnauthorizedInvalidToken() from e return payload def _get_uid(self, payload): @@ -216,7 +216,7 @@ def _unregister_auth_method(self): try: delattr(IrHttp.__class__, f"_auth_method_jwt_{rec.name}") delattr(IrHttp.__class__, f"_auth_method_public_or_jwt_{rec.name}") - except AttributeError: + except AttributeError: # pylint: disable=except-pass pass @api.model_create_multi diff --git a/auth_jwt/models/ir_http.py b/auth_jwt/models/ir_http.py index 89bfec6cde..e53f7c420d 100644 --- a/auth_jwt/models/ir_http.py +++ b/auth_jwt/models/ir_http.py @@ -83,7 +83,7 @@ def _auth_method_jwt(cls, validator_name=None): uid = validator._get_and_check_uid(payload) assert uid partner_id = validator._get_and_check_partner_id(payload) - request.uid = uid # this resets request.env + request.update_env(user=uid) request.jwt_payload = payload request.jwt_partner_id = partner_id diff --git a/auth_jwt/tests/test_auth_jwt.py b/auth_jwt/tests/test_auth_jwt.py index c1e00c660c..f54527b646 100644 --- a/auth_jwt/tests/test_auth_jwt.py +++ b/auth_jwt/tests/test_auth_jwt.py @@ -263,7 +263,7 @@ def test_user_id_strategy(self): authorization = "Bearer " + self._create_token() with self._mock_request(authorization=authorization) as request: self.env["ir.http"]._auth_method_jwt_validator5() - self.assertEqual(request.uid, validator.static_user_id.id) + self.assertEqual(request.env.uid, validator.static_user_id.id) def test_partner_id_strategy_email_found(self): partner = self.env["res.partner"].search([("email", "!=", False)])[0] @@ -399,7 +399,9 @@ def test_name_check(self): def test_public_or_jwt_no_token(self): with self._mock_request(authorization=None) as request: self.env["ir.http"]._auth_method_public_or_jwt() - assert request.uid == self.env.ref("base.public_user").id + request.update_env.assert_called_once_with( + user=self.env.ref("base.public_user").id + ) assert not hasattr(request, "jwt_payload") def test_public_or_jwt_valid_token(self): diff --git a/auth_jwt/views/auth_jwt_validator_views.xml b/auth_jwt/views/auth_jwt_validator_views.xml index 6bafe97978..4ccce4af5b 100644 --- a/auth_jwt/views/auth_jwt_validator_views.xml +++ b/auth_jwt/views/auth_jwt_validator_views.xml @@ -62,7 +62,7 @@ auth.jwt.validator.tree auth.jwt.validator - + From cda5566840a8eeea58c49bc951b63b8a98fc3df8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= Date: Tue, 6 Jun 2023 15:54:10 +0200 Subject: [PATCH 24/43] [MIG] auth_jwt: convert unit tests to integration tests The unit tests were broken for non-functional reasons (interaction with the mock) and is easier to implement as integration test. --- auth_jwt/tests/test_auth_jwt.py | 17 ++--------------- 1 file changed, 2 insertions(+), 15 deletions(-) diff --git a/auth_jwt/tests/test_auth_jwt.py b/auth_jwt/tests/test_auth_jwt.py index f54527b646..5295f4aeb1 100644 --- a/auth_jwt/tests/test_auth_jwt.py +++ b/auth_jwt/tests/test_auth_jwt.py @@ -72,6 +72,7 @@ def _create_validator( issuer="http://the.issuer", secret_key="thesecret", partner_id_required=False, + static_user_id=1, ): return self.env["auth.jwt.validator"].create( dict( @@ -82,6 +83,7 @@ def _create_validator( audience=audience, issuer=issuer, user_id_strategy="static", + static_user_id=static_user_id, partner_id_strategy="email", partner_id_required=partner_id_required, ) @@ -258,13 +260,6 @@ def test_invalid_validation_auto_chain(self): "Validators mustn't make a closed chain: " "validator -> validator.", ) - def test_user_id_strategy(self): - validator = self._create_validator("validator5") - authorization = "Bearer " + self._create_token() - with self._mock_request(authorization=authorization) as request: - self.env["ir.http"]._auth_method_jwt_validator5() - self.assertEqual(request.env.uid, validator.static_user_id.id) - def test_partner_id_strategy_email_found(self): partner = self.env["res.partner"].search([("email", "!=", False)])[0] self._create_validator("validator6") @@ -396,14 +391,6 @@ def test_name_check(self): with self.assertRaises(ValidationError): self._create_validator(name="not an identifier") - def test_public_or_jwt_no_token(self): - with self._mock_request(authorization=None) as request: - self.env["ir.http"]._auth_method_public_or_jwt() - request.update_env.assert_called_once_with( - user=self.env.ref("base.public_user").id - ) - assert not hasattr(request, "jwt_payload") - def test_public_or_jwt_valid_token(self): self._create_validator("validator") authorization = "Bearer " + self._create_token() From 9a78860cb86cb00420e0004a6d747eb3706d2f24 Mon Sep 17 00:00:00 2001 From: oca-ci Date: Wed, 7 Jun 2023 11:10:49 +0000 Subject: [PATCH 25/43] [UPD] Update auth_jwt.pot --- auth_jwt/i18n/auth_jwt.pot | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/auth_jwt/i18n/auth_jwt.pot b/auth_jwt/i18n/auth_jwt.pot index 3c056f6751..b6989930e2 100644 --- a/auth_jwt/i18n/auth_jwt.pot +++ b/auth_jwt/i18n/auth_jwt.pot @@ -4,7 +4,7 @@ # msgid "" msgstr "" -"Project-Id-Version: Odoo Server 14.0\n" +"Project-Id-Version: Odoo Server 16.0\n" "Report-Msgid-Bugs-To: \n" "Last-Translator: \n" "Language-Team: \n" @@ -40,7 +40,6 @@ msgstr "" #. module: auth_jwt #: model:ir.model.fields,field_description:auth_jwt.field_auth_jwt_validator__display_name -#: model:ir.model.fields,field_description:auth_jwt.field_ir_http__display_name msgid "Display Name" msgstr "" @@ -91,7 +90,6 @@ msgstr "" #. module: auth_jwt #: model:ir.model.fields,field_description:auth_jwt.field_auth_jwt_validator__id -#: model:ir.model.fields,field_description:auth_jwt.field_ir_http__id msgid "ID" msgstr "" @@ -128,7 +126,6 @@ msgstr "" #. module: auth_jwt #: model:ir.model.fields,field_description:auth_jwt.field_auth_jwt_validator____last_update -#: model:ir.model.fields,field_description:auth_jwt.field_ir_http____last_update msgid "Last Modified on" msgstr "" @@ -148,6 +145,7 @@ msgid "Name" msgstr "" #. module: auth_jwt +#. odoo-python #: code:addons/auth_jwt/models/auth_jwt_validator.py:0 #, python-format msgid "Name %r is not a valid python identifier." @@ -259,6 +257,7 @@ msgid "User Id Strategy" msgstr "" #. module: auth_jwt +#. odoo-python #: code:addons/auth_jwt/models/auth_jwt_validator.py:0 #, python-format msgid "Validators mustn't make a closed chain: {}." @@ -266,6 +265,5 @@ msgstr "" #. module: auth_jwt #: model_terms:ir.ui.view,arch_db:auth_jwt.view_auth_jwt_validator_form -#: model_terms:ir.ui.view,arch_db:auth_jwt.view_auth_jwt_validator_tree msgid "arch" msgstr "" From 7c33f97735335c464caaf4c4915c5df2c754172f Mon Sep 17 00:00:00 2001 From: OCA-git-bot Date: Wed, 7 Jun 2023 11:20:42 +0000 Subject: [PATCH 26/43] [UPD] README.rst --- auth_jwt/README.rst | 10 +++++----- auth_jwt/static/description/index.html | 6 +++--- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/auth_jwt/README.rst b/auth_jwt/README.rst index 002a5092bf..82b57b1d3a 100644 --- a/auth_jwt/README.rst +++ b/auth_jwt/README.rst @@ -14,13 +14,13 @@ Auth JWT :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 - :target: https://github.com/OCA/server-auth/tree/14.0/auth_jwt + :target: https://github.com/OCA/server-auth/tree/16.0/auth_jwt :alt: OCA/server-auth .. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png - :target: https://translation.odoo-community.org/projects/server-auth-14-0/server-auth-14-0-auth_jwt + :target: https://translation.odoo-community.org/projects/server-auth-16-0/server-auth-16-0-auth_jwt :alt: Translate me on Weblate .. |badge5| image:: https://img.shields.io/badge/runbot-Try%20me-875A7B.png - :target: https://runbot.odoo-community.org/runbot/251/14.0 + :target: https://runbot.odoo-community.org/runbot/251/16.0 :alt: Try me on Runbot |badge1| |badge2| |badge3| |badge4| |badge5| @@ -102,7 +102,7 @@ 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 smashing it by providing a detailed and welcomed -`feedback `_. +`feedback `_. Do not contact contributors directly about support or help with technical issues. @@ -140,6 +140,6 @@ Current `maintainer `__: |maintainer-sbidoul| -This module is part of the `OCA/server-auth `_ project on GitHub. +This module is part of the `OCA/server-auth `_ project on GitHub. You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/auth_jwt/static/description/index.html b/auth_jwt/static/description/index.html index bd621ae44d..03f946d46d 100644 --- a/auth_jwt/static/description/index.html +++ b/auth_jwt/static/description/index.html @@ -367,7 +367,7 @@

Auth JWT

!! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! --> -

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

+

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

JWT bearer token authentication.

Table of contents

@@ -442,7 +442,7 @@

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 smashing it by providing a detailed and welcomed -feedback.

+feedback.

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

@@ -468,7 +468,7 @@

Maintainers

promote its widespread use.

Current maintainer:

sbidoul

-

This module is part of the OCA/server-auth project on GitHub.

+

This module is part of the OCA/server-auth project on GitHub.

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

From d8017fb3c4056c38222f0cc9788d63f0cae188b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= Date: Wed, 7 Jun 2023 16:14:23 +0200 Subject: [PATCH 27/43] auth_jwt: add cookie mode --- auth_jwt/exceptions.py | 8 +++ auth_jwt/models/auth_jwt_validator.py | 41 +++++++++++++++- auth_jwt/models/ir_http.py | 54 +++++++++++++++++++-- auth_jwt/readme/USAGE.rst | 11 ++++- auth_jwt/tests/test_auth_jwt.py | 6 ++- auth_jwt/views/auth_jwt_validator_views.xml | 26 ++++++++-- 6 files changed, 133 insertions(+), 13 deletions(-) diff --git a/auth_jwt/exceptions.py b/auth_jwt/exceptions.py index bc81b3ae12..d6129d5f82 100644 --- a/auth_jwt/exceptions.py +++ b/auth_jwt/exceptions.py @@ -8,6 +8,10 @@ class UnauthorizedMissingAuthorizationHeader(Unauthorized): pass +class UnauthorizedMissingCookie(Unauthorized): + pass + + class UnauthorizedMalformedAuthorizationHeader(Unauthorized): pass @@ -44,3 +48,7 @@ def __init__(self, errors): for validator_name, error in self.errors.items() ) ) + + +class UnauthorizedConfigurationError(Unauthorized): + pass diff --git a/auth_jwt/models/auth_jwt_validator.py b/auth_jwt/models/auth_jwt_validator.py index 9d0daec563..d8263cf86d 100644 --- a/auth_jwt/models/auth_jwt_validator.py +++ b/auth_jwt/models/auth_jwt_validator.py @@ -1,7 +1,9 @@ # Copyright 2021 ACSONE SA/NV # License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). +import datetime import logging +from calendar import timegm from functools import partial import jwt # pylint: disable=missing-manifest-dependency @@ -73,6 +75,23 @@ class AuthJwtValidator(models.Model): help="Next validator to try if this one fails", ) + cookie_enabled = fields.Boolean( + help=( + "Convert the JWT token into an HttpOnly Secure cookie. " + "When both an Authorization header and the cookie are present " + "in the request, the cookie is ignored." + ) + ) + cookie_name = fields.Char(default="authorization") + cookie_path = fields.Char(default="/") + cookie_max_age = fields.Integer( + default=86400 * 365, + help="Number of seconds until the cookie expires (Max-Age).", + ) + cookie_secure = fields.Boolean( + default=True, help="Set to false only for development without https." + ) + _sql_constraints = [ ("name_uniq", "unique(name)", "JWT validator names must be unique !"), ] @@ -126,9 +145,27 @@ def _get_key(self, kid): jwks_client = PyJWKClient(self.public_key_jwk_uri, cache_keys=False) return jwks_client.get_signing_key(kid).key - def _decode(self, token): + def _encode(self, payload, secret, expire): + """Encode and sign a JWT payload so it can be decoded and validated with + _decode(). + + The aud and iss claims are set to this validator's values. + The exp claim is set according to the expire parameter. + """ + payload = dict( + payload, + exp=timegm(datetime.datetime.utcnow().utctimetuple()) + expire, + aud=self.audience, + iss=self.issuer, + ) + return jwt.encode(payload, key=secret, algorithm="HS256") + + def _decode(self, token, secret=None): """Validate and decode a JWT token, return the payload.""" - if self.signature_type == "secret": + if secret: + key = secret + algorithm = "HS256" + elif self.signature_type == "secret": key = self.secret_key algorithm = self.secret_algorithm else: diff --git a/auth_jwt/models/ir_http.py b/auth_jwt/models/ir_http.py index e53f7c420d..c1d9bdd3a0 100644 --- a/auth_jwt/models/ir_http.py +++ b/auth_jwt/models/ir_http.py @@ -9,8 +9,10 @@ from ..exceptions import ( CompositeJwtError, + UnauthorizedConfigurationError, UnauthorizedMalformedAuthorizationHeader, UnauthorizedMissingAuthorizationHeader, + UnauthorizedMissingCookie, UnauthorizedSessionMismatch, ) @@ -54,12 +56,33 @@ def _authenticate(cls, endpoint): raise UnauthorizedSessionMismatch() return super()._authenticate(endpoint) + @classmethod + def _get_jwt_cookie_secret(cls): + secret = request.env["ir.config_parameter"].sudo().get_param("database.secret") + if not secret: + _logger.error("database.secret system parameter is not set.") + raise UnauthorizedConfigurationError() + return secret + + @classmethod + def _get_jwt_payload(cls, validator): + """Obtain and validate the JWT payload from the request authorization header or + cookie.""" + try: + token = cls._get_bearer_token() + assert token + return validator._decode(token) + except UnauthorizedMissingAuthorizationHeader: + if not validator.cookie_enabled: + raise + token = cls._get_cookie_token(validator.cookie_name) + assert token + return validator._decode(token, secret=cls._get_jwt_cookie_secret()) + @classmethod def _auth_method_jwt(cls, validator_name=None): assert not request.uid assert not request.session.uid - token = cls._get_bearer_token() - assert token # # Use request cursor to allow partner creation strategy in validator env = api.Environment(request.cr, SUPERUSER_ID, {}) validator = env["auth.jwt.validator"]._get_validator_by_name(validator_name) @@ -69,7 +92,7 @@ def _auth_method_jwt(cls, validator_name=None): exceptions = {} while validator: try: - payload = validator._decode(token) + payload = cls._get_jwt_payload(validator) break except Exception as e: exceptions[validator.name] = e @@ -80,6 +103,23 @@ def _auth_method_jwt(cls, validator_name=None): raise list(exceptions.values())[0] raise CompositeJwtError(exceptions) + if validator.cookie_enabled: + if not validator.cookie_name: + _logger.info("Cookie name not set for validator %s", validator.name) + raise UnauthorizedConfigurationError() + request.future_response.set_cookie( + key=validator.cookie_name, + value=validator._encode( + payload, + secret=cls._get_jwt_cookie_secret(), + expire=validator.cookie_max_age, + ), + max_age=validator.cookie_max_age, + path=validator.cookie_path or "/", + secure=validator.cookie_secure, + httponly=True, + ) + uid = validator._get_and_check_uid(payload) assert uid partner_id = validator._get_and_check_partner_id(payload) @@ -106,3 +146,11 @@ def _get_bearer_token(cls): _logger.info("Malformed Authorization header.") raise UnauthorizedMalformedAuthorizationHeader() return mo.group(1) + + @classmethod + def _get_cookie_token(cls, cookie_name): + token = request.httprequest.cookies.get(cookie_name) + if not token: + _logger.info("Missing cookie %s.", cookie_name) + raise UnauthorizedMissingCookie() + return token diff --git a/auth_jwt/readme/USAGE.rst b/auth_jwt/readme/USAGE.rst index be48400b6e..7d42e750a9 100644 --- a/auth_jwt/readme/USAGE.rst +++ b/auth_jwt/readme/USAGE.rst @@ -24,7 +24,8 @@ The JWT validator can be configured with the following properties: In addition, the ``exp`` claim is validated to reject expired tokens. If the ``Authorization`` HTTP header is missing, malformed, or contains -an invalid token, the request is rejected with a 401 (Unauthorized) code. +an invalid token, the request is rejected with a 401 (Unauthorized) code, +unless the cookie mode is enabled (see below). If the token is valid, the request executes with the configured user id. By default the user id selection strategy is ``static`` (i.e. the same for all @@ -53,3 +54,11 @@ endpoints that need to work for anonymous users, but can be enhanced when an authenticated user is know. A typical use case is a "add to cart" endpoint that can work for anonymous users, but can be enhanced by binding the cart to a known customer when the authenticated user is known. + +You can enable a cookie mode on JWT validators. In this case, the JWT payload obtained +from the ``Authorization`` header is returned as a Http-Only cookie. This mode is +sometimes simpler for front-end applications which do not then need to store and protect +the JWT token across requests and can simply rely on the cookie management mechanisms of +browsers. When both the ``Authorization`` header and a cookie are provided, the cookie +is ignored in order to let clients authenticate with a different user by providing a new +JWT token. diff --git a/auth_jwt/tests/test_auth_jwt.py b/auth_jwt/tests/test_auth_jwt.py index 5295f4aeb1..695ec2b773 100644 --- a/auth_jwt/tests/test_auth_jwt.py +++ b/auth_jwt/tests/test_auth_jwt.py @@ -90,11 +90,13 @@ def _create_validator( ) def test_missing_authorization_header(self): + self._create_validator("validator") with self._mock_request(authorization=None): with self.assertRaises(UnauthorizedMissingAuthorizationHeader): - self.env["ir.http"]._auth_method_jwt() + self.env["ir.http"]._auth_method_jwt(validator_name="validator") def test_malformed_authorization_header(self): + self._create_validator("validator") for authorization in ( "a", "Bearer", @@ -105,7 +107,7 @@ def test_malformed_authorization_header(self): ): with self._mock_request(authorization=authorization): with self.assertRaises(UnauthorizedMalformedAuthorizationHeader): - self.env["ir.http"]._auth_method_jwt() + self.env["ir.http"]._auth_method_jwt(validator_name="validator") def test_auth_method_valid_token(self): self._create_validator("validator") diff --git a/auth_jwt/views/auth_jwt_validator_views.xml b/auth_jwt/views/auth_jwt_validator_views.xml index 4ccce4af5b..bc907038a9 100644 --- a/auth_jwt/views/auth_jwt_validator_views.xml +++ b/auth_jwt/views/auth_jwt_validator_views.xml @@ -7,12 +7,12 @@
- + - - + + - + - + + + + + + +
From 621b4908d25e92f747ccd6b566f58e870b541b26 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= Date: Thu, 8 Jun 2023 08:25:31 +0200 Subject: [PATCH 28/43] auth_jwt: clarify exceptions Distinguish errors that lead to a 401 from internal configuration errors. --- auth_jwt/exceptions.py | 4 ++-- auth_jwt/models/ir_http.py | 13 +++++++------ auth_jwt/tests/test_auth_jwt.py | 4 ++-- 3 files changed, 11 insertions(+), 10 deletions(-) diff --git a/auth_jwt/exceptions.py b/auth_jwt/exceptions.py index d6129d5f82..e8af54d114 100644 --- a/auth_jwt/exceptions.py +++ b/auth_jwt/exceptions.py @@ -36,7 +36,7 @@ class UnauthorizedPartnerNotFound(Unauthorized): pass -class CompositeJwtError(Unauthorized): +class UnauthorizedCompositeJwtError(Unauthorized): """Indicate that multiple errors occurred during JWT chain validation.""" def __init__(self, errors): @@ -50,5 +50,5 @@ def __init__(self, errors): ) -class UnauthorizedConfigurationError(Unauthorized): +class ConfigurationError(InternalServerError): pass diff --git a/auth_jwt/models/ir_http.py b/auth_jwt/models/ir_http.py index c1d9bdd3a0..a78b74f415 100644 --- a/auth_jwt/models/ir_http.py +++ b/auth_jwt/models/ir_http.py @@ -8,8 +8,9 @@ from odoo.http import request from ..exceptions import ( - CompositeJwtError, - UnauthorizedConfigurationError, + ConfigurationError, + Unauthorized, + UnauthorizedCompositeJwtError, UnauthorizedMalformedAuthorizationHeader, UnauthorizedMissingAuthorizationHeader, UnauthorizedMissingCookie, @@ -61,7 +62,7 @@ def _get_jwt_cookie_secret(cls): secret = request.env["ir.config_parameter"].sudo().get_param("database.secret") if not secret: _logger.error("database.secret system parameter is not set.") - raise UnauthorizedConfigurationError() + raise ConfigurationError() return secret @classmethod @@ -94,19 +95,19 @@ def _auth_method_jwt(cls, validator_name=None): try: payload = cls._get_jwt_payload(validator) break - except Exception as e: + except Unauthorized as e: exceptions[validator.name] = e validator = validator.next_validator_id if not payload: if len(exceptions) == 1: raise list(exceptions.values())[0] - raise CompositeJwtError(exceptions) + raise UnauthorizedCompositeJwtError(exceptions) if validator.cookie_enabled: if not validator.cookie_name: _logger.info("Cookie name not set for validator %s", validator.name) - raise UnauthorizedConfigurationError() + raise ConfigurationError() request.future_response.set_cookie( key=validator.cookie_name, value=validator._encode( diff --git a/auth_jwt/tests/test_auth_jwt.py b/auth_jwt/tests/test_auth_jwt.py index 695ec2b773..20fb59b7cb 100644 --- a/auth_jwt/tests/test_auth_jwt.py +++ b/auth_jwt/tests/test_auth_jwt.py @@ -15,8 +15,8 @@ from ..exceptions import ( AmbiguousJwtValidator, - CompositeJwtError, JwtValidatorNotFound, + UnauthorizedCompositeJwtError, UnauthorizedInvalidToken, UnauthorizedMalformedAuthorizationHeader, UnauthorizedMissingAuthorizationHeader, @@ -196,7 +196,7 @@ def test_auth_method_invalid_token_on_chain(self): authorization = "Bearer " + self._create_token() with self._mock_request(authorization=authorization): - with self.assertRaises(CompositeJwtError) as composite_error: + with self.assertRaises(UnauthorizedCompositeJwtError) as composite_error: self.env["ir.http"]._auth_method_jwt_validator() self.assertEqual( str(composite_error.exception), From 91333a6e8a37363b3e79a832fc6612a75c5c588e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= Date: Thu, 8 Jun 2023 08:26:57 +0200 Subject: [PATCH 29/43] auth_jwt: minor refactoring --- auth_jwt/models/auth_jwt_validator.py | 8 ++++++++ auth_jwt/models/ir_http.py | 12 ++---------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/auth_jwt/models/auth_jwt_validator.py b/auth_jwt/models/auth_jwt_validator.py index d8263cf86d..72b099c09c 100644 --- a/auth_jwt/models/auth_jwt_validator.py +++ b/auth_jwt/models/auth_jwt_validator.py @@ -15,6 +15,7 @@ from ..exceptions import ( AmbiguousJwtValidator, + ConfigurationError, JwtValidatorNotFound, UnauthorizedInvalidToken, UnauthorizedPartnerNotFound, @@ -272,3 +273,10 @@ def write(self, vals): def unlink(self): self._unregister_auth_method() return super().unlink() + + def _get_jwt_cookie_secret(self): + secret = self.env["ir.config_parameter"].sudo().get_param("database.secret") + if not secret: + _logger.error("database.secret system parameter is not set.") + raise ConfigurationError() + return secret diff --git a/auth_jwt/models/ir_http.py b/auth_jwt/models/ir_http.py index a78b74f415..3c4a31a256 100644 --- a/auth_jwt/models/ir_http.py +++ b/auth_jwt/models/ir_http.py @@ -57,14 +57,6 @@ def _authenticate(cls, endpoint): raise UnauthorizedSessionMismatch() return super()._authenticate(endpoint) - @classmethod - def _get_jwt_cookie_secret(cls): - secret = request.env["ir.config_parameter"].sudo().get_param("database.secret") - if not secret: - _logger.error("database.secret system parameter is not set.") - raise ConfigurationError() - return secret - @classmethod def _get_jwt_payload(cls, validator): """Obtain and validate the JWT payload from the request authorization header or @@ -78,7 +70,7 @@ def _get_jwt_payload(cls, validator): raise token = cls._get_cookie_token(validator.cookie_name) assert token - return validator._decode(token, secret=cls._get_jwt_cookie_secret()) + return validator._decode(token, secret=validator._get_jwt_cookie_secret()) @classmethod def _auth_method_jwt(cls, validator_name=None): @@ -112,7 +104,7 @@ def _auth_method_jwt(cls, validator_name=None): key=validator.cookie_name, value=validator._encode( payload, - secret=cls._get_jwt_cookie_secret(), + secret=validator._get_jwt_cookie_secret(), expire=validator.cookie_max_age, ), max_age=validator.cookie_max_age, From 2353a46bb55e9dbb4dbe32f5a4672fcf69e2551d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= Date: Thu, 8 Jun 2023 13:05:40 +0200 Subject: [PATCH 30/43] [IMP] auth_jwt: refactor Extract _parse_bearer_authorization function for easier reuse by fastapi_auth_jwt --- auth_jwt/models/auth_jwt_validator.py | 22 ++++++++++++++++++++++ auth_jwt/models/ir_http.py | 17 +++-------------- 2 files changed, 25 insertions(+), 14 deletions(-) diff --git a/auth_jwt/models/auth_jwt_validator.py b/auth_jwt/models/auth_jwt_validator.py index 72b099c09c..6d2e938843 100644 --- a/auth_jwt/models/auth_jwt_validator.py +++ b/auth_jwt/models/auth_jwt_validator.py @@ -3,6 +3,7 @@ import datetime import logging +import re from calendar import timegm from functools import partial @@ -18,11 +19,15 @@ ConfigurationError, JwtValidatorNotFound, UnauthorizedInvalidToken, + UnauthorizedMalformedAuthorizationHeader, + UnauthorizedMissingAuthorizationHeader, UnauthorizedPartnerNotFound, ) _logger = logging.getLogger(__name__) +AUTHORIZATION_RE = re.compile(r"^Bearer ([^ ]+)$") + class AuthJwtValidator(models.Model): _name = "auth.jwt.validator" @@ -280,3 +285,20 @@ def _get_jwt_cookie_secret(self): _logger.error("database.secret system parameter is not set.") raise ConfigurationError() return secret + + @api.model + def _parse_bearer_authorization(self, authorization): + """Parse a Bearer token authorization header and return the token. + + Raises UnauthorizedMissingAuthorizationHeader if authorization is falsy. + Raises UnauthorizedMalformedAuthorizationHeader if invalid. + """ + if not authorization: + _logger.info("Missing Authorization header.") + raise UnauthorizedMissingAuthorizationHeader() + # https://tools.ietf.org/html/rfc6750#section-2.1 + mo = AUTHORIZATION_RE.match(authorization) + if not mo: + _logger.info("Malformed Authorization header.") + raise UnauthorizedMalformedAuthorizationHeader() + return mo.group(1) diff --git a/auth_jwt/models/ir_http.py b/auth_jwt/models/ir_http.py index 3c4a31a256..efa2c49b76 100644 --- a/auth_jwt/models/ir_http.py +++ b/auth_jwt/models/ir_http.py @@ -2,7 +2,6 @@ # License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). import logging -import re from odoo import SUPERUSER_ID, api, models from odoo.http import request @@ -11,7 +10,6 @@ ConfigurationError, Unauthorized, UnauthorizedCompositeJwtError, - UnauthorizedMalformedAuthorizationHeader, UnauthorizedMissingAuthorizationHeader, UnauthorizedMissingCookie, UnauthorizedSessionMismatch, @@ -20,9 +18,6 @@ _logger = logging.getLogger(__name__) -AUTHORIZATION_RE = re.compile(r"^Bearer ([^ ]+)$") - - class IrHttpJwt(models.AbstractModel): _inherit = "ir.http" @@ -130,15 +125,9 @@ def _auth_method_public_or_jwt(cls, validator_name=None): def _get_bearer_token(cls): # https://tools.ietf.org/html/rfc2617#section-3.2.2 authorization = request.httprequest.environ.get("HTTP_AUTHORIZATION") - if not authorization: - _logger.info("Missing Authorization header.") - raise UnauthorizedMissingAuthorizationHeader() - # https://tools.ietf.org/html/rfc6750#section-2.1 - mo = AUTHORIZATION_RE.match(authorization) - if not mo: - _logger.info("Malformed Authorization header.") - raise UnauthorizedMalformedAuthorizationHeader() - return mo.group(1) + return request.env["auth.jwt.validator"]._parse_bearer_authorization( + authorization + ) @classmethod def _get_cookie_token(cls, cookie_name): From 8aeef4a989f6a487133dda019c47b9cc7e614910 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= Date: Thu, 8 Jun 2023 16:35:36 +0200 Subject: [PATCH 31/43] [FIX] auth_jwt: don't use public mode if a cookie is present --- auth_jwt/models/ir_http.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/auth_jwt/models/ir_http.py b/auth_jwt/models/ir_http.py index efa2c49b76..b65118fd88 100644 --- a/auth_jwt/models/ir_http.py +++ b/auth_jwt/models/ir_http.py @@ -118,7 +118,13 @@ def _auth_method_jwt(cls, validator_name=None): @classmethod def _auth_method_public_or_jwt(cls, validator_name=None): if "HTTP_AUTHORIZATION" not in request.httprequest.environ: - return cls._auth_method_public() + env = api.Environment(request.cr, SUPERUSER_ID, {}) + validator = env["auth.jwt.validator"]._get_validator_by_name(validator_name) + assert len(validator) == 1 + if not validator.cookie_enabled or not request.httprequest.cookies.get( + validator.cookie_name + ): + return cls._auth_method_public() return cls._auth_method_jwt(validator_name) @classmethod From e003ebd722d02c6063672f92f78fbebb31a0fbc1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= Date: Fri, 16 Jun 2023 18:34:54 +0200 Subject: [PATCH 32/43] [IMP] auth_jwt: check cookie_name is present in cookie mode --- auth_jwt/models/auth_jwt_validator.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/auth_jwt/models/auth_jwt_validator.py b/auth_jwt/models/auth_jwt_validator.py index 6d2e938843..13649adad2 100644 --- a/auth_jwt/models/auth_jwt_validator.py +++ b/auth_jwt/models/auth_jwt_validator.py @@ -126,6 +126,18 @@ def _check_next_validator_id(self): ) ) + @api.constrains("cookie_enabled", "cookie_name") + def _check_cookie_name(self): + for rec in self: + if rec.cookie_enabled and not rec.cookie_name: + raise ValidationError( + _( + "A cookie name must be provided on JWT validator %s " + "because it has cookie mode enabled." + ) + % (rec.name,) + ) + @api.model def _get_validator_by_name_domain(self, validator_name): if validator_name: From 6131bfff7afea23895fa761b01b3a681cf50f702 Mon Sep 17 00:00:00 2001 From: oca-ci Date: Fri, 23 Jun 2023 15:09:28 +0000 Subject: [PATCH 33/43] [UPD] Update auth_jwt.pot --- auth_jwt/i18n/auth_jwt.pot | 77 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 77 insertions(+) diff --git a/auth_jwt/i18n/auth_jwt.pot b/auth_jwt/i18n/auth_jwt.pot index b6989930e2..9068730315 100644 --- a/auth_jwt/i18n/auth_jwt.pot +++ b/auth_jwt/i18n/auth_jwt.pot @@ -13,6 +13,15 @@ msgstr "" "Content-Transfer-Encoding: \n" "Plural-Forms: \n" +#. module: auth_jwt +#. odoo-python +#: code:addons/auth_jwt/models/auth_jwt_validator.py:0 +#, python-format +msgid "" +"A cookie name must be provided on JWT validator %s because it has cookie " +"mode enabled." +msgstr "" + #. module: auth_jwt #: model_terms:ir.ui.view,arch_db:auth_jwt.view_auth_jwt_validator_form msgid "Algorithm" @@ -28,6 +37,44 @@ msgstr "" msgid "Comma separated list of audiences, to validate aud." msgstr "" +#. module: auth_jwt +#: model:ir.model.fields,help:auth_jwt.field_auth_jwt_validator__cookie_enabled +msgid "" +"Convert the JWT token into an HttpOnly Secure cookie. When both an " +"Authorization header and the cookie are present in the request, the cookie " +"is ignored." +msgstr "" + +#. module: auth_jwt +#: model_terms:ir.ui.view,arch_db:auth_jwt.view_auth_jwt_validator_form +msgid "Cookie" +msgstr "" + +#. module: auth_jwt +#: model:ir.model.fields,field_description:auth_jwt.field_auth_jwt_validator__cookie_enabled +msgid "Cookie Enabled" +msgstr "" + +#. module: auth_jwt +#: model:ir.model.fields,field_description:auth_jwt.field_auth_jwt_validator__cookie_max_age +msgid "Cookie Max Age" +msgstr "" + +#. module: auth_jwt +#: model:ir.model.fields,field_description:auth_jwt.field_auth_jwt_validator__cookie_name +msgid "Cookie Name" +msgstr "" + +#. module: auth_jwt +#: model:ir.model.fields,field_description:auth_jwt.field_auth_jwt_validator__cookie_path +msgid "Cookie Path" +msgstr "" + +#. module: auth_jwt +#: model:ir.model.fields,field_description:auth_jwt.field_auth_jwt_validator__cookie_secure +msgid "Cookie Secure" +msgstr "" + #. module: auth_jwt #: model:ir.model.fields,field_description:auth_jwt.field_auth_jwt_validator__create_uid msgid "Created by" @@ -68,6 +115,11 @@ msgstr "" msgid "From email claim" msgstr "" +#. module: auth_jwt +#: model_terms:ir.ui.view,arch_db:auth_jwt.view_auth_jwt_validator_form +msgid "General" +msgstr "" + #. module: auth_jwt #: model:ir.model.fields.selection,name:auth_jwt.selection__auth_jwt_validator__secret_algorithm__hs256 msgid "HS256 - HMAC using SHA-256 hash algorithm" @@ -161,6 +213,11 @@ msgstr "" msgid "Next validator to try if this one fails" msgstr "" +#. module: auth_jwt +#: model:ir.model.fields,help:auth_jwt.field_auth_jwt_validator__cookie_max_age +msgid "Number of seconds until the cookie expires (Max-Age)." +msgstr "" + #. module: auth_jwt #: model:ir.model.fields.selection,name:auth_jwt.selection__auth_jwt_validator__public_key_algorithm__ps256 msgid "PS256 - RSASSA-PSS using SHA-256 and MGF1 padding with SHA-256" @@ -176,6 +233,11 @@ msgstr "" msgid "PS512 - RSASSA-PSS using SHA-512 and MGF1 padding with SHA-512" msgstr "" +#. module: auth_jwt +#: model_terms:ir.ui.view,arch_db:auth_jwt.view_auth_jwt_validator_form +msgid "Partner" +msgstr "" + #. module: auth_jwt #: model:ir.model.fields,field_description:auth_jwt.field_auth_jwt_validator__partner_id_required msgid "Partner Id Required" @@ -231,6 +293,11 @@ msgstr "" msgid "Secret Key" msgstr "" +#. module: auth_jwt +#: model:ir.model.fields,help:auth_jwt.field_auth_jwt_validator__cookie_secure +msgid "Set to false only for development without https." +msgstr "" + #. module: auth_jwt #: model:ir.model.fields,field_description:auth_jwt.field_auth_jwt_validator__signature_type msgid "Signature Type" @@ -251,6 +318,16 @@ msgstr "" msgid "To validate iss." msgstr "" +#. module: auth_jwt +#: model_terms:ir.ui.view,arch_db:auth_jwt.view_auth_jwt_validator_form +msgid "Token validation" +msgstr "" + +#. module: auth_jwt +#: model_terms:ir.ui.view,arch_db:auth_jwt.view_auth_jwt_validator_form +msgid "User" +msgstr "" + #. module: auth_jwt #: model:ir.model.fields,field_description:auth_jwt.field_auth_jwt_validator__user_id_strategy msgid "User Id Strategy" From 9942618bb2a42b4b12b8ac870e46e58b2b1d205f Mon Sep 17 00:00:00 2001 From: OCA-git-bot Date: Fri, 23 Jun 2023 15:12:24 +0000 Subject: [PATCH 34/43] [UPD] README.rst --- auth_jwt/README.rst | 11 ++++++++++- auth_jwt/static/description/index.html | 10 +++++++++- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/auth_jwt/README.rst b/auth_jwt/README.rst index 82b57b1d3a..df2b8daca7 100644 --- a/auth_jwt/README.rst +++ b/auth_jwt/README.rst @@ -66,7 +66,8 @@ The JWT validator can be configured with the following properties: In addition, the ``exp`` claim is validated to reject expired tokens. If the ``Authorization`` HTTP header is missing, malformed, or contains -an invalid token, the request is rejected with a 401 (Unauthorized) code. +an invalid token, the request is rejected with a 401 (Unauthorized) code, +unless the cookie mode is enabled (see below). If the token is valid, the request executes with the configured user id. By default the user id selection strategy is ``static`` (i.e. the same for all @@ -96,6 +97,14 @@ authenticated user is know. A typical use case is a "add to cart" endpoint that for anonymous users, but can be enhanced by binding the cart to a known customer when the authenticated user is known. +You can enable a cookie mode on JWT validators. In this case, the JWT payload obtained +from the ``Authorization`` header is returned as a Http-Only cookie. This mode is +sometimes simpler for front-end applications which do not then need to store and protect +the JWT token across requests and can simply rely on the cookie management mechanisms of +browsers. When both the ``Authorization`` header and a cookie are provided, the cookie +is ignored in order to let clients authenticate with a different user by providing a new +JWT token. + Bug Tracker =========== diff --git a/auth_jwt/static/description/index.html b/auth_jwt/static/description/index.html index 03f946d46d..467640b5ad 100644 --- a/auth_jwt/static/description/index.html +++ b/auth_jwt/static/description/index.html @@ -412,7 +412,8 @@

Usage

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

If the Authorization HTTP header is missing, malformed, or contains -an invalid token, the request is rejected with a 401 (Unauthorized) code.

+an invalid token, the request is rejected with a 401 (Unauthorized) code, +unless the cookie mode is enabled (see below).

If the token is valid, the request executes with the configured user id. By default the user id selection strategy is static (i.e. the same for all requests) and the selected user is configured on the JWT validator. Additional @@ -436,6 +437,13 @@

Usage

authenticated user is know. A typical use case is a “add to cart” endpoint that can work for anonymous users, but can be enhanced by binding the cart to a known customer when the authenticated user is known.

+

You can enable a cookie mode on JWT validators. In this case, the JWT payload obtained +from the Authorization header is returned as a Http-Only cookie. This mode is +sometimes simpler for front-end applications which do not then need to store and protect +the JWT token across requests and can simply rely on the cookie management mechanisms of +browsers. When both the Authorization header and a cookie are provided, the cookie +is ignored in order to let clients authenticate with a different user by providing a new +JWT token.

Bug Tracker

From b2001adc9375fad508ad2f4ef4cfb1167c2b9d36 Mon Sep 17 00:00:00 2001 From: OCA-git-bot Date: Fri, 23 Jun 2023 15:12:25 +0000 Subject: [PATCH 35/43] auth_jwt 16.0.1.1.0 --- auth_jwt/__manifest__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/auth_jwt/__manifest__.py b/auth_jwt/__manifest__.py index 903eb0d11b..059b9bc7ec 100644 --- a/auth_jwt/__manifest__.py +++ b/auth_jwt/__manifest__.py @@ -5,7 +5,7 @@ "name": "Auth JWT", "summary": """ JWT bearer token authentication.""", - "version": "16.0.1.0.0", + "version": "16.0.1.1.0", "license": "LGPL-3", "author": "ACSONE SA/NV,Odoo Community Association (OCA)", "maintainers": ["sbidoul"], From b3db3c40cc1401caa27439e44db67109ece5ece0 Mon Sep 17 00:00:00 2001 From: Ivorra78 Date: Fri, 25 Aug 2023 13:52:48 +0000 Subject: [PATCH 36/43] Added translation using Weblate (Spanish) --- auth_jwt/i18n/es.po | 347 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 347 insertions(+) create mode 100644 auth_jwt/i18n/es.po diff --git a/auth_jwt/i18n/es.po b/auth_jwt/i18n/es.po new file mode 100644 index 0000000000..c5be458a7a --- /dev/null +++ b/auth_jwt/i18n/es.po @@ -0,0 +1,347 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * auth_jwt +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.0\n" +"Report-Msgid-Bugs-To: \n" +"Last-Translator: Automatically generated\n" +"Language-Team: none\n" +"Language: es\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" + +#. module: auth_jwt +#. odoo-python +#: code:addons/auth_jwt/models/auth_jwt_validator.py:0 +#, python-format +msgid "" +"A cookie name must be provided on JWT validator %s because it has cookie " +"mode enabled." +msgstr "" + +#. module: auth_jwt +#: model_terms:ir.ui.view,arch_db:auth_jwt.view_auth_jwt_validator_form +msgid "Algorithm" +msgstr "" + +#. module: auth_jwt +#: model:ir.model.fields,field_description:auth_jwt.field_auth_jwt_validator__audience +msgid "Audience" +msgstr "" + +#. module: auth_jwt +#: model:ir.model.fields,help:auth_jwt.field_auth_jwt_validator__audience +msgid "Comma separated list of audiences, to validate aud." +msgstr "" + +#. module: auth_jwt +#: model:ir.model.fields,help:auth_jwt.field_auth_jwt_validator__cookie_enabled +msgid "" +"Convert the JWT token into an HttpOnly Secure cookie. When both an " +"Authorization header and the cookie are present in the request, the cookie " +"is ignored." +msgstr "" + +#. module: auth_jwt +#: model_terms:ir.ui.view,arch_db:auth_jwt.view_auth_jwt_validator_form +msgid "Cookie" +msgstr "" + +#. module: auth_jwt +#: model:ir.model.fields,field_description:auth_jwt.field_auth_jwt_validator__cookie_enabled +msgid "Cookie Enabled" +msgstr "" + +#. module: auth_jwt +#: model:ir.model.fields,field_description:auth_jwt.field_auth_jwt_validator__cookie_max_age +msgid "Cookie Max Age" +msgstr "" + +#. module: auth_jwt +#: model:ir.model.fields,field_description:auth_jwt.field_auth_jwt_validator__cookie_name +msgid "Cookie Name" +msgstr "" + +#. module: auth_jwt +#: model:ir.model.fields,field_description:auth_jwt.field_auth_jwt_validator__cookie_path +msgid "Cookie Path" +msgstr "" + +#. module: auth_jwt +#: model:ir.model.fields,field_description:auth_jwt.field_auth_jwt_validator__cookie_secure +msgid "Cookie Secure" +msgstr "" + +#. module: auth_jwt +#: model:ir.model.fields,field_description:auth_jwt.field_auth_jwt_validator__create_uid +msgid "Created by" +msgstr "" + +#. module: auth_jwt +#: model:ir.model.fields,field_description:auth_jwt.field_auth_jwt_validator__create_date +msgid "Created on" +msgstr "" + +#. module: auth_jwt +#: model:ir.model.fields,field_description:auth_jwt.field_auth_jwt_validator__display_name +msgid "Display Name" +msgstr "" + +#. module: auth_jwt +#: model:ir.model.fields.selection,name:auth_jwt.selection__auth_jwt_validator__public_key_algorithm__es256 +msgid "ES256 - ECDSA using SHA-256" +msgstr "" + +#. module: auth_jwt +#: model:ir.model.fields.selection,name:auth_jwt.selection__auth_jwt_validator__public_key_algorithm__es256k +msgid "ES256K - ECDSA with secp256k1 curve using SHA-256" +msgstr "" + +#. module: auth_jwt +#: model:ir.model.fields.selection,name:auth_jwt.selection__auth_jwt_validator__public_key_algorithm__es384 +msgid "ES384 - ECDSA using SHA-384" +msgstr "" + +#. module: auth_jwt +#: model:ir.model.fields.selection,name:auth_jwt.selection__auth_jwt_validator__public_key_algorithm__es512 +msgid "ES512 - ECDSA using SHA-512" +msgstr "" + +#. module: auth_jwt +#: model:ir.model.fields.selection,name:auth_jwt.selection__auth_jwt_validator__partner_id_strategy__email +msgid "From email claim" +msgstr "" + +#. module: auth_jwt +#: model_terms:ir.ui.view,arch_db:auth_jwt.view_auth_jwt_validator_form +msgid "General" +msgstr "" + +#. module: auth_jwt +#: model:ir.model.fields.selection,name:auth_jwt.selection__auth_jwt_validator__secret_algorithm__hs256 +msgid "HS256 - HMAC using SHA-256 hash algorithm" +msgstr "" + +#. module: auth_jwt +#: model:ir.model.fields.selection,name:auth_jwt.selection__auth_jwt_validator__secret_algorithm__hs384 +msgid "HS384 - HMAC using SHA-384 hash algorithm" +msgstr "" + +#. module: auth_jwt +#: model:ir.model.fields.selection,name:auth_jwt.selection__auth_jwt_validator__secret_algorithm__hs512 +msgid "HS512 - HMAC using SHA-512 hash algorithm" +msgstr "" + +#. module: auth_jwt +#: model:ir.model,name:auth_jwt.model_ir_http +msgid "HTTP Routing" +msgstr "" + +#. module: auth_jwt +#: model:ir.model.fields,field_description:auth_jwt.field_auth_jwt_validator__id +msgid "ID" +msgstr "" + +#. module: auth_jwt +#: model:ir.model.fields,field_description:auth_jwt.field_auth_jwt_validator__issuer +msgid "Issuer" +msgstr "" + +#. module: auth_jwt +#: model_terms:ir.ui.view,arch_db:auth_jwt.view_auth_jwt_validator_form +msgid "JWK URI" +msgstr "" + +#. module: auth_jwt +#: model:ir.model,name:auth_jwt.model_auth_jwt_validator +msgid "JWT Validator Configuration" +msgstr "" + +#. module: auth_jwt +#: model:ir.actions.act_window,name:auth_jwt.action_auth_jwt_validator +#: model:ir.ui.menu,name:auth_jwt.menu_auth_jwt_validator +msgid "JWT Validators" +msgstr "" + +#. module: auth_jwt +#: model:ir.model.constraint,message:auth_jwt.constraint_auth_jwt_validator_name_uniq +msgid "JWT validator names must be unique !" +msgstr "" + +#. module: auth_jwt +#: model_terms:ir.ui.view,arch_db:auth_jwt.view_auth_jwt_validator_form +msgid "Key" +msgstr "" + +#. module: auth_jwt +#: model:ir.model.fields,field_description:auth_jwt.field_auth_jwt_validator____last_update +msgid "Last Modified on" +msgstr "" + +#. module: auth_jwt +#: model:ir.model.fields,field_description:auth_jwt.field_auth_jwt_validator__write_uid +msgid "Last Updated by" +msgstr "" + +#. module: auth_jwt +#: model:ir.model.fields,field_description:auth_jwt.field_auth_jwt_validator__write_date +msgid "Last Updated on" +msgstr "" + +#. module: auth_jwt +#: model:ir.model.fields,field_description:auth_jwt.field_auth_jwt_validator__name +msgid "Name" +msgstr "" + +#. module: auth_jwt +#. odoo-python +#: code:addons/auth_jwt/models/auth_jwt_validator.py:0 +#, python-format +msgid "Name %r is not a valid python identifier." +msgstr "" + +#. module: auth_jwt +#: model:ir.model.fields,field_description:auth_jwt.field_auth_jwt_validator__next_validator_id +msgid "Next Validator" +msgstr "" + +#. module: auth_jwt +#: model:ir.model.fields,help:auth_jwt.field_auth_jwt_validator__next_validator_id +msgid "Next validator to try if this one fails" +msgstr "" + +#. module: auth_jwt +#: model:ir.model.fields,help:auth_jwt.field_auth_jwt_validator__cookie_max_age +msgid "Number of seconds until the cookie expires (Max-Age)." +msgstr "" + +#. module: auth_jwt +#: model:ir.model.fields.selection,name:auth_jwt.selection__auth_jwt_validator__public_key_algorithm__ps256 +msgid "PS256 - RSASSA-PSS using SHA-256 and MGF1 padding with SHA-256" +msgstr "" + +#. module: auth_jwt +#: model:ir.model.fields.selection,name:auth_jwt.selection__auth_jwt_validator__public_key_algorithm__ps384 +msgid "PS384 - RSASSA-PSS using SHA-384 and MGF1 padding with SHA-384" +msgstr "" + +#. module: auth_jwt +#: model:ir.model.fields.selection,name:auth_jwt.selection__auth_jwt_validator__public_key_algorithm__ps512 +msgid "PS512 - RSASSA-PSS using SHA-512 and MGF1 padding with SHA-512" +msgstr "" + +#. module: auth_jwt +#: model_terms:ir.ui.view,arch_db:auth_jwt.view_auth_jwt_validator_form +msgid "Partner" +msgstr "" + +#. module: auth_jwt +#: model:ir.model.fields,field_description:auth_jwt.field_auth_jwt_validator__partner_id_required +msgid "Partner Id Required" +msgstr "" + +#. module: auth_jwt +#: model:ir.model.fields,field_description:auth_jwt.field_auth_jwt_validator__partner_id_strategy +msgid "Partner Id Strategy" +msgstr "" + +#. module: auth_jwt +#: model:ir.model.fields,field_description:auth_jwt.field_auth_jwt_validator__public_key_algorithm +msgid "Public Key Algorithm" +msgstr "" + +#. module: auth_jwt +#: model:ir.model.fields,field_description:auth_jwt.field_auth_jwt_validator__public_key_jwk_uri +msgid "Public Key Jwk Uri" +msgstr "" + +#. module: auth_jwt +#: model:ir.model.fields.selection,name:auth_jwt.selection__auth_jwt_validator__signature_type__public_key +msgid "Public key" +msgstr "" + +#. module: auth_jwt +#: model:ir.model.fields.selection,name:auth_jwt.selection__auth_jwt_validator__public_key_algorithm__rs256 +msgid "RS256 - RSASSA-PKCS1-v1_5 using SHA-256" +msgstr "" + +#. module: auth_jwt +#: model:ir.model.fields.selection,name:auth_jwt.selection__auth_jwt_validator__public_key_algorithm__rs384 +msgid "RS384 - RSASSA-PKCS1-v1_5 using SHA-384" +msgstr "" + +#. module: auth_jwt +#: model:ir.model.fields.selection,name:auth_jwt.selection__auth_jwt_validator__public_key_algorithm__rs512 +msgid "RS512 - RSASSA-PKCS1-v1_5 using SHA-512" +msgstr "" + +#. module: auth_jwt +#: model:ir.model.fields.selection,name:auth_jwt.selection__auth_jwt_validator__signature_type__secret +msgid "Secret" +msgstr "" + +#. module: auth_jwt +#: model:ir.model.fields,field_description:auth_jwt.field_auth_jwt_validator__secret_algorithm +msgid "Secret Algorithm" +msgstr "" + +#. module: auth_jwt +#: model:ir.model.fields,field_description:auth_jwt.field_auth_jwt_validator__secret_key +msgid "Secret Key" +msgstr "" + +#. module: auth_jwt +#: model:ir.model.fields,help:auth_jwt.field_auth_jwt_validator__cookie_secure +msgid "Set to false only for development without https." +msgstr "" + +#. module: auth_jwt +#: model:ir.model.fields,field_description:auth_jwt.field_auth_jwt_validator__signature_type +msgid "Signature Type" +msgstr "" + +#. module: auth_jwt +#: model:ir.model.fields.selection,name:auth_jwt.selection__auth_jwt_validator__user_id_strategy__static +msgid "Static" +msgstr "" + +#. module: auth_jwt +#: model:ir.model.fields,field_description:auth_jwt.field_auth_jwt_validator__static_user_id +msgid "Static User" +msgstr "" + +#. module: auth_jwt +#: model:ir.model.fields,help:auth_jwt.field_auth_jwt_validator__issuer +msgid "To validate iss." +msgstr "" + +#. module: auth_jwt +#: model_terms:ir.ui.view,arch_db:auth_jwt.view_auth_jwt_validator_form +msgid "Token validation" +msgstr "" + +#. module: auth_jwt +#: model_terms:ir.ui.view,arch_db:auth_jwt.view_auth_jwt_validator_form +msgid "User" +msgstr "" + +#. module: auth_jwt +#: model:ir.model.fields,field_description:auth_jwt.field_auth_jwt_validator__user_id_strategy +msgid "User Id Strategy" +msgstr "" + +#. module: auth_jwt +#. odoo-python +#: code:addons/auth_jwt/models/auth_jwt_validator.py:0 +#, python-format +msgid "Validators mustn't make a closed chain: {}." +msgstr "" + +#. module: auth_jwt +#: model_terms:ir.ui.view,arch_db:auth_jwt.view_auth_jwt_validator_form +msgid "arch" +msgstr "" From 6c6a6fb6140c46fd2fa9bc5e65097cd913536b5a Mon Sep 17 00:00:00 2001 From: Ivorra78 Date: Fri, 25 Aug 2023 13:54:48 +0000 Subject: [PATCH 37/43] Translated using Weblate (Spanish) Currently translated at 100.0% (64 of 64 strings) Translation: server-auth-16.0/server-auth-16.0-auth_jwt Translate-URL: https://translation.odoo-community.org/projects/server-auth-16-0/server-auth-16-0-auth_jwt/es/ --- auth_jwt/i18n/es.po | 134 ++++++++++++++++++++++++-------------------- 1 file changed, 72 insertions(+), 62 deletions(-) diff --git a/auth_jwt/i18n/es.po b/auth_jwt/i18n/es.po index c5be458a7a..f3a2eeaaef 100644 --- a/auth_jwt/i18n/es.po +++ b/auth_jwt/i18n/es.po @@ -6,13 +6,15 @@ msgid "" msgstr "" "Project-Id-Version: Odoo Server 16.0\n" "Report-Msgid-Bugs-To: \n" -"Last-Translator: Automatically generated\n" +"PO-Revision-Date: 2023-09-02 19:25+0000\n" +"Last-Translator: Ivorra78 \n" "Language-Team: none\n" "Language: es\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: \n" "Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Weblate 4.17\n" #. module: auth_jwt #. odoo-python @@ -22,21 +24,23 @@ msgid "" "A cookie name must be provided on JWT validator %s because it has cookie " "mode enabled." msgstr "" +"Se debe proporcionar un nombre de cookie en el validador JWT %s porque tiene " +"habilitado el modo cookie." #. module: auth_jwt #: model_terms:ir.ui.view,arch_db:auth_jwt.view_auth_jwt_validator_form msgid "Algorithm" -msgstr "" +msgstr "Algoritmo" #. module: auth_jwt #: model:ir.model.fields,field_description:auth_jwt.field_auth_jwt_validator__audience msgid "Audience" -msgstr "" +msgstr "Audiencia" #. module: auth_jwt #: model:ir.model.fields,help:auth_jwt.field_auth_jwt_validator__audience msgid "Comma separated list of audiences, to validate aud." -msgstr "" +msgstr "Lista de audiencias separada por comas, para validar aud." #. module: auth_jwt #: model:ir.model.fields,help:auth_jwt.field_auth_jwt_validator__cookie_enabled @@ -45,303 +49,309 @@ msgid "" "Authorization header and the cookie are present in the request, the cookie " "is ignored." msgstr "" +"Convierte el código JWT en una cookie HttpOnly Secure. Cuando tanto la " +"cabecera de autorización como la cookie están presentes en la solicitud, se " +"ignora la cookie." #. module: auth_jwt #: model_terms:ir.ui.view,arch_db:auth_jwt.view_auth_jwt_validator_form msgid "Cookie" msgstr "" +"Paquete de datos que un programa recibe y reenvía sin cambiarlos y que " +"normalmente se emplea para indicar que ha ocurrido un evento o situación " +"especial" #. module: auth_jwt #: model:ir.model.fields,field_description:auth_jwt.field_auth_jwt_validator__cookie_enabled msgid "Cookie Enabled" -msgstr "" +msgstr "Cookie habilitada" #. module: auth_jwt #: model:ir.model.fields,field_description:auth_jwt.field_auth_jwt_validator__cookie_max_age msgid "Cookie Max Age" -msgstr "" +msgstr "Cookie Edad Máxima" #. module: auth_jwt #: model:ir.model.fields,field_description:auth_jwt.field_auth_jwt_validator__cookie_name msgid "Cookie Name" -msgstr "" +msgstr "Nombre de la cookie" #. module: auth_jwt #: model:ir.model.fields,field_description:auth_jwt.field_auth_jwt_validator__cookie_path msgid "Cookie Path" -msgstr "" +msgstr "Ruta de Cookies" #. module: auth_jwt #: model:ir.model.fields,field_description:auth_jwt.field_auth_jwt_validator__cookie_secure msgid "Cookie Secure" -msgstr "" +msgstr "Cookie segura" #. module: auth_jwt #: model:ir.model.fields,field_description:auth_jwt.field_auth_jwt_validator__create_uid msgid "Created by" -msgstr "" +msgstr "Creado por" #. module: auth_jwt #: model:ir.model.fields,field_description:auth_jwt.field_auth_jwt_validator__create_date msgid "Created on" -msgstr "" +msgstr "Creado el" #. module: auth_jwt #: model:ir.model.fields,field_description:auth_jwt.field_auth_jwt_validator__display_name msgid "Display Name" -msgstr "" +msgstr "Mostrar Nombre" #. module: auth_jwt #: model:ir.model.fields.selection,name:auth_jwt.selection__auth_jwt_validator__public_key_algorithm__es256 msgid "ES256 - ECDSA using SHA-256" -msgstr "" +msgstr "ES256 - ECDSA utilizando SHA-256" #. module: auth_jwt #: model:ir.model.fields.selection,name:auth_jwt.selection__auth_jwt_validator__public_key_algorithm__es256k msgid "ES256K - ECDSA with secp256k1 curve using SHA-256" -msgstr "" +msgstr "ES256K - ECDSA con curva secp256k1 utilizando SHA-256" #. module: auth_jwt #: model:ir.model.fields.selection,name:auth_jwt.selection__auth_jwt_validator__public_key_algorithm__es384 msgid "ES384 - ECDSA using SHA-384" -msgstr "" +msgstr "ES384 - ECDSA utilizando SHA-384" #. module: auth_jwt #: model:ir.model.fields.selection,name:auth_jwt.selection__auth_jwt_validator__public_key_algorithm__es512 msgid "ES512 - ECDSA using SHA-512" -msgstr "" +msgstr "ES512 - ECDSA utilizando SHA-512" #. module: auth_jwt #: model:ir.model.fields.selection,name:auth_jwt.selection__auth_jwt_validator__partner_id_strategy__email msgid "From email claim" -msgstr "" +msgstr "De la reclamación por correo electrónico" #. module: auth_jwt #: model_terms:ir.ui.view,arch_db:auth_jwt.view_auth_jwt_validator_form msgid "General" -msgstr "" +msgstr "General" #. module: auth_jwt #: model:ir.model.fields.selection,name:auth_jwt.selection__auth_jwt_validator__secret_algorithm__hs256 msgid "HS256 - HMAC using SHA-256 hash algorithm" -msgstr "" +msgstr "HS256 - HMAC utilizando el algoritmo hash SHA-256" #. module: auth_jwt #: model:ir.model.fields.selection,name:auth_jwt.selection__auth_jwt_validator__secret_algorithm__hs384 msgid "HS384 - HMAC using SHA-384 hash algorithm" -msgstr "" +msgstr "HS384 - HMAC utilizando el algoritmo hash SHA-384" #. module: auth_jwt #: model:ir.model.fields.selection,name:auth_jwt.selection__auth_jwt_validator__secret_algorithm__hs512 msgid "HS512 - HMAC using SHA-512 hash algorithm" -msgstr "" +msgstr "HS512 - HMAC utilizando el algoritmo hash SHA-512" #. module: auth_jwt #: model:ir.model,name:auth_jwt.model_ir_http msgid "HTTP Routing" -msgstr "" +msgstr "Enrutamiento HTTP" #. module: auth_jwt #: model:ir.model.fields,field_description:auth_jwt.field_auth_jwt_validator__id msgid "ID" -msgstr "" +msgstr "ID (identificación)" #. module: auth_jwt #: model:ir.model.fields,field_description:auth_jwt.field_auth_jwt_validator__issuer msgid "Issuer" -msgstr "" +msgstr "Emisor" #. module: auth_jwt #: model_terms:ir.ui.view,arch_db:auth_jwt.view_auth_jwt_validator_form msgid "JWK URI" -msgstr "" +msgstr "URI DE JWK" #. module: auth_jwt #: model:ir.model,name:auth_jwt.model_auth_jwt_validator msgid "JWT Validator Configuration" -msgstr "" +msgstr "Configuración del validador JWT" #. module: auth_jwt #: model:ir.actions.act_window,name:auth_jwt.action_auth_jwt_validator #: model:ir.ui.menu,name:auth_jwt.menu_auth_jwt_validator msgid "JWT Validators" -msgstr "" +msgstr "Validadores JWT" #. module: auth_jwt #: model:ir.model.constraint,message:auth_jwt.constraint_auth_jwt_validator_name_uniq msgid "JWT validator names must be unique !" -msgstr "" +msgstr "¡Los nombres de los validadores JWT deben ser únicos!" #. module: auth_jwt #: model_terms:ir.ui.view,arch_db:auth_jwt.view_auth_jwt_validator_form msgid "Key" -msgstr "" +msgstr "Clave" #. module: auth_jwt #: model:ir.model.fields,field_description:auth_jwt.field_auth_jwt_validator____last_update msgid "Last Modified on" -msgstr "" +msgstr "Última Modificación el" #. module: auth_jwt #: model:ir.model.fields,field_description:auth_jwt.field_auth_jwt_validator__write_uid msgid "Last Updated by" -msgstr "" +msgstr "Última actualización por" #. module: auth_jwt #: model:ir.model.fields,field_description:auth_jwt.field_auth_jwt_validator__write_date msgid "Last Updated on" -msgstr "" +msgstr "Última Actualización el" #. module: auth_jwt #: model:ir.model.fields,field_description:auth_jwt.field_auth_jwt_validator__name msgid "Name" -msgstr "" +msgstr "Nombre" #. module: auth_jwt #. odoo-python #: code:addons/auth_jwt/models/auth_jwt_validator.py:0 #, python-format msgid "Name %r is not a valid python identifier." -msgstr "" +msgstr "El nombre %r no es un identificador python válido." #. module: auth_jwt #: model:ir.model.fields,field_description:auth_jwt.field_auth_jwt_validator__next_validator_id msgid "Next Validator" -msgstr "" +msgstr "Siguiente Validador" #. module: auth_jwt #: model:ir.model.fields,help:auth_jwt.field_auth_jwt_validator__next_validator_id msgid "Next validator to try if this one fails" -msgstr "" +msgstr "Siguiente validador a probar si éste falla" #. module: auth_jwt #: model:ir.model.fields,help:auth_jwt.field_auth_jwt_validator__cookie_max_age msgid "Number of seconds until the cookie expires (Max-Age)." -msgstr "" +msgstr "Número de segundos hasta que expira la cookie (Max-Age)." #. module: auth_jwt #: model:ir.model.fields.selection,name:auth_jwt.selection__auth_jwt_validator__public_key_algorithm__ps256 msgid "PS256 - RSASSA-PSS using SHA-256 and MGF1 padding with SHA-256" -msgstr "" +msgstr "PS256 - RSASSA-PSS utilizando SHA-256 y relleno MGF1 con SHA-256" #. module: auth_jwt #: model:ir.model.fields.selection,name:auth_jwt.selection__auth_jwt_validator__public_key_algorithm__ps384 msgid "PS384 - RSASSA-PSS using SHA-384 and MGF1 padding with SHA-384" -msgstr "" +msgstr "PS384 - RSASSA-PSS utilizando SHA-384 y relleno MGF1 con SHA-384" #. module: auth_jwt #: model:ir.model.fields.selection,name:auth_jwt.selection__auth_jwt_validator__public_key_algorithm__ps512 msgid "PS512 - RSASSA-PSS using SHA-512 and MGF1 padding with SHA-512" -msgstr "" +msgstr "PS512 - RSASSA-PSS utilizando SHA-512 y relleno MGF1 con SHA-512" #. module: auth_jwt #: model_terms:ir.ui.view,arch_db:auth_jwt.view_auth_jwt_validator_form msgid "Partner" -msgstr "" +msgstr "Socio" #. module: auth_jwt #: model:ir.model.fields,field_description:auth_jwt.field_auth_jwt_validator__partner_id_required msgid "Partner Id Required" -msgstr "" +msgstr "Id de socio Obligatorio" #. module: auth_jwt #: model:ir.model.fields,field_description:auth_jwt.field_auth_jwt_validator__partner_id_strategy msgid "Partner Id Strategy" -msgstr "" +msgstr "Estrategia de ID de socio" #. module: auth_jwt #: model:ir.model.fields,field_description:auth_jwt.field_auth_jwt_validator__public_key_algorithm msgid "Public Key Algorithm" -msgstr "" +msgstr "Algoritmo de clave pública" #. module: auth_jwt #: model:ir.model.fields,field_description:auth_jwt.field_auth_jwt_validator__public_key_jwk_uri msgid "Public Key Jwk Uri" -msgstr "" +msgstr "Clave pública Jwk Uri" #. module: auth_jwt #: model:ir.model.fields.selection,name:auth_jwt.selection__auth_jwt_validator__signature_type__public_key msgid "Public key" -msgstr "" +msgstr "Clave pública" #. module: auth_jwt #: model:ir.model.fields.selection,name:auth_jwt.selection__auth_jwt_validator__public_key_algorithm__rs256 msgid "RS256 - RSASSA-PKCS1-v1_5 using SHA-256" -msgstr "" +msgstr "RS256 - RSASSA-PKCS1-v1_5 utilizando SHA-256" #. module: auth_jwt #: model:ir.model.fields.selection,name:auth_jwt.selection__auth_jwt_validator__public_key_algorithm__rs384 msgid "RS384 - RSASSA-PKCS1-v1_5 using SHA-384" -msgstr "" +msgstr "RS384 - RSASSA-PKCS1-v1_5 utilizando SHA-384" #. module: auth_jwt #: model:ir.model.fields.selection,name:auth_jwt.selection__auth_jwt_validator__public_key_algorithm__rs512 msgid "RS512 - RSASSA-PKCS1-v1_5 using SHA-512" -msgstr "" +msgstr "RS512 - RSASSA-PKCS1-v1_5 utilizando SHA-512" #. module: auth_jwt #: model:ir.model.fields.selection,name:auth_jwt.selection__auth_jwt_validator__signature_type__secret msgid "Secret" -msgstr "" +msgstr "Secreto" #. module: auth_jwt #: model:ir.model.fields,field_description:auth_jwt.field_auth_jwt_validator__secret_algorithm msgid "Secret Algorithm" -msgstr "" +msgstr "Algoritmo secreto" #. module: auth_jwt #: model:ir.model.fields,field_description:auth_jwt.field_auth_jwt_validator__secret_key msgid "Secret Key" -msgstr "" +msgstr "Clave secreta" #. module: auth_jwt #: model:ir.model.fields,help:auth_jwt.field_auth_jwt_validator__cookie_secure msgid "Set to false only for development without https." -msgstr "" +msgstr "Establecer a Falso sólo para el desarrollo sin https." #. module: auth_jwt #: model:ir.model.fields,field_description:auth_jwt.field_auth_jwt_validator__signature_type msgid "Signature Type" -msgstr "" +msgstr "Tipo de firma" #. module: auth_jwt #: model:ir.model.fields.selection,name:auth_jwt.selection__auth_jwt_validator__user_id_strategy__static msgid "Static" -msgstr "" +msgstr "Estático" #. module: auth_jwt #: model:ir.model.fields,field_description:auth_jwt.field_auth_jwt_validator__static_user_id msgid "Static User" -msgstr "" +msgstr "Usuario estático" #. module: auth_jwt #: model:ir.model.fields,help:auth_jwt.field_auth_jwt_validator__issuer msgid "To validate iss." -msgstr "" +msgstr "Para validar el iss." #. module: auth_jwt #: model_terms:ir.ui.view,arch_db:auth_jwt.view_auth_jwt_validator_form msgid "Token validation" -msgstr "" +msgstr "Validación de símbolos" #. module: auth_jwt #: model_terms:ir.ui.view,arch_db:auth_jwt.view_auth_jwt_validator_form msgid "User" -msgstr "" +msgstr "Usuario" #. module: auth_jwt #: model:ir.model.fields,field_description:auth_jwt.field_auth_jwt_validator__user_id_strategy msgid "User Id Strategy" -msgstr "" +msgstr "Estrategia de ID de usuario" #. module: auth_jwt #. odoo-python #: code:addons/auth_jwt/models/auth_jwt_validator.py:0 #, python-format msgid "Validators mustn't make a closed chain: {}." -msgstr "" +msgstr "Los validadores no deben hacer una cadena cerrada: {}." #. module: auth_jwt #: model_terms:ir.ui.view,arch_db:auth_jwt.view_auth_jwt_validator_form msgid "arch" -msgstr "" +msgstr "arch" From 80cd757599f9135d7b5586f267345d19f6b7274a Mon Sep 17 00:00:00 2001 From: OCA-git-bot Date: Sun, 3 Sep 2023 16:34:35 +0000 Subject: [PATCH 38/43] [UPD] README.rst --- auth_jwt/README.rst | 15 +++++---- auth_jwt/static/description/index.html | 44 ++++++++++++++------------ 2 files changed, 32 insertions(+), 27 deletions(-) diff --git a/auth_jwt/README.rst b/auth_jwt/README.rst index df2b8daca7..f309f82757 100644 --- a/auth_jwt/README.rst +++ b/auth_jwt/README.rst @@ -2,10 +2,13 @@ Auth JWT ======== -.. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! !! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:d22309ac82ef1eb8879974683b10d4be288eb330fd7e250927f1a8d602dc3988 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! .. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png :target: https://odoo-community.org/page/development-status @@ -19,11 +22,11 @@ Auth JWT .. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png :target: https://translation.odoo-community.org/projects/server-auth-16-0/server-auth-16-0-auth_jwt :alt: Translate me on Weblate -.. |badge5| image:: https://img.shields.io/badge/runbot-Try%20me-875A7B.png - :target: https://runbot.odoo-community.org/runbot/251/16.0 - :alt: Try me on Runbot +.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png + :target: https://runboat.odoo-community.org/builds?repo=OCA/server-auth&target_branch=16.0 + :alt: Try me on Runboat -|badge1| |badge2| |badge3| |badge4| |badge5| +|badge1| |badge2| |badge3| |badge4| |badge5| JWT bearer token authentication. @@ -110,7 +113,7 @@ 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 smashing it by providing a detailed and welcomed +If you spotted it first, help us to smash it by providing a detailed and welcomed `feedback `_. Do not contact contributors directly about support or help with technical issues. diff --git a/auth_jwt/static/description/index.html b/auth_jwt/static/description/index.html index 467640b5ad..109b2c3500 100644 --- a/auth_jwt/static/description/index.html +++ b/auth_jwt/static/description/index.html @@ -1,20 +1,20 @@ - + - + Auth JWT