-
-
Notifications
You must be signed in to change notification settings - Fork 482
[16.0][IMP]auth_jwt: Add the possibility to not renew the cookie in the responses #822
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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 @@ | |
| ) | ||
| 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 @@ | |
| 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", | ||
| ) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not convinced we should propose an algorithm selection here. |
||
|
|
||
| _sql_constraints = [ | ||
| ("name_uniq", "unique(name)", "JWT validator names must be unique !"), | ||
|
|
@@ -163,26 +175,40 @@ | |
| 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): | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would not change this method. It is not necessary to change it if all the logic to get the secret is in |
||
| """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 @@ | |
| 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 | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I prefer keeping this method and return the validator |
||
|
|
||
| @api.model | ||
| def _parse_bearer_authorization(self, authorization): | ||
| """Parse a Bearer token authorization header and return the token. | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This change is not necessary. |
||
|
|
||
| @classmethod | ||
| def _auth_method_jwt(cls, validator_name=None): | ||
|
|
@@ -91,15 +91,14 @@ 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: | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This change does not seem necessary.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. i don't understand why, we need it to not renew the cookie. |
||
| 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, | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This secret is not specific to whether the cookie lifetime is renewed or not.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see a relation with the renewal, if is not set, we dont want to change it.
For example, i have a domain.com who creates a cookie with the jwt for the domain, and i have odoo in odoo.domain.com, so the cookie will be shared between domain and subdomains. I want the validator to check that cookie against the secret/public key, not against that new secret, the cookie should only be checked against that secret if odoo creates its own cookie when the renew is checked.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes that is your use case, but the two changes are logically independent. Deciding whether renewing the cookie automatically and how to sign it are two independent topics. Reasoning about it that way should make the change simpler while still supporting your use case I think.