diff --git a/README.md b/README.md index 6eb6e02ab7..c432e279be 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,7 @@ addon | version | maintainers | summary [auth_api_key](auth_api_key/) | 18.0.1.0.1 | | Authenticate http requests from an API key [auth_api_key_group](auth_api_key_group/) | 18.0.1.0.0 | simahawk | Allow grouping API keys together. Grouping per se does nothing. This feature is supposed to be used by other modules to limit access to services or records based on groups of keys. [auth_api_key_server_env](auth_api_key_server_env/) | 18.0.1.0.0 | | Configure api keys via server env. This can be very useful to avoid mixing your keys between your various environments when restoring databases. All you have to do is to add a new section to your configuration file according to the following convention: +[auth_jwt](auth_jwt/) | 18.0.1.0.0 | sbidoul | JWT bearer token authentication. [auth_oauth_filter_by_domain](auth_oauth_filter_by_domain/) | 18.0.1.0.0 | natuan9 | Filter OAuth providers by domain [auth_oauth_multi_token](auth_oauth_multi_token/) | 18.0.2.0.0 | | Allow multiple connection with the same OAuth account [auth_oidc](auth_oidc/) | 18.0.1.0.0 | sbidoul | Allow users to login through OpenID Connect Provider diff --git a/auth_jwt/README.rst b/auth_jwt/README.rst new file mode 100644 index 0000000000..c63b8c4476 --- /dev/null +++ b/auth_jwt/README.rst @@ -0,0 +1,168 @@ +.. image:: https://odoo-community.org/readme-banner-image + :target: https://odoo-community.org/get-involved?utm_source=readme + :alt: Odoo Community Association + +======== +Auth JWT +======== + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:0257cb75b9a02ab9b3f1aeebe8e0c5aee0b983f8b5ac1692132897dfb1986d02 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png + :target: https://odoo-community.org/page/development-status + :alt: Beta +.. |badge2| image:: https://img.shields.io/badge/license-LGPL--3-blue.png + :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..e4d80df1ff --- /dev/null +++ b/auth_jwt/i18n/auth_jwt.pot @@ -0,0 +1,338 @@ +# 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 +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 +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 +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..8813f94463 --- /dev/null +++ b/auth_jwt/static/description/index.html @@ -0,0 +1,502 @@ + + + + + +README.rst + + + +
+ + + +Odoo Community Association + +
+

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 302ba0e988..1fa3781a8b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,7 @@ # generated from manifests external_dependencies +cryptography email_validator lxml +pyjwt pysaml2 python-jose diff --git a/setup/_metapackage/pyproject.toml b/setup/_metapackage/pyproject.toml index 9514b1ac6e..994befc1b3 100644 --- a/setup/_metapackage/pyproject.toml +++ b/setup/_metapackage/pyproject.toml @@ -1,11 +1,12 @@ [project] name = "odoo-addons-oca-server-auth" -version = "18.0.20250614.2" +version = "18.0.20250826.0" dependencies = [ "odoo-addon-auth_admin_passkey==18.0.*", "odoo-addon-auth_api_key==18.0.*", "odoo-addon-auth_api_key_group==18.0.*", "odoo-addon-auth_api_key_server_env==18.0.*", + "odoo-addon-auth_jwt==18.0.*", "odoo-addon-auth_oauth_filter_by_domain==18.0.*", "odoo-addon-auth_oauth_multi_token==18.0.*", "odoo-addon-auth_oidc==18.0.*",