# 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