Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 35 additions & 16 deletions auth_jwt/models/auth_jwt_validator.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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()
Expand Down Expand Up @@ -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()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
renew_cookie_secret = fields.Char()
cookie_secret = fields.Char()

This secret is not specific to whether the cookie lifetime is renewed or not.

Copy link
Member Author

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.

Copy link
Member

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.

renew_cookie_algorithm = fields.Selection(
SECRET_ALGORITHM_SELECTION,
default="HS256",
)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not convinced we should propose an algorithm selection here.
For the JWT / bearer token, the algorithm must be agreed with the clients so an algorithm selection makes sense.
But here it's the server which protect the cookie for itself (the cookie is opaque to the clients), and we can let the server select the best algorithm.


_sql_constraints = [
("name_uniq", "unique(name)", "JWT validator names must be unique !"),
Expand Down Expand Up @@ -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):
Copy link
Member

Choose a reason for hiding this comment

The 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 _get_jwt_cookie_secret().

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

Check warning on line 187 in auth_jwt/models/auth_jwt_validator.py

View check run for this annotation

Codecov / codecov/patch

auth_jwt/models/auth_jwt_validator.py#L186-L187

Added lines #L186 - L187 were not covered by tests
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

Check warning on line 193 in auth_jwt/models/auth_jwt_validator.py

View check run for this annotation

Codecov / codecov/patch

auth_jwt/models/auth_jwt_validator.py#L192-L193

Added lines #L192 - L193 were not covered by tests
else:
raise ConfigurationError(_("The token cannot be encoded with public key"))

Check warning on line 195 in auth_jwt/models/auth_jwt_validator.py

View check run for this annotation

Codecov / codecov/patch

auth_jwt/models/auth_jwt_validator.py#L195

Added line #L195 was not covered by tests
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
Expand Down Expand Up @@ -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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I prefer keeping this method and return the validator cookie_secret if set else the database secret.


@api.model
def _parse_bearer_authorization(self, authorization):
"""Parse a Bearer token authorization header and return the token.
Expand Down
5 changes: 2 additions & 3 deletions auth_jwt/models/ir_http.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Copy link
Member

Choose a reason for hiding this comment

The 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):
Expand All @@ -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:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change does not seem necessary.

Copy link
Member Author

Choose a reason for hiding this comment

The 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,
Expand Down
18 changes: 18 additions & 0 deletions auth_jwt/views/auth_jwt_validator_views.xml
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,24 @@
name="cookie_max_age"
attrs="{'invisible': [('cookie_enabled', '=', False)]}"
/>
<field
name="cookie_secure"
attrs="{'invisible': [('cookie_enabled', '=', False)]}"
/>
<field
name="renew_cookie_on_response"
attrs="{'invisible': [('cookie_enabled', '=', False)]}"
/>
<field
name="renew_cookie_secret"
attrs="{'invisible': ['|', ('cookie_enabled', '=', False), ('renew_cookie_on_response', '=', False)],
'required': [('cookie_enabled', '=', True), ('renew_cookie_on_response', '=', True)]}"
/>
<field
name="renew_cookie_algorithm"
attrs="{'invisible': ['|', ('cookie_enabled', '=', False), ('renew_cookie_on_response', '=', False)],
'required': [('cookie_enabled', '=', True), ('renew_cookie_on_response', '=', True)]}"
/>
</group>
</group>
</sheet>
Expand Down
2 changes: 2 additions & 0 deletions auth_jwt_demo/demo/auth_jwt_validator.xml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@
<field name="partner_id_required" eval="False" />
<field name="cookie_enabled" eval="True" />
<field name="cookie_name">demo_auth</field>
<field name="renew_cookie_on_response" eval="True" />
<field name="renew_cookie_secret">renew_cookie_secret</field>
</record>
<record id="demo_keycloak_validator" model="auth.jwt.validator">
<field name="name">demo_keycloak</field>
Expand Down