diff --git a/auth_jwt/README.rst b/auth_jwt/README.rst new file mode 100644 index 0000000000..e378dc8875 --- /dev/null +++ b/auth_jwt/README.rst @@ -0,0 +1,164 @@ +======== +Auth JWT +======== + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:608e8780fabb7f7a32193245dd2a7e594810863dcc55aa1dc9e5b2bc3426d74c + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |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-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/18.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-18-0/server-auth-18-0-auth_jwt + :alt: Translate me on Weblate +.. |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=18.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +JWT bearer token authentication. + +**Table of contents** + +.. contents:: + :local: + +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}"`` 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: + +- ``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, 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 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``. + +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. + +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 +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +------- + +* ACSONE SA/NV + +Contributors +------------ + +- Stéphane Bidoul +- Mohamed Alkobrosli + +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..8a311e5bdf --- /dev/null +++ b/auth_jwt/__manifest__.py @@ -0,0 +1,20 @@ +# Copyright 2021 ACSONE SA/NV +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). + +{ + "name": "Auth JWT", + "summary": """ + JWT bearer token authentication.""", + "version": "18.0.1.0.0", + "license": "LGPL-3", + "author": "ACSONE SA/NV,Odoo Community Association (OCA)", + "maintainers": ["sbidoul"], + "website": "https://github.com/OCA/server-auth", + "depends": [], + "external_dependencies": {"python": ["pyjwt", "cryptography"]}, + "data": ["security/ir.model.access.csv", "views/auth_jwt_validator_views.xml"], + "demo": [], + "installable": True, + "application": False, + "auto_install": False, +} diff --git a/auth_jwt/exceptions.py b/auth_jwt/exceptions.py new file mode 100644 index 0000000000..1864954100 --- /dev/null +++ b/auth_jwt/exceptions.py @@ -0,0 +1,54 @@ +# Copyright 2021 ACSONE SA/NV +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl) + +from werkzeug.exceptions import InternalServerError, Unauthorized + + +class UnauthorizedMissingAuthorizationHeader(Unauthorized): + pass + + +class UnauthorizedMissingCookie(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 + + +class UnauthorizedCompositeJwtError(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( + f"{validator_name}: {error}" + for validator_name, error in self.errors.items() + ) + ) + + +class ConfigurationError(InternalServerError): + pass diff --git a/auth_jwt/i18n/auth_jwt.pot b/auth_jwt/i18n/auth_jwt.pot new file mode 100644 index 0000000000..e156e00ea8 --- /dev/null +++ b/auth_jwt/i18n/auth_jwt.pot @@ -0,0 +1,341 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * auth_jwt +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 18.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 +#. 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__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 "" diff --git a/auth_jwt/i18n/es.po b/auth_jwt/i18n/es.po new file mode 100644 index 0000000000..9354d02328 --- /dev/null +++ b/auth_jwt/i18n/es.po @@ -0,0 +1,355 @@ +# 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" +"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 +#: 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 "" +"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 "Algoritmo" + +#. module: auth_jwt +#: model:ir.model.fields,field_description:auth_jwt.field_auth_jwt_validator__audience +msgid "Audience" +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 "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 +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 "" +"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 "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 "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 "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 "Ruta de Cookies" + +#. module: auth_jwt +#: model:ir.model.fields,field_description:auth_jwt.field_auth_jwt_validator__cookie_secure +msgid "Cookie Secure" +msgstr "Cookie segura" + +#. module: auth_jwt +#: model:ir.model.fields,field_description:auth_jwt.field_auth_jwt_validator__create_uid +msgid "Created by" +msgstr "Creado por" + +#. module: auth_jwt +#: model:ir.model.fields,field_description:auth_jwt.field_auth_jwt_validator__create_date +msgid "Created on" +msgstr "Creado el" + +#. module: auth_jwt +#: model:ir.model.fields,field_description:auth_jwt.field_auth_jwt_validator__display_name +msgid "Display Name" +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 "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 "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 "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 "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 "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 "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 "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 "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 "HS512 - HMAC utilizando el algoritmo hash SHA-512" + +#. module: auth_jwt +#: model:ir.model,name:auth_jwt.model_ir_http +msgid "HTTP Routing" +msgstr "Enrutamiento HTTP" + +#. module: auth_jwt +#: model:ir.model.fields,field_description:auth_jwt.field_auth_jwt_validator__id +msgid "ID" +msgstr "ID (identificación)" + +#. module: auth_jwt +#: model:ir.model.fields,field_description:auth_jwt.field_auth_jwt_validator__issuer +msgid "Issuer" +msgstr "Emisor" + +#. module: auth_jwt +#: model_terms:ir.ui.view,arch_db:auth_jwt.view_auth_jwt_validator_form +msgid "JWK URI" +msgstr "URI DE JWK" + +#. module: auth_jwt +#: model:ir.model,name:auth_jwt.model_auth_jwt_validator +msgid "JWT Validator Configuration" +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 "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 "¡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 "Clave" + +#. module: auth_jwt +#: model:ir.model.fields,field_description:auth_jwt.field_auth_jwt_validator__write_uid +msgid "Last Updated by" +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 "Última Actualización el" + +#. module: auth_jwt +#: model:ir.model.fields,field_description:auth_jwt.field_auth_jwt_validator__name +msgid "Name" +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 "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 "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 "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 "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 "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 "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 "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 "Socio" + +#. module: auth_jwt +#: model:ir.model.fields,field_description:auth_jwt.field_auth_jwt_validator__partner_id_required +msgid "Partner Id Required" +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 "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 "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 "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 "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 "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 "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 "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 "Secreto" + +#. module: auth_jwt +#: model:ir.model.fields,field_description:auth_jwt.field_auth_jwt_validator__secret_algorithm +msgid "Secret Algorithm" +msgstr "Algoritmo secreto" + +#. module: auth_jwt +#: model:ir.model.fields,field_description:auth_jwt.field_auth_jwt_validator__secret_key +msgid "Secret Key" +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 "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 "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 "Estático" + +#. module: auth_jwt +#: model:ir.model.fields,field_description:auth_jwt.field_auth_jwt_validator__static_user_id +msgid "Static User" +msgstr "Usuario estático" + +#. module: auth_jwt +#: model:ir.model.fields,help:auth_jwt.field_auth_jwt_validator__issuer +msgid "To validate iss." +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 "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 "Usuario" + +#. module: auth_jwt +#: model:ir.model.fields,field_description:auth_jwt.field_auth_jwt_validator__user_id_strategy +msgid "User Id Strategy" +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 "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 "arch" + +#~ msgid "Last Modified on" +#~ msgstr "Última Modificación el" diff --git a/auth_jwt/i18n/it.po b/auth_jwt/i18n/it.po new file mode 100644 index 0000000000..bcda0b5ff3 --- /dev/null +++ b/auth_jwt/i18n/it.po @@ -0,0 +1,352 @@ +# 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" +"PO-Revision-Date: 2024-01-29 11:35+0000\n" +"Last-Translator: Francesco Foresti \n" +"Language-Team: none\n" +"Language: it\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 +#: 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 "" +"È necessario fornire un nome del cookie sul validatore JWT %s perché ha la " +"modalità cookie abilitata." + +#. module: auth_jwt +#: model_terms:ir.ui.view,arch_db:auth_jwt.view_auth_jwt_validator_form +msgid "Algorithm" +msgstr "Algoritmo" + +#. module: auth_jwt +#: model:ir.model.fields,field_description:auth_jwt.field_auth_jwt_validator__audience +msgid "Audience" +msgstr "Audience" + +#. 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 "Elenco di audience separati da virgole, per validare aud." + +#. 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 "" +"Converti il token JWT in un cookie HttpOnly Secure. Quando nella richiesta " +"sono presenti sia un Authorization header che il cookie, il cookie viene " +"ignorato." + +#. module: auth_jwt +#: model_terms:ir.ui.view,arch_db:auth_jwt.view_auth_jwt_validator_form +msgid "Cookie" +msgstr "Cookie" + +#. module: auth_jwt +#: model:ir.model.fields,field_description:auth_jwt.field_auth_jwt_validator__cookie_enabled +msgid "Cookie Enabled" +msgstr "Cookie abilitato" + +#. module: auth_jwt +#: model:ir.model.fields,field_description:auth_jwt.field_auth_jwt_validator__cookie_max_age +msgid "Cookie Max Age" +msgstr "Durata massima cookie" + +#. module: auth_jwt +#: model:ir.model.fields,field_description:auth_jwt.field_auth_jwt_validator__cookie_name +msgid "Cookie Name" +msgstr "Nome cookie" + +#. module: auth_jwt +#: model:ir.model.fields,field_description:auth_jwt.field_auth_jwt_validator__cookie_path +msgid "Cookie Path" +msgstr "Path cookie" + +#. module: auth_jwt +#: model:ir.model.fields,field_description:auth_jwt.field_auth_jwt_validator__cookie_secure +msgid "Cookie Secure" +msgstr "Cookie secure" + +#. module: auth_jwt +#: model:ir.model.fields,field_description:auth_jwt.field_auth_jwt_validator__create_uid +msgid "Created by" +msgstr "Creato da" + +#. module: auth_jwt +#: model:ir.model.fields,field_description:auth_jwt.field_auth_jwt_validator__create_date +msgid "Created on" +msgstr "Creato il" + +#. module: auth_jwt +#: model:ir.model.fields,field_description:auth_jwt.field_auth_jwt_validator__display_name +msgid "Display Name" +msgstr "Nome visualizzato" + +#. 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 "ES256 - ECDSA usando 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 "ES256K - ECDSA con curva secp256k1 usando 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 "ES384 - ECDSA usando 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 "ES512 - ECDSA usando 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 "Da richiesta e-mail" + +#. module: auth_jwt +#: model_terms:ir.ui.view,arch_db:auth_jwt.view_auth_jwt_validator_form +msgid "General" +msgstr "Generale" + +#. 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 "HS256 - HMAC usando SHA-256 hash algorithm" + +#. 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 "HS384 - HMAC usando SHA-384 hash algorithm" + +#. 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 "HS512 - HMAC usando SHA-512 hash algorithm" + +#. module: auth_jwt +#: model:ir.model,name:auth_jwt.model_ir_http +msgid "HTTP Routing" +msgstr "Instradamento HTTP" + +#. module: auth_jwt +#: model:ir.model.fields,field_description:auth_jwt.field_auth_jwt_validator__id +msgid "ID" +msgstr "ID" + +#. module: auth_jwt +#: model:ir.model.fields,field_description:auth_jwt.field_auth_jwt_validator__issuer +msgid "Issuer" +msgstr "Segnalatore" + +#. module: auth_jwt +#: model_terms:ir.ui.view,arch_db:auth_jwt.view_auth_jwt_validator_form +msgid "JWK URI" +msgstr "URI JWK" + +#. module: auth_jwt +#: model:ir.model,name:auth_jwt.model_auth_jwt_validator +msgid "JWT Validator Configuration" +msgstr "Configurazione validatore 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 "Validatori 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 "I nomi dei validatori JWT devono essere univoci!" + +#. module: auth_jwt +#: model_terms:ir.ui.view,arch_db:auth_jwt.view_auth_jwt_validator_form +msgid "Key" +msgstr "Chiave" + +#. module: auth_jwt +#: model:ir.model.fields,field_description:auth_jwt.field_auth_jwt_validator__write_uid +msgid "Last Updated by" +msgstr "Ultimo aggiornamento di" + +#. module: auth_jwt +#: model:ir.model.fields,field_description:auth_jwt.field_auth_jwt_validator__write_date +msgid "Last Updated on" +msgstr "Ultimo aggiornamento il" + +#. module: auth_jwt +#: model:ir.model.fields,field_description:auth_jwt.field_auth_jwt_validator__name +msgid "Name" +msgstr "Nome" + +#. 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 "Il nome %r non è un identificatore Python valido." + +#. module: auth_jwt +#: model:ir.model.fields,field_description:auth_jwt.field_auth_jwt_validator__next_validator_id +msgid "Next Validator" +msgstr "Validatore successivo" + +#. 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 "Validatore successivo da provare se questo fallisce" + +#. 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 "Numero di secondi fino alla scadenza del cookie (Durata max)." + +#. 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 "PS256 - RSASSA-PSS usando SHA-256 e padding 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 "PS384 - RSASSA-PSS usando SHA-384 e padding 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 "PS512 - RSASSA-PSS usando SHA-512 e padding MGF1 con SHA-512" + +#. module: auth_jwt +#: model_terms:ir.ui.view,arch_db:auth_jwt.view_auth_jwt_validator_form +msgid "Partner" +msgstr "Partner" + +#. module: auth_jwt +#: model:ir.model.fields,field_description:auth_jwt.field_auth_jwt_validator__partner_id_required +msgid "Partner Id Required" +msgstr "Partner ID obbligatorio" + +#. module: auth_jwt +#: model:ir.model.fields,field_description:auth_jwt.field_auth_jwt_validator__partner_id_strategy +msgid "Partner Id Strategy" +msgstr "Strategia Partner ID" + +#. module: auth_jwt +#: model:ir.model.fields,field_description:auth_jwt.field_auth_jwt_validator__public_key_algorithm +msgid "Public Key Algorithm" +msgstr "Algoritmo a chiave pubblica" + +#. 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 "Jwk Uri a chiave pubblica" + +#. module: auth_jwt +#: model:ir.model.fields.selection,name:auth_jwt.selection__auth_jwt_validator__signature_type__public_key +msgid "Public key" +msgstr "Chiave pubblica" + +#. 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 "RS256 - RSASSA-PKCS1-v1_5 usando 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 "RS384 - RSASSA-PKCS1-v1_5 usando 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 "RS512 - RSASSA-PKCS1-v1_5 usando SHA-512" + +#. module: auth_jwt +#: model:ir.model.fields.selection,name:auth_jwt.selection__auth_jwt_validator__signature_type__secret +msgid "Secret" +msgstr "Segreta" + +#. module: auth_jwt +#: model:ir.model.fields,field_description:auth_jwt.field_auth_jwt_validator__secret_algorithm +msgid "Secret Algorithm" +msgstr "Algoritmo segreto" + +#. module: auth_jwt +#: model:ir.model.fields,field_description:auth_jwt.field_auth_jwt_validator__secret_key +msgid "Secret Key" +msgstr "Chiave segreta" + +#. 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 "Imposta su false solo per lo sviluppo senza https." + +#. module: auth_jwt +#: model:ir.model.fields,field_description:auth_jwt.field_auth_jwt_validator__signature_type +msgid "Signature Type" +msgstr "Tipo di firma" + +#. module: auth_jwt +#: model:ir.model.fields.selection,name:auth_jwt.selection__auth_jwt_validator__user_id_strategy__static +msgid "Static" +msgstr "Statica" + +#. module: auth_jwt +#: model:ir.model.fields,field_description:auth_jwt.field_auth_jwt_validator__static_user_id +msgid "Static User" +msgstr "Utente statico" + +#. module: auth_jwt +#: model:ir.model.fields,help:auth_jwt.field_auth_jwt_validator__issuer +msgid "To validate iss." +msgstr "Per validare iss." + +#. module: auth_jwt +#: model_terms:ir.ui.view,arch_db:auth_jwt.view_auth_jwt_validator_form +msgid "Token validation" +msgstr "Convalida del token" + +#. module: auth_jwt +#: model_terms:ir.ui.view,arch_db:auth_jwt.view_auth_jwt_validator_form +msgid "User" +msgstr "Utente" + +#. module: auth_jwt +#: model:ir.model.fields,field_description:auth_jwt.field_auth_jwt_validator__user_id_strategy +msgid "User Id Strategy" +msgstr "Strategia User ID" + +#. 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 "I validatori non devono creare una catena chiusa: {}." + +#. module: auth_jwt +#: model_terms:ir.ui.view,arch_db:auth_jwt.view_auth_jwt_validator_form +msgid "arch" +msgstr "arch" + +#~ msgid "Last Modified on" +#~ msgstr "Ultima modifica il" 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..13649adad2 --- /dev/null +++ b/auth_jwt/models/auth_jwt_validator.py @@ -0,0 +1,316 @@ +# Copyright 2021 ACSONE SA/NV +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). + +import datetime +import logging +import re +from calendar import timegm +from functools import partial + +import jwt # pylint: disable=missing-manifest-dependency +from jwt import PyJWKClient +from werkzeug.exceptions import InternalServerError + +from odoo import _, api, fields, models, tools +from odoo.exceptions import ValidationError + +from ..exceptions import ( + AmbiguousJwtValidator, + ConfigurationError, + JwtValidatorNotFound, + UnauthorizedInvalidToken, + UnauthorizedMalformedAuthorizationHeader, + UnauthorizedMissingAuthorizationHeader, + UnauthorizedPartnerNotFound, +) + +_logger = logging.getLogger(__name__) + +AUTHORIZATION_RE = re.compile(r"^Bearer ([^ ]+)$") + + +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( + [ + # 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( + [ + # 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="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" + ) + static_user_id = fields.Many2one("res.users", default=1) + 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", + ) + + 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 !"), + ] + + @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.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.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: + 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): + jwks_client = PyJWKClient(self.public_key_jwk_uri, cache_keys=False) + return jwks_client.get_signing_key(kid).key + + 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 secret: + key = secret + algorithm = "HS256" + elif self.signature_type == "secret": + key = self.secret_key + algorithm = self.secret_algorithm + else: + try: + header = jwt.get_unverified_header(token) + except Exception as e: + _logger.info("Invalid token: %s", e) + raise UnauthorizedInvalidToken() from e + 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", "aud", "iss"], + verify_exp=True, + verify_aud=True, + verify_iss=True, + ), + audience=self.audience.split(","), + issuer=self.issuer, + ) + except Exception as e: + _logger.info("Invalid token: %s", e) + raise UnauthorizedInvalidToken() from e + 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), + ) + 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: # pylint: disable=except-pass + 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() + + 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 + + @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 new file mode 100644 index 0000000000..7168e50894 --- /dev/null +++ b/auth_jwt/models/ir_http.py @@ -0,0 +1,143 @@ +# Copyright 2021 ACSONE SA/NV +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). + +import logging + +from odoo import SUPERUSER_ID, api, models +from odoo.http import request + +from ..exceptions import ( + ConfigurationError, + Unauthorized, + UnauthorizedCompositeJwtError, + UnauthorizedMissingAuthorizationHeader, + UnauthorizedMissingCookie, + UnauthorizedSessionMismatch, +) + +_logger = logging.getLogger(__name__) + + +class IrHttpJwt(models.AbstractModel): + _inherit = "ir.http" + + @classmethod + def _authenticate(cls, endpoint): + """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. + """ + auth_method = endpoint.routing["auth"] + 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.' + ) + raise UnauthorizedSessionMismatch() + # 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." + ) + raise UnauthorizedSessionMismatch() + return super()._authenticate(endpoint) + + @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=validator._get_jwt_cookie_secret()) + + @classmethod + def _auth_method_jwt(cls, validator_name=None): + assert not request.uid + assert not request.session.uid + # # 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 = cls._get_jwt_payload(validator) + break + 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 UnauthorizedCompositeJwtError(exceptions) + + if validator.cookie_enabled: + if not validator.cookie_name: + _logger.info("Cookie name not set for validator %s", validator.name) + raise ConfigurationError() + request.future_response.set_cookie( + key=validator.cookie_name, + value=validator._encode( + payload, + secret=validator._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) + request.update_env(user=uid) + 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: + 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 + def _get_bearer_token(cls): + # https://tools.ietf.org/html/rfc2617#section-3.2.2 + authorization = request.httprequest.environ.get("HTTP_AUTHORIZATION") + return request.env["auth.jwt.validator"]._parse_bearer_authorization( + authorization + ) + + @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/pyproject.toml b/auth_jwt/pyproject.toml new file mode 100644 index 0000000000..4231d0cccb --- /dev/null +++ b/auth_jwt/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" diff --git a/auth_jwt/readme/CONTRIBUTORS.md b/auth_jwt/readme/CONTRIBUTORS.md new file mode 100644 index 0000000000..d6260f557c --- /dev/null +++ b/auth_jwt/readme/CONTRIBUTORS.md @@ -0,0 +1,2 @@ +- Stéphane Bidoul \<\> +- Mohamed Alkobrosli \<\> diff --git a/auth_jwt/readme/DESCRIPTION.md b/auth_jwt/readme/DESCRIPTION.md new file mode 100644 index 0000000000..9322c82e13 --- /dev/null +++ b/auth_jwt/readme/DESCRIPTION.md @@ -0,0 +1 @@ +JWT bearer token authentication. diff --git a/auth_jwt/readme/INSTALL.md b/auth_jwt/readme/INSTALL.md new file mode 100644 index 0000000000..529cf45e83 --- /dev/null +++ b/auth_jwt/readme/INSTALL.md @@ -0,0 +1 @@ +This module requires the `pyjwt` library to be installed. diff --git a/auth_jwt/readme/USAGE.md b/auth_jwt/readme/USAGE.md new file mode 100644 index 0000000000..b67c4fc331 --- /dev/null +++ b/auth_jwt/readme/USAGE.md @@ -0,0 +1,69 @@ +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}"` 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: + +- `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, +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 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`. + +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. + +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/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 0000000000..3a0328b516 Binary files /dev/null and b/auth_jwt/static/description/icon.png differ diff --git a/auth_jwt/static/description/index.html b/auth_jwt/static/description/index.html new file mode 100644 index 0000000000..2f1623fd24 --- /dev/null +++ b/auth_jwt/static/description/index.html @@ -0,0 +1,496 @@ + + + + + +Auth JWT + + + +
+

Auth JWT

+ + +

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

+

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}" 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:

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

+

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.

+

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

+

Bugs are tracked on GitHub Issues. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +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.

+
+
+
+ + 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..6a87e87cbc --- /dev/null +++ b/auth_jwt/tests/test_auth_jwt.py @@ -0,0 +1,406 @@ +# Copyright 2021 ACSONE SA/NV +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). + +import contextlib +import time +from unittest.mock import Mock + +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, + UnauthorizedCompositeJwtError, + UnauthorizedInvalidToken, + UnauthorizedMalformedAuthorizationHeader, + UnauthorizedMissingAuthorizationHeader, + UnauthorizedPartnerNotFound, +) + + +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=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. + del request.jwt_payload + del request.jwt_partner_id + + 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, + 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", + issuer="http://the.issuer", + secret_key="thesecret", + partner_id_required=False, + static_user_id=1, + ): + return self.env["auth.jwt.validator"].create( + dict( + name=name, + signature_type="secret", + secret_algorithm="HS256", + secret_key=secret_key, + audience=audience, + issuer=issuer, + user_id_strategy="static", + static_user_id=static_user_id, + partner_id_strategy="email", + partner_id_required=partner_id_required, + ) + ) + + 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(validator_name="validator") + + def test_malformed_authorization_header(self): + self._create_validator("validator") + 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(validator_name="validator") + + def test_auth_method_valid_token(self): + 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_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. + 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(UnauthorizedCompositeJwtError) 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_partner_id_strategy_email_found(self): + partner = self.env["res.partner"].search([("email", "!=", False)])[0] + 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): + 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): + 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"] + 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_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_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")) + 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_valid_token(self): + 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 new file mode 100644 index 0000000000..8aac0f500f --- /dev/null +++ b/auth_jwt/views/auth_jwt_validator_views.xml @@ -0,0 +1,103 @@ + + + + auth.jwt.validator.form + auth.jwt.validator + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ + auth.jwt.validator.tree + auth.jwt.validator + + + + + + + + + + + + + + + JWT Validators + auth.jwt.validator + list,form + + +
diff --git a/requirements.txt b/requirements.txt index 8e7bf891df..8566c5de6f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,5 @@ # generated from manifests external_dependencies +cryptography +pyjwt pysaml2 python-jose