exc-djangorestframework-api-key 1.0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,320 @@
1
+ Metadata-Version: 2.4
2
+ Name: exc_djangorestframework_api_key
3
+ Version: 1.0.1
4
+ Summary: API key permissions for the Django REST Framework
5
+ Author-email: Melih Sünbül <m.sunbul@excellence-cloud.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/ExcellenceCloudGmbH/djangorestframework-api-key
8
+ Project-URL: Documentation, https://florimondmanca.github.io/djangorestframework-api-key/
9
+ Classifier: Development Status :: 4 - Beta
10
+ Classifier: Operating System :: OS Independent
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Programming Language :: Python :: 3 :: Only
13
+ Classifier: Programming Language :: Python :: 3.8
14
+ Classifier: Programming Language :: Python :: 3.9
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Environment :: Web Environment
20
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
21
+ Classifier: Framework :: Django
22
+ Classifier: Framework :: Django :: 2.2
23
+ Classifier: Framework :: Django :: 3.2
24
+ Classifier: Framework :: Django :: 4.2
25
+ Classifier: Framework :: Django :: 5.2
26
+ Requires-Python: >=3.8
27
+ Description-Content-Type: text/markdown
28
+ License-File: LICENSE
29
+ Requires-Dist: packaging
30
+ Dynamic: license-file
31
+
32
+ # Django REST Framework API Key
33
+
34
+ API key permissions for the [Django REST Framework](https://www.django-rest-framework.org).
35
+
36
+ <div>
37
+ <a href="https://dev.azure.com/florimondmanca/public/_build/latest?definitionId=7&branchName=master">
38
+ <img src="https://dev.azure.com/florimondmanca/public/_apis/build/status/florimondmanca.djangorestframework-api-key?branchName=master" alt="build status"/>
39
+ </a>
40
+ <a href="https://codecov.io/gh/florimondmanca/djangorestframework-api-key">
41
+ <img src="https://codecov.io/gh/florimondmanca/djangorestframework-api-key/branch/master/graph/badge.svg" alt="coverage">
42
+ </a>
43
+ <a href="https://pypi.org/project/djangorestframework-api-key">
44
+ <img src="https://badge.fury.io/py/djangorestframework-api-key.svg" alt="package version"/>
45
+ </a>
46
+ </div>
47
+ <div>
48
+ <img src="https://img.shields.io/pypi/pyversions/djangorestframework-api-key.svg" alt="python versions"/>
49
+ <img src="https://img.shields.io/pypi/djversions/djangorestframework-api-key.svg?colorB=44b78b" alt="django versions"/>
50
+ <img src="https://img.shields.io/badge/drf-3.8+-7f2d2d.svg" alt="drf versions"/>
51
+ </div>
52
+
53
+ ## Introduction
54
+
55
+ **Django REST Framework API Key is a library for allowing server-side clients to safely use your API.** These clients are typically third-party backends and services (i.e. _machines_) which do not have a user account but still need to interact with your API in a secure way.
56
+
57
+ ### Features
58
+
59
+ - ✌️ **Simple to use**: create, view and revoke API keys via the admin site, or use built-in helpers to create API keys programmatically.
60
+ - 🔒 **As secure as possible**: API keys are treated with the same level of care as user passwords. They are only visible at creation and hashed before storing in the database.
61
+ - 🎨 **Customizable**: satisfy specific business requirements by building your own customized API key models, permission classes and admin panels.
62
+
63
+ ### Should I use API keys?
64
+
65
+ There are important security aspects you need to consider before switching to an API key access control scheme. We've listed some of these in [Security caveats](docs/security.md#caveats), including serving your API over HTTPS.
66
+
67
+ Besides, see [Why and when to use API keys](https://cloud.google.com/endpoints/docs/openapi/when-why-api-key#top_of_page) for hints on whether API keys can fit your use case.
68
+
69
+ API keys are ideal in the following situations:
70
+
71
+ - Blocking anonymous traffic.
72
+ - Implementing API key-based [throttling](https://www.django-rest-framework.org/api-guide/throttling/). (Note that Django REST Framework already has may built-in utilities for this use case.)
73
+ - Identifying usage patterns by logging request information along with the API key.
74
+
75
+ They can also present enough security for authorizing internal services, such as your API server and an internal frontend application.
76
+
77
+ > Please note that this package is NOT meant for authentication. You should NOT use this package to identify individual users, either directly or indirectly.
78
+ >
79
+ > If you need server-to-server authentication, you may want to consider OAuth instead. Libraries such as [django-oauth-toolkit](https://django-oauth-toolkit.readthedocs.io/en/latest/index.html) can help.
80
+
81
+ ## Quickstart
82
+
83
+ Install with `pip`:
84
+
85
+ ```bash
86
+ pip install "djangorestframework-api-key==3.*"
87
+ ```
88
+
89
+ _**Note**: It is highly recommended to **pin your dependency** to the latest major version (as depicted above), as breaking changes may and will happen between major releases._
90
+
91
+ Add the app to your `INSTALLED_APPS`:
92
+
93
+ ```python
94
+ # settings.py
95
+
96
+ INSTALLED_APPS = [
97
+ # ...
98
+ "rest_framework",
99
+ "rest_framework_api_key",
100
+ ]
101
+ ```
102
+
103
+ Run the included migrations:
104
+
105
+ ```bash
106
+ python manage.py migrate
107
+ ```
108
+
109
+ To learn how to configure permissions and manage API keys, head to the [Documentation](https://florimondmanca.github.io/djangorestframework-api-key).
110
+
111
+ ## Changelog
112
+
113
+ See [CHANGELOG.md](https://github.com/florimondmanca/djangorestframework-api-key/tree/master/CHANGELOG.md).
114
+
115
+ ## Contributing
116
+
117
+ See [CONTRIBUTING.md](https://github.com/florimondmanca/djangorestframework-api-key/tree/master/CONTRIBUTING.md).
118
+
119
+ ## License
120
+
121
+ MIT
122
+
123
+ # Changelog
124
+
125
+ All notable changes to this project will be documented in this file.
126
+
127
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
128
+
129
+ ## [fork] exc_djangorestframework_api_key 1.0.1 - 2026-10-25
130
+
131
+ ### Fixed
132
+
133
+ - Add the missing migration for the `APIKey.id` and `APIKey.hashed_key` column widths.
134
+ `models.py` has declared `max_length=150` on both since this fork squashed its migration
135
+ history into a single `0001_initial`, but that squashed migration still created them as
136
+ `varchar(100)`. The model and the schema therefore disagreed, and because
137
+ `KeyGenerator.hash()` returns a 136-character value (Django's default hasher via
138
+ `make_password`), `id` (145 characters) and `hashed_key` (136) both overflowed the real
139
+ column. Creating any API key against a database built from `0001` alone failed with
140
+ `value too long for type character varying(100)`.
141
+
142
+ `0002_alter_apikey_hashed_key_alter_apikey_id` is generated by `makemigrations` and brings
143
+ the schema up to the model. `makemigrations --check` is clean afterwards. No model, runtime
144
+ or API change; databases already created from this fork's source are unaffected apart from
145
+ the two columns widening.
146
+
147
+ ## 3.1.0 - 2025-04-04
148
+
149
+ ### Added
150
+
151
+ - Add official support for Python 3.12 and 3.13, and Django 5.0 up to 5.2, accomodating changes to `USE_TZ`. (Pull #266)
152
+
153
+ ## 3.0.0 - 2023-09-30
154
+
155
+ ### Changed
156
+
157
+ - Use faster SHA512-based key hasher instead of password hashers. Reduces server load by making API key validation orders of magnitude faster (10x to 30x according to estimations, network latency aside). Hashed key will be transparently upgraded the first time `.is_valid()` is called. (Pull #244, Pull #251)
158
+
159
+ ### Removed
160
+
161
+ - Dropped support for Python 3.7, which has reached EOL. (Pull #247)
162
+ - Drop redundant `.has_object_permission()` implementation on `BaseHasAPIKey` when using DRF 3.14.0 or above. (Pull #240)
163
+
164
+ ### Added
165
+
166
+ - Add official support for Python 3.11. (Pull #247)
167
+
168
+ ## 2.3.0 - 2023-01-19
169
+
170
+ ### Removed
171
+
172
+ - Drop support for Python 3.6, which has reached EOL. (Pull #210)
173
+
174
+ ### Fixed
175
+
176
+ - Fix migration 0004 when run against a non default database. (Pull #215)
177
+
178
+ ## 2.2.0 - 2022-03-11
179
+
180
+ ### Added
181
+
182
+ - Added support for Django config detection for different versions (PR #187)
183
+
184
+ ### Changed
185
+
186
+ - Add official support for Django 3.2 and Python 3.9 and 3.10 (PR #189)
187
+ - Bumped `hashed_key` field's `max_length` from 100 to 150 to address length issue with `argon2-cffi` (PR #193)
188
+
189
+ ## 2.1.0 - 2021-09-24
190
+
191
+ ### Added
192
+
193
+ - Add support for custom API `keyword`. (Pull #175)
194
+
195
+ ## 2.0.0 - 2020-04-07
196
+
197
+ **NOTE**: this release drops compatibility with certain Python and Django versions, but contains no other breaking changes. See [Upgrade to 2.0](https://florimondmanca.github.io/djangorestframework-api-key/upgrade/2.0/) for detailed migration steps.
198
+
199
+ ### Removed
200
+
201
+ - Dropped support for Django 2.0 and Django 2.1. (Pull #126)
202
+ - Dropped support for Python 3.5. (Pull #84)
203
+
204
+ ### Added
205
+
206
+ - Add support for Django 3.0. (Pull #82)
207
+ - Add support for Python 3.8. (Pull #81)
208
+ - Add `BaseAPIKeyManager.get_from_key()` to allow retrieving API keys from views. (Pull #93)
209
+ - Add type annotations, and partial support for `django-stubs` and `djangorestframework-stubs`. (Pull #88, Pull #122)
210
+
211
+ ## 1.4.1 - 2019-08-24
212
+
213
+ ### Added
214
+
215
+ - Now ships with type annotations ([PEP 561](https://www.python.org/dev/peps/pep-0561/)). (Pull #73)
216
+
217
+ ## 1.4.0 - 2019-07-16
218
+
219
+ **NOTE**: this release contains migrations. See [Upgrade to v1.4](https://florimondmanca.github.io/djangorestframework-api-key/upgrade/1.4/) for detailed instructions.
220
+
221
+ ### Added
222
+
223
+ - The `prefix` and `hashed_key` are now stored in dedicated fields on the `APIKey` model. (Pull #62)
224
+
225
+ ## 1.3.0 - 2019-06-28
226
+
227
+ **NOTE**: this release contains migrations. In your Django project, run them using:
228
+
229
+ ```python
230
+ python manage.py migrate rest_framework_api_key
231
+ ```
232
+
233
+ ### Added
234
+
235
+ - Add abstract API key model (`AbstractAPIKey`) and base manager (`BaseAPIKeyManager`). (Pull #36)
236
+ - Add base permissions (`BaseHasAPIKey`). (Pull #46)
237
+
238
+ ### Changed
239
+
240
+ - The `id` field of `APIKey` is now non-`editable`.
241
+ - `APIKeyModelAdmin` does not define `fieldsets` anymore. This allows subclasses to benefit from Django's automatic fieldsets. (Pull #52)
242
+
243
+ ### Fixed
244
+
245
+ - Explicitly use `utf-8` encoding in `setup.py`, which could previously lead to issues when installing on certain systems. (Pull #58)
246
+
247
+ ## 1.2.1 - 2019-06-03
248
+
249
+ ### Fixed
250
+
251
+ - Fixed a critical bug in `APIKeyModelAdmin` that prevented `rest_framework_api_key` from passing Django system checks. (Pull #39)
252
+
253
+ ## 1.2.0 - 2019-05-29
254
+
255
+ **NOTE**: this release contains migrations. In your Django project, run them using:
256
+
257
+ ```python
258
+ python manage.py migrate rest_framework_api_key
259
+ ```
260
+
261
+ ### Added
262
+
263
+ - API keys can now have an optional `expiry_date`. (Pull #33) `HasAPIKey` denies access if the API key has expired, i.e. if `expiry_date`, if set, is in the past.
264
+ - It is now possible to search by `prefix` in the API key admin panel.
265
+ - The `prefix` is now displayed in the edit view of the API key admin panel.
266
+
267
+ ## 1.1.0 - 2019-05-14
268
+
269
+ ### Added
270
+
271
+ - Improve documentation on which password hasher is used.
272
+ - Add tests against the Argon2, BcryptSHA256 and PBKDF2SHA1 hashers. (Pull #32)
273
+
274
+ ### Fixed
275
+
276
+ - Fix support for password hashers that generate hashes that contain dots. (Pull #31)
277
+
278
+ ## 1.0.0 - 2019-04-24
279
+
280
+ **This release is incompatible with 0.x**. See [Upgrade to 1.0](https://florimondmanca.github.io/djangorestframework-api-key/upgrade/1.4/) for migration steps.
281
+
282
+ ### Removed
283
+
284
+ - Remove `HasAPIKeyOrIsAuthenticated` permission class. You should use bitwise composition now, e.g. `HasAPIKey | IsAuthenticated`.
285
+ - Drop the `DRF_API_KEY_*` settings. (Pull #19)
286
+
287
+ ### Changed
288
+
289
+ - Switch to a new API key generation and validation scheme. Clients must now authorize using a single API key header (Pull #19). The header is `Authorization` by default. It can be customized using the `API_KEY_CUSTOM_HEADER` setting (Pull #26). Use the `name` field to identify clients.
290
+
291
+ ### Added
292
+
293
+ - Add support for Django 2.2. (Pull #27)
294
+ - Add programmatic API key creation using `APIKey.objects.create_key()`. (Pull #19)
295
+
296
+ ### Fixed
297
+
298
+ - Improved API key storage using Django's password hashing helpers. (Uses the default Django password hasher.) (Pull #19)
299
+
300
+ ## 0.4.0 - 2019-04-21
301
+
302
+ ### Removed
303
+
304
+ - Drop support for Python 3.4. Only 3.5, 3.6 and 3.7 are supported now.
305
+ - Drop support for Django < 2.0. Only 2.0 and 2.1 are supported now.
306
+
307
+ ### Fixed
308
+
309
+ - `HasAPIKey` now implements `.has_object_permissions()`, which allows to compose it with other permission classes and perform object-level permission checks. (Pull #25)
310
+
311
+ ## 0.3.1 - 2018-11-17
312
+
313
+ _Initial changelog entry._
314
+
315
+ ### Added
316
+
317
+ - `APIKey` model.
318
+ - `HasAPIKey` and `HasAPIKeyOrIsAuthenticated` permission classes.
319
+ - Generate, view and revoke API keys from the Django admin.
320
+ - Authenticate requests using the `Api-Token` and `Api-Secret-Key` headers. Customizable via the `DRF_API_KEY_TOKEN_HEADER` and `DRF_API_KEY_SECRET_KEY_HEADER` settings.
@@ -0,0 +1,15 @@
1
+ exc_djangorestframework_api_key-1.0.1.dist-info/licenses/LICENSE,sha256=nyXOn1Sbn1D5fSkFR5uhWjZQxGgEpsjK2gULbbNB8Jc,1072
2
+ rest_framework_api_key/__init__.py,sha256=aP4cXxNPfxDe5Cfu3fLOjslzKbIn0E7Aa9sl1-a78Hk,287
3
+ rest_framework_api_key/admin.py,sha256=vtqAAWEsjapUOup9PJujgQX1gBSWxTVcsVAWL8j5J3A,1631
4
+ rest_framework_api_key/apps.py,sha256=DyjUGi532pZQbM3WGK3pvnnrhgSzg9UGtj0izWDfTd4,157
5
+ rest_framework_api_key/crypto.py,sha256=J6wLNa_XOBdpExB6TzDQc3QKzwlSUqJ0cXNhWRi2N8M,2662
6
+ rest_framework_api_key/models.py,sha256=fXG_nlDsJwGnw0av7btlimWkXzN0Y5Eu7pwuk53HbVA,5062
7
+ rest_framework_api_key/permissions.py,sha256=9G4bAz5NsgUwxWN01SIld4fxNTj1GNjW7FOqNg5ymp8,2786
8
+ rest_framework_api_key/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
9
+ rest_framework_api_key/migrations/0001_initial.py,sha256=cF-AMHqa6yjabaXi71jfBSbgKJI5cdVcVUTCgnN9B9w,1398
10
+ rest_framework_api_key/migrations/0002_alter_apikey_hashed_key_alter_apikey_id.py,sha256=6JOcb9TdYamOzzD__BnrbxindpuMnT1xaDhNHCL2jlc,625
11
+ rest_framework_api_key/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
12
+ exc_djangorestframework_api_key-1.0.1.dist-info/METADATA,sha256=fmWhZya9kEz5SX1mLuW2_5y_j78WR4Dcu7V0ZunSh4E,12508
13
+ exc_djangorestframework_api_key-1.0.1.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
14
+ exc_djangorestframework_api_key-1.0.1.dist-info/top_level.txt,sha256=8kBfKhYyv2-t77c6-nIyrSe96btxb6p_uuoxgyAmphA,23
15
+ exc_djangorestframework_api_key-1.0.1.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.2.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2018 Florimond Manca
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ rest_framework_api_key
@@ -0,0 +1,11 @@
1
+ try:
2
+ import django
3
+ except ImportError: # pragma: no cover
4
+ pass
5
+ else:
6
+ if django.VERSION < (3, 2): # pragma: no cover
7
+ default_app_config = "rest_framework_api_key.apps.RestFrameworkApiKeyConfig"
8
+
9
+ __version__ = "1.0.1"
10
+
11
+ __all__ = ["__version__", "default_app_config"]
@@ -0,0 +1,61 @@
1
+ import typing
2
+
3
+ from django.contrib import admin, messages
4
+ from django.db import models
5
+ from django.http.request import HttpRequest
6
+ from django.utils.translation import gettext_lazy as _
7
+
8
+ from .models import AbstractAPIKey, APIKey
9
+
10
+
11
+ class APIKeyModelAdmin(admin.ModelAdmin):
12
+ model: typing.Type[AbstractAPIKey]
13
+
14
+ list_display = (
15
+ "prefix",
16
+ "name",
17
+ "created",
18
+ "expiry_date",
19
+ "_has_expired",
20
+ "revoked",
21
+ )
22
+ list_filter = ("created",)
23
+ search_fields = ("name", "prefix")
24
+
25
+ def get_readonly_fields(
26
+ self, request: HttpRequest, obj: models.Model = None
27
+ ) -> typing.Tuple[str, ...]:
28
+ obj = typing.cast(AbstractAPIKey, obj)
29
+ fields: typing.Tuple[str, ...]
30
+
31
+ fields = ("prefix",)
32
+ if obj is not None and obj.revoked:
33
+ fields = fields + ("name", "revoked", "expiry_date")
34
+
35
+ return fields
36
+
37
+ def save_model(
38
+ self,
39
+ request: HttpRequest,
40
+ obj: AbstractAPIKey,
41
+ form: typing.Any = None,
42
+ change: bool = False,
43
+ ) -> None:
44
+ created = not obj.pk
45
+
46
+ if created:
47
+ key = self.model.objects.assign_key(obj)
48
+ obj.save()
49
+ message = _(
50
+ "The API key for {} is: {}. ".format(obj.name, key)
51
+ + "Please store it somewhere safe: "
52
+ + "you will not be able to see it again."
53
+ )
54
+ messages.add_message(request, messages.WARNING, message)
55
+ else:
56
+ obj.save()
57
+
58
+
59
+ admin.site.register(APIKey, APIKeyModelAdmin)
60
+
61
+ APIKeyAdmin = APIKeyModelAdmin # Compatibility with <1.3
@@ -0,0 +1,6 @@
1
+ from django.apps import AppConfig
2
+
3
+
4
+ class RestFrameworkApiKeyConfig(AppConfig):
5
+ name = "rest_framework_api_key"
6
+ verbose_name = "API Key Permissions"
@@ -0,0 +1,82 @@
1
+ import hashlib
2
+ import typing
3
+
4
+ from django.contrib.auth.hashers import (
5
+ BasePasswordHasher,
6
+ check_password,
7
+ make_password,
8
+ )
9
+ from django.utils.crypto import constant_time_compare, get_random_string
10
+
11
+
12
+ def concatenate(left: str, right: str) -> str:
13
+ return "{}.{}".format(left, right)
14
+
15
+
16
+ def split(concatenated: str) -> typing.Tuple[str, str]:
17
+ left, _, right = concatenated.partition(".")
18
+ return left, right
19
+
20
+
21
+ class Sha512ApiKeyHasher(BasePasswordHasher):
22
+ """
23
+ An API key hasher using the sha512 algorithm.
24
+
25
+ This hasher should *NEVER* be used in Django's `PASSWORD_HASHERS` setting.
26
+ It is insecure for use in hashing passwords, but is safe for hashing
27
+ high entropy, randomly generated API keys.
28
+ """
29
+
30
+ algorithm = "sha512"
31
+
32
+ def salt(self) -> str:
33
+ """No need for a salt on a high entropy key."""
34
+ return ""
35
+
36
+ def encode(self, password: str, salt: str) -> str:
37
+ if salt != "":
38
+ raise ValueError("salt is unnecessary for high entropy API tokens.")
39
+ hash = hashlib.sha512(password.encode()).hexdigest()
40
+ return "%s$$%s" % (self.algorithm, hash)
41
+
42
+ def verify(self, password: str, encoded: str) -> bool:
43
+ encoded_2 = self.encode(password, "")
44
+ return constant_time_compare(encoded, encoded_2)
45
+
46
+
47
+ class KeyGenerator:
48
+ preferred_hasher = Sha512ApiKeyHasher()
49
+
50
+ def __init__(self, prefix_length: int = 8, secret_key_length: int = 32):
51
+ self.prefix_length = prefix_length
52
+ self.secret_key_length = secret_key_length
53
+
54
+ def get_prefix(self) -> str:
55
+ return get_random_string(self.prefix_length)
56
+
57
+ def get_secret_key(self) -> str:
58
+ return get_random_string(self.secret_key_length)
59
+
60
+ def hash(self, value: str) -> str:
61
+ return make_password(value, hasher=self.preferred_hasher)
62
+
63
+ def generate(self) -> typing.Tuple[str, str, str]:
64
+ prefix = self.get_prefix()
65
+ secret_key = self.get_secret_key()
66
+ key = concatenate(prefix, secret_key)
67
+ hashed_key = self.hash(key)
68
+ return key, prefix, hashed_key
69
+
70
+ def verify(self, key: str, hashed_key: str) -> bool:
71
+ if self.using_preferred_hasher(hashed_key):
72
+ # New simpler hasher
73
+ result = self.preferred_hasher.verify(key, hashed_key)
74
+ else:
75
+ # Slower password hashers from Django
76
+ # If verified, these will be transparently updated to the preferred hasher
77
+ result = check_password(key, hashed_key)
78
+
79
+ return result
80
+
81
+ def using_preferred_hasher(self, hashed_key: str) -> bool:
82
+ return hashed_key.startswith(f"{self.preferred_hasher.algorithm}$$")
@@ -0,0 +1,31 @@
1
+ # Generated by Django 3.0.14 on 2022-02-15 20:00
2
+
3
+ from django.db import migrations, models
4
+
5
+
6
+ class Migration(migrations.Migration):
7
+ initial = True
8
+
9
+ dependencies = [
10
+ ]
11
+
12
+ operations = [
13
+ migrations.CreateModel(
14
+ name='APIKey',
15
+ fields=[
16
+ ('id', models.CharField(editable=False, max_length=100, primary_key=True, serialize=False, unique=True)),
17
+ ('prefix', models.CharField(editable=False, max_length=8, unique=True)),
18
+ ('hashed_key', models.CharField(editable=False, max_length=100)),
19
+ ('created', models.DateTimeField(auto_now_add=True, db_index=True)),
20
+ ('name', models.CharField(default=None, help_text='A free-form name for the API key. Need not be unique. 50 characters max.', max_length=50)),
21
+ ('revoked', models.BooleanField(blank=True, default=False, help_text='If the API key is revoked, clients cannot use it anymore. (This cannot be undone.)')),
22
+ ('expiry_date', models.DateTimeField(blank=True, help_text='Once API key expires, clients cannot use it anymore.', null=True, verbose_name='Expires')),
23
+ ],
24
+ options={
25
+ 'verbose_name': 'API key',
26
+ 'verbose_name_plural': 'API keys',
27
+ 'ordering': ('-created',),
28
+ 'abstract': False,
29
+ },
30
+ ),
31
+ ]
@@ -0,0 +1,23 @@
1
+ # Generated by Django 6.0.8 on 2026-10-25 15:37
2
+
3
+ from django.db import migrations, models
4
+
5
+
6
+ class Migration(migrations.Migration):
7
+
8
+ dependencies = [
9
+ ('rest_framework_api_key', '0001_initial'),
10
+ ]
11
+
12
+ operations = [
13
+ migrations.AlterField(
14
+ model_name='apikey',
15
+ name='hashed_key',
16
+ field=models.CharField(editable=False, max_length=150),
17
+ ),
18
+ migrations.AlterField(
19
+ model_name='apikey',
20
+ name='id',
21
+ field=models.CharField(editable=False, max_length=150, primary_key=True, serialize=False, unique=True),
22
+ ),
23
+ ]
File without changes
@@ -0,0 +1,163 @@
1
+ import typing
2
+
3
+ from django.core.exceptions import ValidationError
4
+ from django.db import models
5
+ from django.utils import timezone
6
+ from django.utils.translation import gettext_lazy as _
7
+
8
+ from .crypto import KeyGenerator, concatenate, split
9
+
10
+
11
+ class BaseAPIKeyManager(models.Manager):
12
+ key_generator = KeyGenerator()
13
+
14
+ def assign_key(self, obj: "AbstractAPIKey") -> str:
15
+ try:
16
+ key, prefix, hashed_key = self.key_generator.generate()
17
+ except ValueError: # Compatibility with < 1.4
18
+ generate = typing.cast(
19
+ typing.Callable[[], typing.Tuple[str, str]], self.key_generator.generate
20
+ )
21
+ key, hashed_key = generate()
22
+ pk = hashed_key
23
+ prefix, hashed_key = split(hashed_key)
24
+ else:
25
+ pk = concatenate(prefix, hashed_key)
26
+
27
+ obj.id = pk
28
+ obj.prefix = prefix
29
+ obj.hashed_key = hashed_key
30
+
31
+ return key
32
+
33
+ def create_key(self, **kwargs: typing.Any) -> typing.Tuple["AbstractAPIKey", str]:
34
+ # Prevent from manually setting the primary key.
35
+ kwargs.pop("id", None)
36
+ obj = self.model(**kwargs)
37
+ key = self.assign_key(obj)
38
+ obj.save()
39
+ return obj, key
40
+
41
+ def get_usable_keys(self) -> models.QuerySet:
42
+ return self.filter(revoked=False)
43
+
44
+ def get_from_key(self, key: str) -> "AbstractAPIKey":
45
+ prefix, _, _ = key.partition(".")
46
+ queryset = self.get_usable_keys()
47
+
48
+ try:
49
+ api_key = queryset.get(prefix=prefix)
50
+ except self.model.DoesNotExist:
51
+ raise # For the sake of being explicit.
52
+
53
+ if not api_key.is_valid(key):
54
+ raise self.model.DoesNotExist("Key is not valid.")
55
+ else:
56
+ return api_key
57
+
58
+ def is_valid(self, key: str) -> bool:
59
+ try:
60
+ api_key = self.get_from_key(key)
61
+ except self.model.DoesNotExist:
62
+ return False
63
+
64
+ if api_key.has_expired:
65
+ return False
66
+
67
+ return True
68
+
69
+
70
+ class APIKeyManager(BaseAPIKeyManager):
71
+ pass
72
+
73
+
74
+ class AbstractAPIKey(models.Model):
75
+ objects = APIKeyManager()
76
+
77
+ id = models.CharField(max_length=150, unique=True, primary_key=True, editable=False)
78
+ prefix = models.CharField(max_length=8, unique=True, editable=False)
79
+ hashed_key = models.CharField(max_length=150, editable=False)
80
+ created = models.DateTimeField(auto_now_add=True, db_index=True)
81
+ name = models.CharField(
82
+ max_length=50,
83
+ blank=False,
84
+ default=None,
85
+ help_text=(
86
+ _(
87
+ "A free-form name for the API key. "
88
+ "Need not be unique. "
89
+ "50 characters max."
90
+ )
91
+ ),
92
+ )
93
+ revoked = models.BooleanField(
94
+ blank=True,
95
+ default=False,
96
+ help_text=(
97
+ _(
98
+ "If the API key is revoked, clients cannot use it anymore. "
99
+ "(This cannot be undone.)"
100
+ )
101
+ ),
102
+ )
103
+ expiry_date = models.DateTimeField(
104
+ blank=True,
105
+ null=True,
106
+ verbose_name=_("Expires"),
107
+ help_text=_("Once API key expires, clients cannot use it anymore."),
108
+ )
109
+
110
+ class Meta: # noqa
111
+ abstract = True
112
+ ordering = ("-created",)
113
+ verbose_name = "API key"
114
+ verbose_name_plural = "API keys"
115
+
116
+ def __init__(self, *args: typing.Any, **kwargs: typing.Any):
117
+ super().__init__(*args, **kwargs)
118
+ # Store the initial value of `revoked` to detect changes.
119
+ self._initial_revoked = self.revoked
120
+
121
+ def _has_expired(self) -> bool:
122
+ if self.expiry_date is None:
123
+ return False
124
+ return self.expiry_date < timezone.now()
125
+
126
+ _has_expired.short_description = "Has expired" # type: ignore
127
+ _has_expired.boolean = True # type: ignore
128
+ has_expired = property(_has_expired)
129
+
130
+ def is_valid(self, key: str) -> bool:
131
+ key_generator = type(self).objects.key_generator
132
+ valid = key_generator.verify(key, self.hashed_key)
133
+
134
+ # Transparently update the key to use the preferred hasher
135
+ # if it is using an outdated hasher.
136
+ if valid and not key_generator.using_preferred_hasher(self.hashed_key):
137
+ # Note that since the PK includes the hashed key,
138
+ # they will be internally inconsistent following this upgrade.
139
+ # See: https://github.com/florimondmanca/djangorestframework-api-key/issues/128
140
+ self.hashed_key = key_generator.hash(key)
141
+ self.save()
142
+
143
+ return valid
144
+
145
+ def clean(self) -> None:
146
+ self._validate_revoked()
147
+
148
+ def save(self, *args: typing.Any, **kwargs: typing.Any) -> None:
149
+ self._validate_revoked()
150
+ super().save(*args, **kwargs)
151
+
152
+ def _validate_revoked(self) -> None:
153
+ if self._initial_revoked and not self.revoked:
154
+ raise ValidationError(
155
+ _("The API key has been revoked, which cannot be undone.")
156
+ )
157
+
158
+ def __str__(self) -> str:
159
+ return str(self.name)
160
+
161
+
162
+ class APIKey(AbstractAPIKey):
163
+ pass
@@ -0,0 +1,81 @@
1
+ import typing
2
+
3
+ import packaging.version
4
+ from django.conf import settings
5
+ from django.http import HttpRequest
6
+ from rest_framework import __version__ as __drf_version__
7
+ from rest_framework import permissions
8
+
9
+ from .models import AbstractAPIKey, APIKey
10
+
11
+ _drf_version = packaging.version.parse(__drf_version__)
12
+ _3_14_0 = packaging.version.parse("3.14.0")
13
+
14
+
15
+ class KeyParser:
16
+ keyword = "Api-Key"
17
+
18
+ def get(self, request: HttpRequest) -> typing.Optional[str]:
19
+ custom_header = getattr(settings, "API_KEY_CUSTOM_HEADER", None)
20
+
21
+ if custom_header is not None:
22
+ return self.get_from_header(request, custom_header)
23
+
24
+ return self.get_from_authorization(request)
25
+
26
+ def get_from_authorization(self, request: HttpRequest) -> typing.Optional[str]:
27
+ authorization = request.META.get("HTTP_AUTHORIZATION", "")
28
+
29
+ if not authorization:
30
+ return None
31
+
32
+ keyword, found, key = authorization.partition(" ")
33
+
34
+ if not found:
35
+ return None
36
+
37
+ if keyword.lower() != self.keyword.lower():
38
+ return None
39
+
40
+ return key
41
+
42
+ def get_from_header(self, request: HttpRequest, name: str) -> typing.Optional[str]:
43
+ return request.META.get(name) or None
44
+
45
+
46
+ class BaseHasAPIKey(permissions.BasePermission):
47
+ model: typing.Optional[typing.Type[AbstractAPIKey]] = None
48
+ key_parser = KeyParser()
49
+
50
+ def get_key(self, request: HttpRequest) -> typing.Optional[str]:
51
+ return self.key_parser.get(request)
52
+
53
+ def has_permission(self, request: HttpRequest, view: typing.Any) -> bool:
54
+ assert self.model is not None, (
55
+ "%s must define `.model` with the API key model to use"
56
+ % self.__class__.__name__
57
+ )
58
+ key = self.get_key(request)
59
+ if not key:
60
+ return False
61
+ return self.model.objects.is_valid(key)
62
+
63
+ def has_object_permission(
64
+ self, request: HttpRequest, view: typing.Any, obj: AbstractAPIKey
65
+ ) -> bool:
66
+ if _drf_version < _3_14_0: # pragma: no cover
67
+ # Before DRF 3.14.0 (released in Sept 2022), bitwise OR would skip
68
+ # .has_permision() and only call .has_object_permission(), resulting in
69
+ # API key permissions not being checked unless we implemented
70
+ # .has_object_permission().
71
+ # Since 3.14.0, DRF appropriately checks for both .has_permission() and
72
+ # .has_object_permission() when checking object permissions of a bitwise OR.
73
+ # We kept the old redundant behavior to avoid regressions for users who have
74
+ # not updated their DRF yet.
75
+ return self.has_permission(request, view)
76
+
77
+ return super().has_object_permission(request, view, obj)
78
+
79
+
80
+ class HasAPIKey(BaseHasAPIKey):
81
+ model = APIKey
File without changes