From eb8e330b0cc3dc1e8150eca651ecfced1cccb5e3 Mon Sep 17 00:00:00 2001 From: "Travis A. Everett" Date: Fri, 1 Mar 2019 17:25:14 -0600 Subject: [PATCH] relax config type check on JWT_*_TOKEN_EXPIRES Instead of requiring JWT_ACCESS_TOKEN_EXPIRES and JWT_REFRESH_TOKEN_EXPIRES to be of type `datetime.timedelta` or `False`, checks in config.py support any value that can be successfully added to a `datetime.datetime` object. --- docs/options.rst | 8 +- flask_jwt_extended/config.py | 186 ++++++++++++++++++++--------------- 2 files changed, 113 insertions(+), 81 deletions(-) diff --git a/docs/options.rst b/docs/options.rst index 754769cb..3fe7d6cd 100644 --- a/docs/options.rst +++ b/docs/options.rst @@ -20,10 +20,14 @@ General Options: in a sequence or a set to check more then one location, such as: ``('headers', 'cookies')``. Defaults to ``['headers']`` ``JWT_ACCESS_TOKEN_EXPIRES`` How long an access token should live before it expires. This - takes a ``datetime.timedelta`` or an ``int`` (seconds), and defaults to 15 minutes. + takes any value that can be safely added to a ``datetime.datetime`` object, including + ``datetime.timedelta``, `dateutil.relativedelta `_, + or an ``int`` (seconds), and defaults to 15 minutes. Can be set to ``False`` to disable expiration. ``JWT_REFRESH_TOKEN_EXPIRES`` How long a refresh token should live before it expires. This - takes a ``datetime.timedelta`` or ``int`` (seconds), and defaults to 30 days. + takes any value that can be safely added to a ``datetime.datetime`` object, including + ``datetime.timedelta``, `dateutil.relativedelta `_, + or an ``int`` (seconds), and defaults to 30 days. Can be set to ``False`` to disable expiration. ``JWT_ALGORITHM`` Which algorithm to sign the JWT with. `See here `_ for the options. Defaults to ``'HS256'``. diff --git a/flask_jwt_extended/config.py b/flask_jwt_extended/config.py index 2b2d36b6..85332ead 100644 --- a/flask_jwt_extended/config.py +++ b/flask_jwt_extended/config.py @@ -18,8 +18,18 @@ try: from jwt.algorithms import requires_cryptography except ImportError: # pragma: no cover - requires_cryptography = {'RS256', 'RS384', 'RS512', 'ES256', 'ES384', - 'ES521', 'ES512', 'PS256', 'PS384', 'PS512'} + requires_cryptography = { + "RS256", + "RS384", + "RS512", + "ES256", + "ES384", + "ES521", + "ES512", + "PS256", + "PS384", + "PS512", + } class _Config(object): @@ -47,124 +57,128 @@ def decode_key(self): @property def token_location(self): - locations = current_app.config['JWT_TOKEN_LOCATION'] + locations = current_app.config["JWT_TOKEN_LOCATION"] if isinstance(locations, str): locations = (locations,) elif not isinstance(locations, (Sequence, Set)): - raise RuntimeError('JWT_TOKEN_LOCATION must be a sequence or a set') + raise RuntimeError("JWT_TOKEN_LOCATION must be a sequence or a set") elif not locations: - raise RuntimeError('JWT_TOKEN_LOCATION must contain at least one ' - 'of "headers", "cookies", "query_string", or "json"') + raise RuntimeError( + "JWT_TOKEN_LOCATION must contain at least one " + 'of "headers", "cookies", "query_string", or "json"' + ) for location in locations: - if location not in ('headers', 'cookies', 'query_string', 'json'): - raise RuntimeError('JWT_TOKEN_LOCATION can only contain ' - '"headers", "cookies", "query_string", or "json"') + if location not in ("headers", "cookies", "query_string", "json"): + raise RuntimeError( + "JWT_TOKEN_LOCATION can only contain " + '"headers", "cookies", "query_string", or "json"' + ) return locations @property def jwt_in_cookies(self): - return 'cookies' in self.token_location + return "cookies" in self.token_location @property def jwt_in_headers(self): - return 'headers' in self.token_location + return "headers" in self.token_location @property def jwt_in_query_string(self): - return 'query_string' in self.token_location + return "query_string" in self.token_location @property def jwt_in_json(self): - return 'json' in self.token_location + return "json" in self.token_location @property def header_name(self): - name = current_app.config['JWT_HEADER_NAME'] + name = current_app.config["JWT_HEADER_NAME"] if not name: raise RuntimeError("JWT_ACCESS_HEADER_NAME cannot be empty") return name @property def header_type(self): - return current_app.config['JWT_HEADER_TYPE'] + return current_app.config["JWT_HEADER_TYPE"] @property def query_string_name(self): - return current_app.config['JWT_QUERY_STRING_NAME'] + return current_app.config["JWT_QUERY_STRING_NAME"] @property def access_cookie_name(self): - return current_app.config['JWT_ACCESS_COOKIE_NAME'] + return current_app.config["JWT_ACCESS_COOKIE_NAME"] @property def refresh_cookie_name(self): - return current_app.config['JWT_REFRESH_COOKIE_NAME'] + return current_app.config["JWT_REFRESH_COOKIE_NAME"] @property def access_cookie_path(self): - return current_app.config['JWT_ACCESS_COOKIE_PATH'] + return current_app.config["JWT_ACCESS_COOKIE_PATH"] @property def refresh_cookie_path(self): - return current_app.config['JWT_REFRESH_COOKIE_PATH'] + return current_app.config["JWT_REFRESH_COOKIE_PATH"] @property def cookie_secure(self): - return current_app.config['JWT_COOKIE_SECURE'] + return current_app.config["JWT_COOKIE_SECURE"] @property def cookie_domain(self): - return current_app.config['JWT_COOKIE_DOMAIN'] + return current_app.config["JWT_COOKIE_DOMAIN"] @property def session_cookie(self): - return current_app.config['JWT_SESSION_COOKIE'] + return current_app.config["JWT_SESSION_COOKIE"] @property def cookie_samesite(self): - return current_app.config['JWT_COOKIE_SAMESITE'] + return current_app.config["JWT_COOKIE_SAMESITE"] @property def json_key(self): - return current_app.config['JWT_JSON_KEY'] + return current_app.config["JWT_JSON_KEY"] @property def refresh_json_key(self): - return current_app.config['JWT_REFRESH_JSON_KEY'] + return current_app.config["JWT_REFRESH_JSON_KEY"] @property def csrf_protect(self): - return self.jwt_in_cookies and current_app.config['JWT_COOKIE_CSRF_PROTECT'] + return self.jwt_in_cookies and current_app.config["JWT_COOKIE_CSRF_PROTECT"] @property def csrf_request_methods(self): - return current_app.config['JWT_CSRF_METHODS'] + return current_app.config["JWT_CSRF_METHODS"] @property def csrf_in_cookies(self): - return current_app.config['JWT_CSRF_IN_COOKIES'] + return current_app.config["JWT_CSRF_IN_COOKIES"] @property def access_csrf_cookie_name(self): - return current_app.config['JWT_ACCESS_CSRF_COOKIE_NAME'] + return current_app.config["JWT_ACCESS_CSRF_COOKIE_NAME"] @property def refresh_csrf_cookie_name(self): - return current_app.config['JWT_REFRESH_CSRF_COOKIE_NAME'] + return current_app.config["JWT_REFRESH_CSRF_COOKIE_NAME"] @property def access_csrf_cookie_path(self): - return current_app.config['JWT_ACCESS_CSRF_COOKIE_PATH'] + return current_app.config["JWT_ACCESS_CSRF_COOKIE_PATH"] @property def refresh_csrf_cookie_path(self): - return current_app.config['JWT_REFRESH_CSRF_COOKIE_PATH'] + return current_app.config["JWT_REFRESH_CSRF_COOKIE_PATH"] @staticmethod def _get_depreciated_csrf_header_name(): # This used to be the same option for access and refresh header names. # This gives users a warning if they are still using the old behavior - old_name = current_app.config.get('JWT_CSRF_HEADER_NAME', None) + old_name = current_app.config.get("JWT_CSRF_HEADER_NAME", None) if old_name: msg = ( "JWT_CSRF_HEADER_NAME is depreciated. Use JWT_ACCESS_CSRF_HEADER_NAME " @@ -175,92 +189,106 @@ def _get_depreciated_csrf_header_name(): @property def access_csrf_header_name(self): - return self._get_depreciated_csrf_header_name() or \ - current_app.config['JWT_ACCESS_CSRF_HEADER_NAME'] + return ( + self._get_depreciated_csrf_header_name() + or current_app.config["JWT_ACCESS_CSRF_HEADER_NAME"] + ) @property def refresh_csrf_header_name(self): - return self._get_depreciated_csrf_header_name() or \ - current_app.config['JWT_REFRESH_CSRF_HEADER_NAME'] + return ( + self._get_depreciated_csrf_header_name() + or current_app.config["JWT_REFRESH_CSRF_HEADER_NAME"] + ) @property def access_expires(self): - delta = current_app.config['JWT_ACCESS_TOKEN_EXPIRES'] - if type(delta) is int: - delta = datetime.timedelta(seconds=delta) - if not isinstance(delta, datetime.timedelta) and delta is not False: - err = 'JWT_ACCESS_TOKEN_EXPIRES must be a ' \ - 'datetime.timedelta, int or False' - raise RuntimeError(err) + delta = current_app.config["JWT_ACCESS_TOKEN_EXPIRES"] + if delta != False: + try: + delta + datetime.datetime.now() + except TypeError as e: + err = ( + "must be able to add JWT_ACCESS_TOKEN_EXPIRES to datetime.datetime" + ) + raise RuntimeError(err) from e return delta @property def refresh_expires(self): - delta = current_app.config['JWT_REFRESH_TOKEN_EXPIRES'] - if type(delta) is int: - delta = datetime.timedelta(seconds=delta) - if not isinstance(delta, datetime.timedelta) and delta is not False: - err = 'JWT_REFRESH_TOKEN_EXPIRES must be a ' \ - 'datetime.timedelta, int or False' - raise RuntimeError(err) + delta = current_app.config["JWT_REFRESH_TOKEN_EXPIRES"] + if delta != False: + try: + delta + datetime.datetime.now() + except TypeError as e: + err = ( + "must be able to add JWT_REFRESH_TOKEN_EXPIRES to datetime.datetime" + ) + raise RuntimeError(err) from e return delta @property def algorithm(self): - return current_app.config['JWT_ALGORITHM'] + return current_app.config["JWT_ALGORITHM"] @property def blacklist_enabled(self): - return current_app.config['JWT_BLACKLIST_ENABLED'] + return current_app.config["JWT_BLACKLIST_ENABLED"] @property def blacklist_checks(self): - check_type = current_app.config['JWT_BLACKLIST_TOKEN_CHECKS'] + check_type = current_app.config["JWT_BLACKLIST_TOKEN_CHECKS"] if isinstance(check_type, str): check_type = (check_type,) elif not isinstance(check_type, (Sequence, Set)): - raise RuntimeError('JWT_BLACKLIST_TOKEN_CHECKS must be a sequence or a set') + raise RuntimeError("JWT_BLACKLIST_TOKEN_CHECKS must be a sequence or a set") for item in check_type: - if item not in ('access', 'refresh'): + if item not in ("access", "refresh"): err = 'JWT_BLACKLIST_TOKEN_CHECKS must be "access" or "refresh"' raise RuntimeError(err) return check_type @property def blacklist_access_tokens(self): - return 'access' in self.blacklist_checks + return "access" in self.blacklist_checks @property def blacklist_refresh_tokens(self): - return 'refresh' in self.blacklist_checks + return "refresh" in self.blacklist_checks @property def _secret_key(self): - key = current_app.config['JWT_SECRET_KEY'] + key = current_app.config["JWT_SECRET_KEY"] if not key: - key = current_app.config.get('SECRET_KEY', None) + key = current_app.config.get("SECRET_KEY", None) if not key: - raise RuntimeError('JWT_SECRET_KEY or flask SECRET_KEY ' - 'must be set when using symmetric ' - 'algorithm "{}"'.format(self.algorithm)) + raise RuntimeError( + "JWT_SECRET_KEY or flask SECRET_KEY " + "must be set when using symmetric " + 'algorithm "{}"'.format(self.algorithm) + ) return key @property def _public_key(self): - key = current_app.config['JWT_PUBLIC_KEY'] + key = current_app.config["JWT_PUBLIC_KEY"] if not key: - raise RuntimeError('JWT_PUBLIC_KEY must be set to use ' - 'asymmetric cryptography algorithm ' - '"{}"'.format(self.algorithm)) + raise RuntimeError( + "JWT_PUBLIC_KEY must be set to use " + "asymmetric cryptography algorithm " + '"{}"'.format(self.algorithm) + ) return key @property def _private_key(self): - key = current_app.config['JWT_PRIVATE_KEY'] + key = current_app.config["JWT_PRIVATE_KEY"] if not key: - raise RuntimeError('JWT_PRIVATE_KEY must be set to use ' - 'asymmetric cryptography algorithm ' - '"{}"'.format(self.algorithm)) + raise RuntimeError( + "JWT_PRIVATE_KEY must be set to use " + "asymmetric cryptography algorithm " + '"{}"'.format(self.algorithm) + ) return key @property @@ -268,19 +296,19 @@ def cookie_max_age(self): # Returns the appropiate value for max_age for flask set_cookies. If # session cookie is true, return None, otherwise return a number of # seconds a long ways in the future - return None if self.session_cookie else 2147483647 # 2^31 + return None if self.session_cookie else 2_147_483_647 # 2^31 @property def identity_claim_key(self): - return current_app.config['JWT_IDENTITY_CLAIM'] + return current_app.config["JWT_IDENTITY_CLAIM"] @property def user_claims_key(self): - return current_app.config['JWT_USER_CLAIMS'] + return current_app.config["JWT_USER_CLAIMS"] @property def user_claims_in_refresh_token(self): - return current_app.config['JWT_CLAIMS_IN_REFRESH_TOKEN'] + return current_app.config["JWT_CLAIMS_IN_REFRESH_TOKEN"] @property def exempt_methods(self): @@ -288,7 +316,7 @@ def exempt_methods(self): @property def error_msg_key(self): - return current_app.config['JWT_ERROR_MESSAGE_KEY'] + return current_app.config["JWT_ERROR_MESSAGE_KEY"] @property def json_encoder(self): @@ -296,11 +324,11 @@ def json_encoder(self): @property def audience(self): - return current_app.config['JWT_DECODE_AUDIENCE'] + return current_app.config["JWT_DECODE_AUDIENCE"] @property def leeway(self): - return current_app.config['JWT_DECODE_LEEWAY'] + return current_app.config["JWT_DECODE_LEEWAY"] config = _Config()