diff --git a/auth_jwt/models/auth_jwt_validator.py b/auth_jwt/models/auth_jwt_validator.py index 13649adad2..efe4332a0f 100644 --- a/auth_jwt/models/auth_jwt_validator.py +++ b/auth_jwt/models/auth_jwt_validator.py @@ -27,6 +27,12 @@ _logger = logging.getLogger(__name__) AUTHORIZATION_RE = re.compile(r"^Bearer ([^ ]+)$") +SECRET_ALGORITHM_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"), +] class AuthJwtValidator(models.Model): @@ -39,12 +45,7 @@ class AuthJwtValidator(models.Model): ) 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"), - ], + SECRET_ALGORITHM_SELECTION, default="HS256", ) public_key_jwk_uri = fields.Char() @@ -97,6 +98,17 @@ class AuthJwtValidator(models.Model): cookie_secure = fields.Boolean( default=True, help="Set to false only for development without https." ) + renew_cookie_on_response = fields.Boolean( + help="Renew the cookie in every response to stay the client " + "authenticatedas long it use the API. Don't mark unless you have a " + "way to invalidate sessions", + default=True, + ) + renew_cookie_secret = fields.Char() + renew_cookie_algorithm = fields.Selection( + SECRET_ALGORITHM_SELECTION, + default="HS256", + ) _sql_constraints = [ ("name_uniq", "unique(name)", "JWT validator names must be unique !"), @@ -163,26 +175,40 @@ 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): + def _encode(self, payload, expire, secret=False): """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. """ + if secret: + key = secret + algorithm = "HS256" + elif self.renew_cookie_on_response: + key = self.renew_cookie_secret + algorithm = self.renew_cookie_algorithm + elif self.signature_type == "secret": + key = self.secret_key + algorithm = self.secret_algorithm + else: + raise ConfigurationError(_("The token cannot be encoded with public key")) payload = dict( payload, exp=timegm(datetime.datetime.utcnow().utctimetuple()) + expire, aud=self.audience, iss=self.issuer, ) - return jwt.encode(payload, key=secret, algorithm="HS256") + return jwt.encode(payload, key=key, algorithm=algorithm) - def _decode(self, token, secret=None): + def _decode(self, token, secret=None, cookie_secret=False): """Validate and decode a JWT token, return the payload.""" if secret: key = secret algorithm = "HS256" + elif self.renew_cookie_on_response and cookie_secret: + key = self.renew_cookie_secret + algorithm = self.renew_cookie_algorithm elif self.signature_type == "secret": key = self.secret_key algorithm = self.secret_algorithm @@ -291,13 +317,6 @@ 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. diff --git a/auth_jwt/models/ir_http.py b/auth_jwt/models/ir_http.py index b65118fd88..bdfcd66a4d 100644 --- a/auth_jwt/models/ir_http.py +++ b/auth_jwt/models/ir_http.py @@ -65,7 +65,7 @@ def _get_jwt_payload(cls, validator): raise token = cls._get_cookie_token(validator.cookie_name) assert token - return validator._decode(token, secret=validator._get_jwt_cookie_secret()) + return validator._decode(token, cookie_secret=True) @classmethod def _auth_method_jwt(cls, validator_name=None): @@ -91,7 +91,7 @@ def _auth_method_jwt(cls, validator_name=None): raise list(exceptions.values())[0] raise UnauthorizedCompositeJwtError(exceptions) - if validator.cookie_enabled: + if validator.cookie_enabled and validator.renew_cookie_on_response: if not validator.cookie_name: _logger.info("Cookie name not set for validator %s", validator.name) raise ConfigurationError() @@ -99,7 +99,6 @@ def _auth_method_jwt(cls, validator_name=None): 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, diff --git a/auth_jwt/views/auth_jwt_validator_views.xml b/auth_jwt/views/auth_jwt_validator_views.xml index bc907038a9..583a969b0d 100644 --- a/auth_jwt/views/auth_jwt_validator_views.xml +++ b/auth_jwt/views/auth_jwt_validator_views.xml @@ -68,6 +68,24 @@ name="cookie_max_age" attrs="{'invisible': [('cookie_enabled', '=', False)]}" /> + + + + diff --git a/auth_jwt_demo/demo/auth_jwt_validator.xml b/auth_jwt_demo/demo/auth_jwt_validator.xml index 6bba454a67..63e5663720 100644 --- a/auth_jwt_demo/demo/auth_jwt_validator.xml +++ b/auth_jwt_demo/demo/auth_jwt_validator.xml @@ -24,6 +24,8 @@ demo_auth + + renew_cookie_secret demo_keycloak