diff --git a/CHANGELOG.md b/CHANGELOG.md index 09ebb87cd..3f1f6816c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,19 @@ ## Unreleased +## Version 5.2.2 + +Major security release + +* Revert #605 in https://github.com/jazzband/djangorestframework-simplejwt/pull/629 +* Fix typo in blacklist_app.rst by @cbscsm in https://github.com/jazzband/djangorestframework-simplejwt/pull/593 + +## Version 5.2.1 + +* Add Swedish translations by @PasinduPrabhashitha in https://github.com/jazzband/djangorestframework-simplejwt/pull/579 +* Fixed issue #543 by @armenak-baburyan in https://github.com/jazzband/djangorestframework-simplejwt/pull/586 +* Fix uncaught exception with JWK by @jerr0328 in https://github.com/jazzband/djangorestframework-simplejwt/pull/600 +* Test on Django 4.1 by @2ykwang in https://github.com/jazzband/djangorestframework-simplejwt/pull/604 + ## Version 5.2.0 * Remove the JWTTokenUserAuthentication from the Experimental Features #546 by @byrpatrick in https://github.com/jazzband/djangorestframework-simplejwt/pull/547 diff --git a/docs/experimental_features.md b/docs/experimental_features.md deleted file mode 100644 index 937d6de28..000000000 --- a/docs/experimental_features.md +++ /dev/null @@ -1,23 +0,0 @@ - -The `JWTTokenUserAuth` backend\'s `authenticate` method does -not perform a database lookup to obtain a user instance. Instead, it -returns a `ninja_jwt.models.TokenUser` instance which acts as a -stateless user object backed only by a validated token instead of a -record in a database. This can facilitate developing single sign-on -functionality between separately hosted Django apps which all share the -same token secret key. To use this feature, add the -`ninja_jwt.authentication.JWTTokenUserAuth` backend (instead -of the default `JWTAuth` backend) to the Django Ninja Extra route definition - -```python -from ninja_extra import api_controller, route -from ninja_jwt.authentication import JWTTokenUserAuth - - -@api_controller -class MyController: - @route.get('/some-endpoint', auth=JWTTokenUserAuth()) - def some_endpoint(self): - pass - -``` diff --git a/docs/getting_started.md b/docs/getting_started.md index 10b2a64ef..8abacfb71 100644 --- a/docs/getting_started.md +++ b/docs/getting_started.md @@ -97,3 +97,19 @@ curl \ ... {"access":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX3BrIjoxLCJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiY29sZF9zdHVmZiI6IuKYgyIsImV4cCI6MTIzNTY3LCJqdGkiOiJjNzE4ZTVkNjgzZWQ0NTQyYTU0NWJkM2VmMGI0ZGQ0ZSJ9.ekxRxgb9OKmHkfy-zs1Ro_xs1eMLXiR17dIDBVxeT-w"} ``` + +Cryptographic Dependencies (Optional) +------------------------------------- + +If you are planning on encoding or decoding tokens using certain digital +signature algorithms (i.e. RSA and ECDSA; visit PyJWT for other algorithms), you will need to install the +cryptography_ library. This can be installed explicitly, or as a required +extra in the `django-ninja-jwt` requirement: + + pip install django-ninja-jwt[crypto] + + +The `django-ninja-jwt[crypto]` format is recommended in requirements +files in projects using `Ninja JWT`, as a separate `cryptography` requirement +line may later be mistaken for an unused requirement and removed. +[cryptography](https://cryptography.io) \ No newline at end of file diff --git a/docs/settings.md b/docs/settings.md index 657b6b790..436ad2f26 100644 --- a/docs/settings.md +++ b/docs/settings.md @@ -57,192 +57,194 @@ are valid. This `timedelta` value is added to the current UTC time during token generation to obtain the token's default `exp` claim value. -`ROTATE_REFRESH_TOKENS` -======================= +``BLACKLIST_AFTER_ROTATION`` +---------------------------- -When set to `True`, if a refresh token is submitted to the -`TokenRefreshView`, a new refresh token will be returned along with the -new access token. This new refresh token will be supplied via a -`refresh` key in the JSON response. New refresh tokens will have a -renewed expiration time which is determined by adding the timedelta in -the `REFRESH_TOKEN_LIFETIME` setting to the current time when the -request is made. If the blacklist app is in use and the -`BLACKLIST_AFTER_ROTATION` setting is set to `True`, refresh tokens -submitted to the refresh view will be added to the blacklist. - -`BLACKLIST_AFTER_ROTATION` -========================== - -When set to `True`, causes refresh tokens submitted to the -`TokenRefreshView` to be added to the blacklist if the blacklist app is -in use and the `ROTATE_REFRESH_TOKENS` setting is set to `True`. You -need to add `'ninja_jwt.token_blacklist',` to your `INSTALLED_APPS` in -the settings file to use this setting. +When set to ``True``, causes refresh tokens submitted to the +``TokenRefreshView`` to be added to the blacklist if the blacklist app is in +use and the ``ROTATE_REFRESH_TOKENS`` setting is set to ``True``. +You need to add ``'ninja_jwt.token_blacklist',`` to your +``INSTALLED_APPS`` in the settings file to use this setting. Learn more about `/blacklist_app`{.interpreted-text role="doc"}. `UPDATE_LAST_LOGIN` =================== -When set to `True`, last_login field in the auth_user table is updated -upon login (TokenObtainPairController). +When set to ``True``, last_login field in the auth_user table is updated upon +login (TokenObtainPairView). -> Warning: Updating last_login will dramatically increase the number of -> database transactions. People abusing the views could slow the server -> and this could be a security vulnerability. If you really want this, -> throttle the endpoint. + Warning: Updating last_login will dramatically increase the number of database + transactions. People abusing the views could slow the server and this could be + a security vulnerability. If you really want this, throttle the endpoint with + DRF at the very least. `ALGORITHM` =========== The algorithm from the PyJWT library which will be used to perform -signing/verification operations on tokens. To use symmetric HMAC signing -and verification, the following algorithms may be used: `'HS256'`, -`'HS384'`, `'HS512'`. When an HMAC algorithm is chosen, the -`SIGNING_KEY` setting will be used as both the signing key and the -verifying key. In that case, the `VERIFYING_KEY` setting will be -ignored. To use asymmetric RSA signing and verification, the following -algorithms may be used: `'RS256'`, `'RS384'`, `'RS512'`. When an RSA -algorithm is chosen, the `SIGNING_KEY` setting must be set to a string -that contains an RSA private key. Likewise, the `VERIFYING_KEY` setting -must be set to a string that contains an RSA public key. +signing/verification operations on tokens. To use symmetric HMAC signing and +verification, the following algorithms may be used: ``'HS256'``, ``'HS384'``, +``'HS512'``. When an HMAC algorithm is chosen, the ``SIGNING_KEY`` setting +will be used as both the signing key and the verifying key. In that case, the +``VERIFYING_KEY`` setting will be ignored. To use asymmetric RSA signing and +verification, the following algorithms may be used: ``'RS256'``, ``'RS384'``, +``'RS512'``. When an RSA algorithm is chosen, the ``SIGNING_KEY`` setting must +be set to a string that contains an RSA private key. Likewise, the +``VERIFYING_KEY`` setting must be set to a string that contains an RSA public +key. `SIGNING_KEY` ============= -The signing key that is used to sign the content of generated tokens. -For HMAC signing, this should be a random string with at least as many -bits of data as is required by the signing protocol. For RSA signing, -this should be a string that contains an RSA private key that is 2048 -bits or longer. Since Ninja JWT defaults to using 256-bit HMAC signing, -the `SIGNING_KEY` setting defaults to the value of the `SECRET_KEY` -setting for your django project. Although this is the most reasonable -default that Ninja JWT can provide, it is recommended that developers -change this setting to a value that is independent from the django -project secret key. This will make changing the signing key used for +The signing key that is used to sign the content of generated tokens. For HMAC +signing, this should be a random string with at least as many bits of data as +is required by the signing protocol. For RSA signing, this should be a string +that contains an RSA private key that is 2048 bits or longer. Since Simple JWT +defaults to using 256-bit HMAC signing, the ``SIGNING_KEY`` setting defaults to +the value of the ``SECRET_KEY`` setting for your django project. Although this +is the most reasonable default that Simple JWT can provide, it is recommended +that developers change this setting to a value that is independent from the +django project secret key. This will make changing the signing key used for tokens easier in the event that it is compromised. `VERIFYING_KEY` =============== -The verifying key which is used to verify the content of generated -tokens. If an HMAC algorithm has been specified by the `ALGORITHM` -setting, the `VERIFYING_KEY` setting will be ignored and the value of -the `SIGNING_KEY` setting will be used. If an RSA algorithm has been -specified by the `ALGORITHM` setting, the `VERIFYING_KEY` setting must -be set to a string that contains an RSA public key. +The verifying key which is used to verify the content of generated tokens. If +an HMAC algorithm has been specified by the ``ALGORITHM`` setting, the +``VERIFYING_KEY`` setting will be ignored and the value of the ``SIGNING_KEY`` +setting will be used. If an RSA algorithm has been specified by the +``ALGORITHM`` setting, the ``VERIFYING_KEY`` setting must be set to a string +that contains an RSA public key. `AUDIENCE` ========== -The audience claim to be included in generated tokens and/or validated -in decoded tokens. When set to `None`, this field is excluded from -tokens and is not validated. +The audience claim to be included in generated tokens and/or validated in +decoded tokens. When set to ``None``, this field is excluded from tokens and is +not validated. `ISSUER` ======== -The issuer claim to be included in generated tokens and/or validated in -decoded tokens. When set to `None`, this field is excluded from tokens -and is not validated. +The issuer claim to be included in generated tokens and/or validated in decoded +tokens. When set to ``None``, this field is excluded from tokens and is not +validated. -`JWK_URL` -========= -The JWK_URL is used to dynamically resolve the public keys needed to -verify the signing of tokens. When using Auth0 for example you might set -this to ''. When -set to `None`, this field is excluded from the token backend and is not -used during validation. +``JWK_URL`` +---------- -`LEEWAY` -======== +The JWK_URL is used to dynamically resolve the public keys needed to verify the +signing of tokens. When using Auth0 for example you might set this to +'https://yourdomain.auth0.com/.well-known/jwks.json'. When set to ``None``, +this field is excluded from the token backend and is not used during +validation. -Leeway is used to give some margin to the expiration time. This can be -an integer for seconds or a `datetime.timedelta`. Please reference - +``LEEWAY`` +---------- + +Leeway is used to give some margin to the expiration time. This can be an +integer for seconds or a ``datetime.timedelta``. Please reference +https://pyjwt.readthedocs.io/en/latest/usage.html#expiration-time-claim-exp for more information. -`USER_ID_FIELD` -=============== +``AUTH_HEADER_TYPES`` +--------------------- -The database field from the user model that will be included in -generated tokens to identify users. It is recommended that the value of -this setting specifies a field that does not normally change once its -initial value is chosen. For example, specifying a `username` or -`email` field would be a poor choice since an account's username or -email might change depending on how account management in a given -service is designed. This could allow a new account to be created with -an old username while an existing token is still valid which uses that -username as a user identifier. - -`USER_ID_CLAIM` -=============== +The authorization header type(s) that will be accepted for views that require +authentication. For example, a value of ``'Bearer'`` means that views +requiring authentication would look for a header with the following format: +``Authorization: Bearer ``. This setting may also contain a list or +tuple of possible header types (e.g. ``('Bearer', 'JWT')``). If a list or +tuple is used in this way, and authentication fails, the first item in the +collection will be used to build the "WWW-Authenticate" header in the response. -The claim in generated tokens which will be used to store user -identifiers. For example, a setting value of `'user_id'` would mean -generated tokens include a `user_id` claim that contains the user's -identifier. +``AUTH_HEADER_NAME`` +---------------------------- -`USER_AUTHENTICATION_RULE` -========================== +The authorization header name to be used for authentication. +The default is ``HTTP_AUTHORIZATION`` which will accept the +``Authorization`` header in the request. For example if you'd +like to use ``X_Access_Token`` in the header of your requests +please specify the ``AUTH_HEADER_NAME`` to be +``HTTP_X_ACCESS_TOKEN`` in your settings. -Callable to determine if the user is permitted to authenticate. This -rule is applied after a valid token is processed. The user object is -passed to the callable as an argument. The default rule is to check that -the `is_active` flag is still `True`. The callable must return a -boolean, `True` if authorized, `False` otherwise resulting in a 401 -status code. +``USER_ID_FIELD`` +----------------- -`AUTH_TOKEN_CLASSES` -==================== +The database field from the user model that will be included in generated +tokens to identify users. It is recommended that the value of this setting +specifies a field that does not normally change once its initial value is +chosen. For example, specifying a "username" or "email" field would be a poor +choice since an account's username or email might change depending on how +account management in a given service is designed. This could allow a new +account to be created with an old username while an existing token is still +valid which uses that username as a user identifier. -A list of dot paths to classes that specify the types of token that are -allowed to prove authentication. More about this in the `Token types` -section below. +``USER_ID_CLAIM`` +----------------- -`TOKEN_TYPE_CLAIM` -================== +The claim in generated tokens which will be used to store user identifiers. +For example, a setting value of ``'user_id'`` would mean generated tokens +include a "user_id" claim that contains the user's identifier. -The claim name that is used to store a token's type. More about this in -the `Token types` section below. +``USER_AUTHENTICATION_RULE`` +---------------------------- -`JTI_CLAIM` -=========== +Callable to determine if the user is permitted to authenticate. This rule +is applied after a valid token is processed. The user object is passed +to the callable as an argument. The default rule is to check that the ``is_active`` +flag is still ``True``. The callable must return a boolean, ``True`` if authorized, +``False`` otherwise resulting in a 401 status code. -The claim name that is used to store a token's unique identifier. This -identifier is used to identify revoked tokens in the blacklist app. It -may be necessary in some cases to use another claim besides the default -`jti` claim to store such a value. +``AUTH_TOKEN_CLASSES`` +---------------------- -`TOKEN_USER_CLASS` -================== +A list of dot paths to classes that specify the types of token that are allowed +to prove authentication. More about this in the "Token types" section below. -A stateless user object which is backed by a validated token. Used only -for the experimental JWTTokenUserAuthentication authentication backend. -The value is a dotted path to your subclass of -`rest_framework_simplejwt.models.TokenUser`, which also is the default. +``TOKEN_TYPE_CLAIM`` +-------------------- -`SLIDING_TOKEN_LIFETIME` -======================== +The claim name that is used to store a token's type. More about this in the +"Token types" section below. + +``JTI_CLAIM`` +------------- + +The claim name that is used to store a token's unique identifier. This +identifier is used to identify revoked tokens in the blacklist app. It may be +necessary in some cases to use another claim besides the default "jti" claim to +store such a value. + +``TOKEN_USER_CLASS`` +-------------------- + +A stateless user object which is backed by a validated token. Used only for +the JWTStatelessUserAuthentication authentication backend. The value +is a dotted path to your subclass of ``rest_framework_simplejwt.models.TokenUser``, +which also is the default. + +``SLIDING_TOKEN_LIFETIME`` +-------------------------- + +A ``datetime.timedelta`` object which specifies how long sliding tokens are +valid to prove authentication. This ``timedelta`` value is added to the +current UTC time during token generation to obtain the token's default "exp" +claim value. More about this in the "Sliding tokens" section below. -A `datetime.timedelta` object which specifies how long sliding tokens -are valid to prove authentication. This `timedelta` value is added to -the current UTC time during token generation to obtain the token's -default `exp` claim value. More about this in the `Sliding tokens` -section below. +``SLIDING_TOKEN_REFRESH_LIFETIME`` +---------------------------------- -`SLIDING_TOKEN_REFRESH_LIFETIME` -================================ +A ``datetime.timedelta`` object which specifies how long sliding tokens are +valid to be refreshed. This ``timedelta`` value is added to the current UTC +time during token generation to obtain the token's default "exp" claim value. +More about this in the "Sliding tokens" section below. -A `datetime.timedelta` object which specifies how long sliding tokens -are valid to be refreshed. This `timedelta` value is added to the -current UTC time during token generation to obtain the token's default -`exp` claim value. More about this in the `Sliding tokens` section -below. +``SLIDING_TOKEN_REFRESH_EXP_CLAIM`` +----------------------------------- -`SLIDING_TOKEN_REFRESH_EXP_CLAIM` -================================= +The claim name that is used to store the expiration time of a sliding token's +refresh period. More about this in the "Sliding tokens" section below. -The claim name that is used to store the expiration time of a sliding -token's refresh period. More about this in the `Sliding tokens` -section below. diff --git a/ninja_jwt/authentication.py b/ninja_jwt/authentication.py index c257ed00a..b07fe66e8 100644 --- a/ninja_jwt/authentication.py +++ b/ninja_jwt/authentication.py @@ -75,10 +75,10 @@ def authenticate(self, request: HttpRequest, token: str) -> Any: return self.jwt_authenticate(request, token) -class JWTTokenUserAuth(JWTBaseAuthentication, HttpBearer): +class JWTStatelessUserAuthentication(JWTBaseAuthentication, HttpBearer): """ - Experimental features - JWTTokenUserAuth backend + An authentication plugin that authenticates requests through a JSON web + token provided in a request header without performing a database lookup to obtain a user instance. """ def authenticate(self, request: HttpRequest, token: str) -> Any: @@ -97,6 +97,9 @@ def get_user(self, validated_token: Any) -> Type[AbstractUser]: return api_settings.TOKEN_USER_CLASS(validated_token) +JWTTokenUserAuth = JWTStatelessUserAuthentication + + def default_user_authentication_rule(user) -> bool: # Prior to Django 1.10, inactive users could be authenticated with the # default `ModelBackend`. As of Django 1.10, the `ModelBackend` diff --git a/ninja_jwt/backends.py b/ninja_jwt/backends.py index 691c32d61..1a6cd1b52 100644 --- a/ninja_jwt/backends.py +++ b/ninja_jwt/backends.py @@ -10,13 +10,13 @@ from .utils import format_lazy try: - from jwt import PyJWKClient + from jwt import PyJWKClient, PyJWKClientError JWK_CLIENT_AVAILABLE = True except ImportError: JWK_CLIENT_AVAILABLE = False -ALLOWED_ALGORITHMS = ( +ALLOWED_ALGORITHMS = { "HS256", "HS384", "HS512", @@ -26,7 +26,7 @@ "ES256", "ES384", "ES512", -) +} class TokenBackend: @@ -95,7 +95,10 @@ def get_verifying_key(self, token) -> bytes: return self.signing_key if self.jwks_client: - return self.jwks_client.get_signing_key_from_jwt(token).key + try: + return self.jwks_client.get_signing_key_from_jwt(token).key + except PyJWKClientError as ex: + raise TokenBackendError(_("Token is invalid or expired")) from ex return self.verifying_key @@ -144,5 +147,5 @@ def decode(self, token, verify=True) -> Dict[str, Any]: ) except InvalidAlgorithmError as ex: raise TokenBackendError(_("Invalid algorithm specified")) from ex - except InvalidTokenError: - raise TokenBackendError(_("Token is invalid or expired")) + except InvalidTokenError as ex: + raise TokenBackendError(_("Token is invalid or expired")) from ex diff --git a/ninja_jwt/locale/es/LC_MESSAGES/django.po b/ninja_jwt/locale/es/LC_MESSAGES/django.po index e8ef0214c..f9c328a04 100644 --- a/ninja_jwt/locale/es/LC_MESSAGES/django.po +++ b/ninja_jwt/locale/es/LC_MESSAGES/django.po @@ -46,14 +46,14 @@ msgid "" "Unrecognized type '{}', 'leeway' must be of type int, float or timedelta." msgstr "" -#: backends.py:147 -msgid "Invalid algorithm specified" -msgstr "Algoritmo especificado no válido" - -#: backends.py:149 exceptions.py:38 tokens.py:44 +#: backends.py:102 backends.py:152 exceptions.py:38 tokens.py:44 msgid "Token is invalid or expired" msgstr "El token es inválido o ha expirado" +#: backends.py:150 +msgid "Invalid algorithm specified" +msgstr "Algoritmo especificado no válido" + #: serializers.py:30 msgid "No active account found with the given credentials" msgstr "La combinación de credenciales no tiene una cuenta activa" diff --git a/ninja_jwt/locale/es_AR/LC_MESSAGES/django.po b/ninja_jwt/locale/es_AR/LC_MESSAGES/django.po index 266da6940..6bc063fb1 100644 --- a/ninja_jwt/locale/es_AR/LC_MESSAGES/django.po +++ b/ninja_jwt/locale/es_AR/LC_MESSAGES/django.po @@ -49,16 +49,17 @@ msgstr "" #: backends.py:88 msgid "" "Unrecognized type '{}', 'leeway' must be of type int, float or timedelta." -msgstr "" -#: backends.py:147 -msgid "Invalid algorithm specified" msgstr "" -#: backends.py:149 exceptions.py:38 tokens.py:44 +#: backends.py:102 backends.py:152 exceptions.py:38 tokens.py:44 msgid "Token is invalid or expired" msgstr "El token es inválido o ha expirado" +#: backends.py:150 +msgid "Invalid algorithm specified" +msgstr "" + #: serializers.py:30 msgid "No active account found with the given credentials" msgstr "" diff --git a/ninja_jwt/locale/es_CL/LC_MESSAGES/django.po b/ninja_jwt/locale/es_CL/LC_MESSAGES/django.po index ef08e9bb5..bf79a738b 100644 --- a/ninja_jwt/locale/es_CL/LC_MESSAGES/django.po +++ b/ninja_jwt/locale/es_CL/LC_MESSAGES/django.po @@ -46,14 +46,14 @@ msgid "" "Unrecognized type '{}', 'leeway' must be of type int, float or timedelta." msgstr "" -#: backends.py:147 -msgid "Invalid algorithm specified" -msgstr "" - -#: backends.py:149 exceptions.py:38 tokens.py:44 +#: backends.py:102 backends.py:152 exceptions.py:38 tokens.py:44 msgid "Token is invalid or expired" msgstr "Token inválido o expirado" +#: backends.py:150 +msgid "Invalid algorithm specified" +msgstr "" + #: serializers.py:30 msgid "No active account found with the given credentials" msgstr "" diff --git a/ninja_jwt/locale/fa_IR/LC_MESSAGES/django.po b/ninja_jwt/locale/fa_IR/LC_MESSAGES/django.po index 82406b0f0..f580d224d 100644 --- a/ninja_jwt/locale/fa_IR/LC_MESSAGES/django.po +++ b/ninja_jwt/locale/fa_IR/LC_MESSAGES/django.po @@ -42,16 +42,17 @@ msgstr "" #: backends.py:88 msgid "" "Unrecognized type '{}', 'leeway' must be of type int, float or timedelta." -msgstr "" -#: backends.py:147 -msgid "Invalid algorithm specified" msgstr "" -#: backends.py:149 exceptions.py:38 tokens.py:44 +#: backends.py:102 backends.py:152 exceptions.py:38 tokens.py:44 msgid "Token is invalid or expired" msgstr "توکن نامعتبر است یا منقضی شده است" +#: backends.py:150 +msgid "Invalid algorithm specified" +msgstr "" + #: serializers.py:30 msgid "No active account found with the given credentials" msgstr "هیچ اکانت فعالی برای اطلاعات داده شده یافت نشد" diff --git a/ninja_jwt/locale/fr/LC_MESSAGES/django.po b/ninja_jwt/locale/fr/LC_MESSAGES/django.po index 9f18fe37a..2205714da 100644 --- a/ninja_jwt/locale/fr/LC_MESSAGES/django.po +++ b/ninja_jwt/locale/fr/LC_MESSAGES/django.po @@ -46,14 +46,14 @@ msgid "" "Unrecognized type '{}', 'leeway' must be of type int, float or timedelta." msgstr "" -#: backends.py:147 -msgid "Invalid algorithm specified" -msgstr "L'algorithme spécifié est invalide" - -#: backends.py:149 exceptions.py:38 tokens.py:44 +#: backends.py:102 backends.py:152 exceptions.py:38 tokens.py:44 msgid "Token is invalid or expired" msgstr "Le jeton est invalide ou expiré" +#: backends.py:150 +msgid "Invalid algorithm specified" +msgstr "L'algorithme spécifié est invalide" + #: serializers.py:30 msgid "No active account found with the given credentials" msgstr "Aucun compte actif n'a été trouvé avec les identifiants fournis" diff --git a/ninja_jwt/locale/id_ID/LC_MESSAGES/django.po b/ninja_jwt/locale/id_ID/LC_MESSAGES/django.po index 1bf40310e..bdc61cc82 100644 --- a/ninja_jwt/locale/id_ID/LC_MESSAGES/django.po +++ b/ninja_jwt/locale/id_ID/LC_MESSAGES/django.po @@ -45,14 +45,14 @@ msgid "" "Unrecognized type '{}', 'leeway' must be of type int, float or timedelta." msgstr "" -#: backends.py:147 -msgid "Invalid algorithm specified" -msgstr "Algoritma yang ditentukan tidak valid" - -#: backends.py:149 exceptions.py:38 tokens.py:44 +#: backends.py:102 backends.py:152 exceptions.py:38 tokens.py:44 msgid "Token is invalid or expired" msgstr "Token tidak valid atau kedaluwarsa" +#: backends.py:150 +msgid "Invalid algorithm specified" +msgstr "Algoritma yang ditentukan tidak valid" + #: serializers.py:30 msgid "No active account found with the given credentials" msgstr "Tidak ada akun aktif yang ditemukan dengan kredensial yang diberikan" diff --git a/ninja_jwt/locale/it_IT/LC_MESSAGES/django.po b/ninja_jwt/locale/it_IT/LC_MESSAGES/django.po index 7cca6dff3..327ac04db 100644 --- a/ninja_jwt/locale/it_IT/LC_MESSAGES/django.po +++ b/ninja_jwt/locale/it_IT/LC_MESSAGES/django.po @@ -49,14 +49,14 @@ msgid "" "Unrecognized type '{}', 'leeway' must be of type int, float or timedelta." msgstr "" -#: backends.py:147 -msgid "Invalid algorithm specified" -msgstr "L'algoritmo specificato non è valido" - -#: backends.py:149 exceptions.py:38 tokens.py:44 +#: backends.py:102 backends.py:152 exceptions.py:38 tokens.py:44 msgid "Token is invalid or expired" msgstr "Il token non è valido o è scaduto" +#: backends.py:150 +msgid "Invalid algorithm specified" +msgstr "L'algoritmo specificato non è valido" + #: serializers.py:30 msgid "No active account found with the given credentials" msgstr "Nessun account attivo trovato con queste credenziali" diff --git a/ninja_jwt/locale/ko_KR/LC_MESSAGES/django.po b/ninja_jwt/locale/ko_KR/LC_MESSAGES/django.po index 49704c66a..0b6df170c 100644 --- a/ninja_jwt/locale/ko_KR/LC_MESSAGES/django.po +++ b/ninja_jwt/locale/ko_KR/LC_MESSAGES/django.po @@ -47,14 +47,14 @@ msgstr "" "알 수 없는 타입 '{}', 'leeway' 값은 반드시 int, float 또는 timedelta 타입이어" "야 합니다." -#: backends.py:147 -msgid "Invalid algorithm specified" -msgstr "잘못된 알고리즘이 지정되었습니다" - -#: backends.py:149 exceptions.py:38 tokens.py:44 +#: backends.py:102 backends.py:152 exceptions.py:38 tokens.py:44 msgid "Token is invalid or expired" msgstr "유효하지 않거나 만료된 토큰" +#: backends.py:150 +msgid "Invalid algorithm specified" +msgstr "잘못된 알고리즘이 지정되었습니다" + #: serializers.py:30 msgid "No active account found with the given credentials" msgstr "지정된 자격 증명에 해당하는 활성화된 사용자를 찾을 수 없습니다" diff --git a/ninja_jwt/locale/nl_NL/LC_MESSAGES/django.po b/ninja_jwt/locale/nl_NL/LC_MESSAGES/django.po index 47b91aa51..f07db53ed 100644 --- a/ninja_jwt/locale/nl_NL/LC_MESSAGES/django.po +++ b/ninja_jwt/locale/nl_NL/LC_MESSAGES/django.po @@ -43,16 +43,17 @@ msgstr "" #: backends.py:88 msgid "" "Unrecognized type '{}', 'leeway' must be of type int, float or timedelta." -msgstr "" -#: backends.py:147 -msgid "Invalid algorithm specified" msgstr "" -#: backends.py:149 exceptions.py:38 tokens.py:44 +#: backends.py:102 backends.py:152 exceptions.py:38 tokens.py:44 msgid "Token is invalid or expired" msgstr "Token is niet geldig of verlopen" +#: backends.py:150 +msgid "Invalid algorithm specified" +msgstr "" + #: serializers.py:30 msgid "No active account found with the given credentials" msgstr "Geen actief account gevonden voor deze gegevens" diff --git a/ninja_jwt/locale/pl_PL/LC_MESSAGES/django.po b/ninja_jwt/locale/pl_PL/LC_MESSAGES/django.po index b1f6160b5..fa295200c 100644 --- a/ninja_jwt/locale/pl_PL/LC_MESSAGES/django.po +++ b/ninja_jwt/locale/pl_PL/LC_MESSAGES/django.po @@ -42,16 +42,17 @@ msgstr "" #: backends.py:88 msgid "" "Unrecognized type '{}', 'leeway' must be of type int, float or timedelta." -msgstr "" -#: backends.py:147 -msgid "Invalid algorithm specified" msgstr "" -#: backends.py:149 exceptions.py:38 tokens.py:44 +#: backends.py:102 backends.py:152 exceptions.py:38 tokens.py:44 msgid "Token is invalid or expired" msgstr "Token jest niepoprawny lub wygasł" +#: backends.py:150 +msgid "Invalid algorithm specified" +msgstr "" + #: serializers.py:30 msgid "No active account found with the given credentials" msgstr "Nie znaleziono aktywnego konta dla podanych danych uwierzytelniających" diff --git a/ninja_jwt/locale/pt_BR/LC_MESSAGES/django.po b/ninja_jwt/locale/pt_BR/LC_MESSAGES/django.po index 1b6303241..0da814688 100644 --- a/ninja_jwt/locale/pt_BR/LC_MESSAGES/django.po +++ b/ninja_jwt/locale/pt_BR/LC_MESSAGES/django.po @@ -46,14 +46,14 @@ msgid "" "Unrecognized type '{}', 'leeway' must be of type int, float or timedelta." msgstr "" -#: backends.py:147 -msgid "Invalid algorithm specified" -msgstr "Algoritmo inválido especificado" - -#: backends.py:149 exceptions.py:38 tokens.py:44 +#: backends.py:102 backends.py:152 exceptions.py:38 tokens.py:44 msgid "Token is invalid or expired" msgstr "O token é inválido ou expirado" +#: backends.py:150 +msgid "Invalid algorithm specified" +msgstr "Algoritmo inválido especificado" + #: serializers.py:30 msgid "No active account found with the given credentials" msgstr "Usuário e/ou senha incorreto(s)" diff --git a/ninja_jwt/locale/ro/LC_MESSAGES/django.mo b/ninja_jwt/locale/ro/LC_MESSAGES/django.mo new file mode 100644 index 000000000..d87cddd11 Binary files /dev/null and b/ninja_jwt/locale/ro/LC_MESSAGES/django.mo differ diff --git a/ninja_jwt/locale/ro/LC_MESSAGES/django.po b/ninja_jwt/locale/ro/LC_MESSAGES/django.po new file mode 100644 index 000000000..3a8d5d036 --- /dev/null +++ b/ninja_jwt/locale/ro/LC_MESSAGES/django.po @@ -0,0 +1,117 @@ +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR Daniel Cuznetov , 2022. +msgid "" +msgstr "" +"Project-Id-Version: djangorestframework_simplejwt\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2022-07-13 10:45+0100\n" +"Last-Translator: Daniel Cuznetov \n" +"Language: ro\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=1; plural=0;\n" + +#: authentication.py:78 +msgid "Authorization header must contain two space-delimited values" +msgstr "" +"Header-ul(antetul) de autorizare trebuie să conțină două valori separate " +"prin spațiu" + +#: authentication.py:104 +msgid "Given token not valid for any token type" +msgstr "Tokenul dat nu este valid pentru niciun tip de token" + +#: authentication.py:116 authentication.py:143 +msgid "Token contained no recognizable user identification" +msgstr "Tokenul nu conține date de identificare a utilizatorului" + +#: authentication.py:121 +msgid "User not found" +msgstr "Utilizatorul nu a fost găsit" + +#: authentication.py:124 +msgid "User is inactive" +msgstr "Utilizatorul este inactiv" + +#: backends.py:67 +msgid "Unrecognized algorithm type '{}'" +msgstr "Tipul de algoritm '{}' nu este recunoscut" + +#: backends.py:73 +msgid "You must have cryptography installed to use {}." +msgstr "Trebuie să aveți instalată criptografia pentru a utiliza {}." + +#: backends.py:88 +msgid "" +"Unrecognized type '{}', 'leeway' must be of type int, float or timedelta." +msgstr "" +"Tipul '{}' nu este recunoscut, 'leeway' trebuie să fie de tip int, float sau " +"timedelta." + +#: backends.py:102 backends.py:152 exceptions.py:38 tokens.py:44 +msgid "Token is invalid or expired" +msgstr "Token nu este valid sau a expirat" + +#: backends.py:150 +msgid "Invalid algorithm specified" +msgstr "Algoritm nevalid specificat" + +#: serializers.py:30 +msgid "No active account found with the given credentials" +msgstr "Nu a fost găsit cont activ cu aceste date de autentificare" + +#: settings.py:70 +msgid "" +"The '{}' setting has been removed. Please refer to '{}' for available " +"settings." +msgstr "" +"Setarea '{}' a fost ștearsă. Referați la '{}' pentru setări disponibile." + +#: token_blacklist/admin.py:68 +msgid "jti" +msgstr "jti" + +#: token_blacklist/admin.py:74 +msgid "user" +msgstr "utilizator" + +#: token_blacklist/admin.py:80 +msgid "created at" +msgstr "creat la" + +#: token_blacklist/admin.py:86 +msgid "expires at" +msgstr "expiră la" + +#: token_blacklist/apps.py:7 +msgid "Token Blacklist" +msgstr "Listă de token-uri blocate" + +#: tokens.py:30 +msgid "Cannot create token with no type or lifetime" +msgstr "Nu se poate crea token fără tip sau durată de viață" + +#: tokens.py:102 +msgid "Token has no id" +msgstr "Tokenul nu are id" + +#: tokens.py:115 +msgid "Token has no type" +msgstr "Tokenul nu are tip" + +#: tokens.py:118 +msgid "Token has wrong type" +msgstr "Tokenul are tipul greșit" + +#: tokens.py:170 +msgid "Token has no '{}' claim" +msgstr "Tokenul nu are reclamația '{}'" + +#: tokens.py:175 +msgid "Token '{}' claim has expired" +msgstr "Reclamația tokenului '{}' a expirat" + +#: tokens.py:230 +msgid "Token is blacklisted" +msgstr "Tokenul este în listă de tokenuri blocate" diff --git a/ninja_jwt/locale/ru_RU/LC_MESSAGES/django.po b/ninja_jwt/locale/ru_RU/LC_MESSAGES/django.po index e7f5df55e..554b33ed0 100644 --- a/ninja_jwt/locale/ru_RU/LC_MESSAGES/django.po +++ b/ninja_jwt/locale/ru_RU/LC_MESSAGES/django.po @@ -50,14 +50,14 @@ msgid "" "Unrecognized type '{}', 'leeway' must be of type int, float or timedelta." msgstr "" -#: backends.py:147 -msgid "Invalid algorithm specified" -msgstr "" - -#: backends.py:149 exceptions.py:38 tokens.py:44 +#: backends.py:102 backends.py:152 exceptions.py:38 tokens.py:44 msgid "Token is invalid or expired" msgstr "Токен недействителен или просрочен" +#: backends.py:150 +msgid "Invalid algorithm specified" +msgstr "" + #: serializers.py:30 msgid "No active account found with the given credentials" msgstr "Не найдено активной учетной записи с указанными данными" diff --git a/ninja_jwt/locale/sv/LC_MESSAGES/django.mo b/ninja_jwt/locale/sv/LC_MESSAGES/django.mo index 5370a8829..5c75a354d 100644 Binary files a/ninja_jwt/locale/sv/LC_MESSAGES/django.mo and b/ninja_jwt/locale/sv/LC_MESSAGES/django.mo differ diff --git a/ninja_jwt/locale/sv/LC_MESSAGES/django.po b/ninja_jwt/locale/sv/LC_MESSAGES/django.po index d9e754e92..6e12ed405 100644 --- a/ninja_jwt/locale/sv/LC_MESSAGES/django.po +++ b/ninja_jwt/locale/sv/LC_MESSAGES/django.po @@ -14,8 +14,7 @@ msgstr "" #: authentication.py:78 msgid "Authorization header must contain two space-delimited values" -msgstr "" -"Auktoriseringshuvudet måste innehålla två mellanslagsavgränsade värden" +msgstr "Auktoriseringshuvudet måste innehålla två mellanslagsavgränsade värden" #: authentication.py:104 msgid "Given token not valid for any token type" @@ -46,14 +45,14 @@ msgid "" "Unrecognized type '{}', 'leeway' must be of type int, float or timedelta." msgstr "" -#: backends.py:147 -msgid "Invalid algorithm specified" -msgstr "Ogiltig algoritm har angetts" - -#: backends.py:149 exceptions.py:38 tokens.py:44 +#: backends.py:102 backends.py:152 exceptions.py:38 tokens.py:44 msgid "Token is invalid or expired" msgstr "Token är ogiltig eller har löpt ut" +#: backends.py:150 +msgid "Invalid algorithm specified" +msgstr "Ogiltig algoritm har angetts" + #: serializers.py:30 msgid "No active account found with the given credentials" msgstr "Inget aktivt konto hittades med de angivna användaruppgifterna" @@ -63,8 +62,7 @@ msgid "" "The '{}' setting has been removed. Please refer to '{}' for available " "settings." msgstr "" -"Inställningen '{}' har tagits bort. Se '{}' för tillgängliga " -"inställningar" +"Inställningen '{}' har tagits bort. Se '{}' för tillgängliga inställningar" #: token_blacklist/admin.py:68 msgid "jti" diff --git a/ninja_jwt/settings.py b/ninja_jwt/settings.py index bf091514e..3a9cd7b56 100644 --- a/ninja_jwt/settings.py +++ b/ninja_jwt/settings.py @@ -58,8 +58,8 @@ class Config: TOKEN_USER_CLASS: Any = Field("ninja_jwt.models.TokenUser") AUTH_TOKEN_CLASSES: List[Any] = Field(["ninja_jwt.tokens.AccessToken"]) JSON_ENCODER: Optional[Any] = Field(None) - TOKEN_TYPE_CLAIM: str = Field("token_type") - JTI_CLAIM: str = Field("jti") + TOKEN_TYPE_CLAIM: Optional[str] = Field("token_type") + JTI_CLAIM: Optional[str] = Field("jti") SLIDING_TOKEN_REFRESH_EXP_CLAIM: str = Field("refresh_exp") SLIDING_TOKEN_LIFETIME: timedelta = Field(timedelta(minutes=5)) SLIDING_TOKEN_REFRESH_LIFETIME: timedelta = Field(timedelta(days=1)) diff --git a/pyproject.toml b/pyproject.toml index b01f91032..7e6901ff2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -63,6 +63,7 @@ Source = "https://github.com/eadwinCode/django-ninja-jwt" [project.optional-dependencies] test = [ + "cryptography", "pytest", "pytest-cov", "pytest-django", @@ -74,6 +75,11 @@ test = [ "python-jose==3.3.0", "click==8.0.4" ] + +crypto = [ + "cryptography>=3.3.1", +] + doc = [ "mkdocs", "mkdocs-material", diff --git a/tests/test_backends.py b/tests/test_backends.py index 98783876b..b70aa6757 100644 --- a/tests/test_backends.py +++ b/tests/test_backends.py @@ -42,13 +42,7 @@ def default(self, obj): class TestTokenBackend: - backends = ( - TokenBackend("HS256", SECRET), - TokenBackend("RS256", PRIVATE_KEY, PUBLIC_KEY), - TokenBackend("ES256", ES256_PRIVATE_KEY, ES256_PUBLIC_KEY), - TokenBackend("ES384", ES256_PRIVATE_KEY, ES256_PUBLIC_KEY), - TokenBackend("ES512", ES256_PRIVATE_KEY, ES256_PUBLIC_KEY), - ) + backends = () @pytest.fixture(autouse=True) def setUp(self): @@ -59,6 +53,13 @@ def setUp(self): "RS256", PRIVATE_KEY, PUBLIC_KEY, AUDIENCE, ISSUER ) self.payload = {"foo": "bar"} + self.backends = ( + self.hmac_token_backend, + self.rsa_token_backend, + TokenBackend("ES256", ES256_PRIVATE_KEY, ES256_PUBLIC_KEY), + TokenBackend("ES384", ES256_PRIVATE_KEY, ES256_PUBLIC_KEY), + TokenBackend("ES512", ES256_PRIVATE_KEY, ES256_PUBLIC_KEY), + ) def test_init(self): # Should reject unknown algorithms @@ -252,18 +253,6 @@ def test_decode_success(self, title, backend): assert backend.decode(token) == payload - def test_decode_rsa_with_no_expiry(self): - no_exp_token = jwt.encode(self.payload, PRIVATE_KEY, algorithm="RS256") - - self.rsa_token_backend.decode(no_exp_token) - - def test_decode_rsa_with_no_expiry_no_verify(self): - no_exp_token = jwt.encode(self.payload, PRIVATE_KEY, algorithm="RS256") - - assert ( - self.hmac_token_backend.decode(no_exp_token, verify=False) == self.payload - ) - def test_decode_aud_iss_success(self): self.payload["exp"] = aware_utcnow() + timedelta(days=1) self.payload["foo"] = "baz" @@ -313,6 +302,39 @@ def test_decode_rsa_aud_iss_jwk_success(self): assert jwk_token_backend.decode(token) == self.payload + @pytest.mark.skipif( + not JWK_CLIENT_AVAILABLE, + reason="PyJWT 1.7.1 doesn't have JWK client", + ) + def test_decode_jwk_missing_key_raises_tokenbackenderror(self): + self.payload["exp"] = aware_utcnow() + timedelta(days=1) + self.payload["foo"] = "baz" + self.payload["aud"] = AUDIENCE + self.payload["iss"] = ISSUER + + token = jwt.encode( + self.payload, + PRIVATE_KEY_2, + algorithm="RS256", + headers={"kid": "230498151c214b788dd97f22b85410a5"}, + ) + + with patch("ninja_jwt.backends.PyJWKClient") as mock_jwk_module: + mock_jwk_client = mock.MagicMock() + + mock_jwk_module.return_value = mock_jwk_client + mock_jwk_client.get_signing_key_from_jwt.side_effect = jwt.PyJWKClientError( + "Unable to find a signing key that matches" + ) + + # Note the PRIV,PUB care is intentially the original pairing + jwk_token_backend = TokenBackend( + "RS256", PRIVATE_KEY, PUBLIC_KEY, AUDIENCE, ISSUER, JWK_URL + ) + + with pytest.raises(TokenBackendError, match="Token is invalid or expired"): + jwk_token_backend.decode(token) + def test_decode_when_algorithm_not_available(self): token = jwt.encode(self.payload, PRIVATE_KEY, algorithm="RS256") if IS_OLD_JWT: diff --git a/tests/test_token_blacklist.py b/tests/test_token_blacklist.py index 649c37b63..07fb8aafc 100644 --- a/tests/test_token_blacklist.py +++ b/tests/test_token_blacklist.py @@ -4,7 +4,6 @@ from django.contrib.auth.models import User from django.core.management import call_command from django.db.models import BigAutoField -from django.test import TestCase from ninja_jwt.exceptions import TokenError from ninja_jwt.schema import TokenVerifySerializer @@ -13,7 +12,7 @@ from ninja_jwt.tokens import AccessToken, RefreshToken, SlidingToken from ninja_jwt.utils import aware_utcnow, datetime_from_epoch -from .utils import MigrationTestCase, override_api_settings +from .utils import MigrationTestCase @pytest.mark.django_db @@ -176,6 +175,29 @@ def test_it_should_delete_any_expired_tokens(self): not_expired_3["jti"], ] + def test_token_blacklist_will_not_be_removed_on_User_delete(self): + token = RefreshToken.for_user(self.user) + outstanding_token = OutstandingToken.objects.first() + + # Should raise no exception + RefreshToken(str(token)) + + # Add token to blacklist + BlacklistedToken.objects.create(token=outstanding_token) + + with pytest.raises(TokenError) as e: + # Should raise exception + RefreshToken(str(token)) + assert "blacklisted" in e.exception.args[0] + + # Delete the User and try again + self.user.delete() + + with pytest.raises(TokenError) as e: + # Should raise exception + RefreshToken(str(token)) + assert "blacklisted" in e.exception.args[0] + @pytest.mark.django_db class TestTokenVerifySerializerShouldHonourBlacklist(MigrationTestCase): diff --git a/tests/test_tokens.py b/tests/test_tokens.py index 5142d15ac..04c97f136 100644 --- a/tests/test_tokens.py +++ b/tests/test_tokens.py @@ -218,6 +218,18 @@ def test_set_jti(self): assert "jti" in token assert old_jti != token["jti"] + def test_optional_jti(self, monkeypatch): + with monkeypatch.context() as m: + m.setattr(api_settings, "JTI_CLAIM", None) + token = MyToken() + assert "jti" not in token + + def test_optional_type_token(self, monkeypatch): + with monkeypatch.context() as m: + m.setattr(api_settings, "TOKEN_TYPE_CLAIM", None) + token = MyToken() + assert "type" not in token + def test_set_exp(self): now = make_utc(datetime(year=2000, month=1, day=1)) @@ -333,20 +345,6 @@ def test_check_exp(self): "refresh_exp", current_time=current_time + timedelta(days=2) ) - def test_check_token_not_expired_if_in_leeway(self): - token = MyToken() - token.set_exp("refresh_exp", lifetime=timedelta(days=1)) - - datetime_in_leeway = token.current_time + timedelta(days=1) - - with pytest.raises(TokenError): - token.check_exp("refresh_exp", current_time=datetime_in_leeway) - - # a token 1 day expired is valid if leeway is 2 days - token.token_backend.leeway = timedelta(days=2).total_seconds() - token.check_exp("refresh_exp", current_time=datetime_in_leeway) - token.token_backend.leeway = 0 - @pytest.mark.django_db def test_for_user(self, monkeypatch): username = "test_user" diff --git a/tests/test_views.py b/tests/test_views.py index 96a43bc29..28f70138c 100644 --- a/tests/test_views.py +++ b/tests/test_views.py @@ -490,6 +490,5 @@ def test_it_should_return_401_if_token_is_blacklisted(self): ) # make sure other tests are not affected del self.view_name - assert res.status_code == 401 assert res.data["code"] == "token_not_valid"