authentikate 2.2.0__tar.gz → 2.2.1__tar.gz
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.
- {authentikate-2.2.0 → authentikate-2.2.1}/PKG-INFO +1 -1
- {authentikate-2.2.0 → authentikate-2.2.1}/authentikate/base_models.py +13 -13
- {authentikate-2.2.0 → authentikate-2.2.1}/authentikate/expand.py +52 -39
- authentikate-2.2.1/authentikate/migrations/0006_alter_app_identifier_alter_release_unique_together.py +22 -0
- {authentikate-2.2.0 → authentikate-2.2.1}/authentikate/models.py +3 -1
- {authentikate-2.2.0 → authentikate-2.2.1}/authentikate/provenance/models.py +7 -10
- {authentikate-2.2.0 → authentikate-2.2.1}/authentikate/strawberry/extension.py +4 -1
- {authentikate-2.2.0 → authentikate-2.2.1}/pyproject.toml +14 -11
- {authentikate-2.2.0 → authentikate-2.2.1}/.gitignore +0 -0
- {authentikate-2.2.0 → authentikate-2.2.1}/LICENSE +0 -0
- {authentikate-2.2.0 → authentikate-2.2.1}/README.md +0 -0
- {authentikate-2.2.0 → authentikate-2.2.1}/authentikate/__init__.py +0 -0
- {authentikate-2.2.0 → authentikate-2.2.1}/authentikate/admin.py +0 -0
- {authentikate-2.2.0 → authentikate-2.2.1}/authentikate/apps.py +0 -0
- {authentikate-2.2.0 → authentikate-2.2.1}/authentikate/decode.py +0 -0
- {authentikate-2.2.0 → authentikate-2.2.1}/authentikate/errors.py +0 -0
- {authentikate-2.2.0 → authentikate-2.2.1}/authentikate/migrations/0001_initial.py +0 -0
- {authentikate-2.2.0 → authentikate-2.2.1}/authentikate/migrations/0002_membership.py +0 -0
- {authentikate-2.2.0 → authentikate-2.2.1}/authentikate/migrations/0003_app_release_client_release.py +0 -0
- {authentikate-2.2.0 → authentikate-2.2.1}/authentikate/migrations/0004_device_client_device.py +0 -0
- {authentikate-2.2.0 → authentikate-2.2.1}/authentikate/migrations/0005_alter_client_client_id.py +0 -0
- {authentikate-2.2.0 → authentikate-2.2.1}/authentikate/migrations/__init__.py +0 -0
- {authentikate-2.2.0 → authentikate-2.2.1}/authentikate/protocols.py +0 -0
- {authentikate-2.2.0 → authentikate-2.2.1}/authentikate/provenance/__init__.py +0 -0
- {authentikate-2.2.0 → authentikate-2.2.1}/authentikate/provenance/canonical.py +0 -0
- {authentikate-2.2.0 → authentikate-2.2.1}/authentikate/provenance/decode.py +0 -0
- {authentikate-2.2.0 → authentikate-2.2.1}/authentikate/provenance/verify.py +0 -0
- {authentikate-2.2.0 → authentikate-2.2.1}/authentikate/settings.py +0 -0
- {authentikate-2.2.0 → authentikate-2.2.1}/authentikate/strawberry/__init__.py +0 -0
- {authentikate-2.2.0 → authentikate-2.2.1}/authentikate/strawberry/directives.py +0 -0
- {authentikate-2.2.0 → authentikate-2.2.1}/authentikate/strawberry/info.py +0 -0
- {authentikate-2.2.0 → authentikate-2.2.1}/authentikate/strawberry/types.py +0 -0
- {authentikate-2.2.0 → authentikate-2.2.1}/authentikate/utils.py +0 -0
- {authentikate-2.2.0 → authentikate-2.2.1}/authentikate/vars.py +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import hashlib
|
|
2
2
|
import logging
|
|
3
3
|
import asyncio
|
|
4
|
-
from typing import Literal,
|
|
4
|
+
from typing import Literal, Union, Annotated, cast
|
|
5
5
|
import httpx
|
|
6
6
|
from pydantic import (
|
|
7
7
|
BaseModel,
|
|
@@ -94,30 +94,28 @@ class JWTToken(BaseModel):
|
|
|
94
94
|
""" The client device identifier """
|
|
95
95
|
|
|
96
96
|
@field_validator("aud", mode="before")
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
) -> list[str] | None:
|
|
97
|
+
@classmethod
|
|
98
|
+
def aud_to_list(cls, v: str | list[str] | None) -> list[str] | None:
|
|
100
99
|
"""Convert the aud to a list"""
|
|
101
100
|
return coerce_aud_to_list(v)
|
|
102
101
|
|
|
103
102
|
@field_validator("sub", mode="before")
|
|
104
|
-
|
|
103
|
+
@classmethod
|
|
104
|
+
def sub_to_username(cls, v: str) -> str:
|
|
105
105
|
"""Convert the sub to a username compatible string"""
|
|
106
106
|
if isinstance(v, int):
|
|
107
107
|
return str(v)
|
|
108
108
|
return v
|
|
109
109
|
|
|
110
110
|
@field_validator("iat", mode="before")
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
) -> datetime.datetime | None:
|
|
111
|
+
@classmethod
|
|
112
|
+
def iat_to_datetime(cls, v: int) -> datetime.datetime | None:
|
|
114
113
|
"""Convert the iat to a datetime object"""
|
|
115
114
|
return coerce_unix_to_datetime(v)
|
|
116
115
|
|
|
117
116
|
@field_validator("exp", mode="before")
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
) -> datetime.datetime | None:
|
|
117
|
+
@classmethod
|
|
118
|
+
def exp_to_datetime(cls, v: int) -> datetime.datetime | None:
|
|
121
119
|
"""Convert the exp to a datetime object"""
|
|
122
120
|
return coerce_unix_to_datetime(v)
|
|
123
121
|
|
|
@@ -266,7 +264,8 @@ class JWKIssuer(Issuer):
|
|
|
266
264
|
"""The JWKS document of the issuer (a dict with a "keys" list)"""
|
|
267
265
|
|
|
268
266
|
@field_validator("jwks", mode="before")
|
|
269
|
-
|
|
267
|
+
@classmethod
|
|
268
|
+
def validate_jwks_dict(cls, v: Dict[str, Any]) -> Dict[str, Any]:
|
|
270
269
|
"""Validate the jwks dict"""
|
|
271
270
|
if not isinstance(v, dict):
|
|
272
271
|
raise ValueError("jwks_dict must be a dict")
|
|
@@ -531,7 +530,8 @@ class ProvenanceSettings(BaseModel):
|
|
|
531
530
|
"""The signature algorithms allowed for provenance tokens (alg is pinned)."""
|
|
532
531
|
|
|
533
532
|
@field_validator("algorithms")
|
|
534
|
-
|
|
533
|
+
@classmethod
|
|
534
|
+
def reject_unsafe_algorithms(cls, v: list[str]) -> list[str]:
|
|
535
535
|
"""Pin the alg per RFC 8725: forbid an empty list and the ``none`` alg.
|
|
536
536
|
|
|
537
537
|
An empty allow-list or ``alg: none`` would let an attacker present an
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from dataclasses import dataclass
|
|
2
2
|
from django.contrib.auth.models import Group
|
|
3
|
+
from django.db import IntegrityError
|
|
3
4
|
from authentikate import base_models, models
|
|
4
5
|
import logging
|
|
5
6
|
from typing import cast
|
|
@@ -167,26 +168,7 @@ async def aexpand_user_from_token(
|
|
|
167
168
|
|
|
168
169
|
try:
|
|
169
170
|
user = await models.User.objects.aget(sub=token.sub, iss=token.iss)
|
|
170
|
-
if user.changed_hash != token.changed_hash:
|
|
171
|
-
# User has changed, update the user object
|
|
172
|
-
user.first_name = token.preferred_username
|
|
173
|
-
user.changed_hash = token.changed_hash
|
|
174
|
-
|
|
175
|
-
if organization is not None:
|
|
176
|
-
user.active_organization = organization
|
|
177
|
-
elif token.active_org:
|
|
178
|
-
current_org, _ = await models.Organization.objects.aget_or_create(
|
|
179
|
-
slug=token.active_org,
|
|
180
|
-
)
|
|
181
|
-
user.active_organization = current_org
|
|
182
|
-
|
|
183
|
-
await user.asave()
|
|
184
|
-
await aset_user_groups(user, token.roles)
|
|
185
|
-
|
|
186
|
-
return user
|
|
187
|
-
|
|
188
171
|
except models.User.DoesNotExist:
|
|
189
|
-
|
|
190
172
|
user = models.User(
|
|
191
173
|
sub=token.sub,
|
|
192
174
|
username=token_to_username(token),
|
|
@@ -196,6 +178,30 @@ async def aexpand_user_from_token(
|
|
|
196
178
|
user.first_name = token.preferred_username
|
|
197
179
|
user.changed_hash = token.changed_hash
|
|
198
180
|
|
|
181
|
+
if organization is not None:
|
|
182
|
+
user.active_organization = organization
|
|
183
|
+
elif token.active_org:
|
|
184
|
+
current_org, _ = await models.Organization.objects.aget_or_create(
|
|
185
|
+
slug=token.active_org,
|
|
186
|
+
)
|
|
187
|
+
user.active_organization = current_org
|
|
188
|
+
|
|
189
|
+
try:
|
|
190
|
+
await user.asave()
|
|
191
|
+
await aset_user_groups(user, token.roles)
|
|
192
|
+
return user
|
|
193
|
+
except IntegrityError:
|
|
194
|
+
# Lost a concurrent create race: another request authenticating the
|
|
195
|
+
# same token already inserted this (sub, iss) user. Fall through to
|
|
196
|
+
# treat the winner's row as an existing user instead of propagating
|
|
197
|
+
# the IntegrityError.
|
|
198
|
+
user = await models.User.objects.aget(sub=token.sub, iss=token.iss)
|
|
199
|
+
|
|
200
|
+
if user.changed_hash != token.changed_hash:
|
|
201
|
+
# The token's user metadata changed since we last saw it: sync it across.
|
|
202
|
+
user.first_name = token.preferred_username
|
|
203
|
+
user.changed_hash = token.changed_hash
|
|
204
|
+
|
|
199
205
|
if organization is not None:
|
|
200
206
|
user.active_organization = organization
|
|
201
207
|
elif token.active_org:
|
|
@@ -206,7 +212,8 @@ async def aexpand_user_from_token(
|
|
|
206
212
|
|
|
207
213
|
await user.asave()
|
|
208
214
|
await aset_user_groups(user, token.roles)
|
|
209
|
-
|
|
215
|
+
|
|
216
|
+
return user
|
|
210
217
|
|
|
211
218
|
|
|
212
219
|
async def aexpand_token_context(
|
|
@@ -240,25 +247,7 @@ def expand_user_from_token(
|
|
|
240
247
|
|
|
241
248
|
try:
|
|
242
249
|
user = models.User.objects.get(sub=token.sub, iss=token.iss)
|
|
243
|
-
if user.changed_hash != token.changed_hash:
|
|
244
|
-
# User has changed, update the user object
|
|
245
|
-
user.first_name = token.preferred_username
|
|
246
|
-
user.changed_hash = token.changed_hash
|
|
247
|
-
set_user_groups(user, token.roles)
|
|
248
|
-
|
|
249
|
-
if token.active_org:
|
|
250
|
-
current_org, _ = models.Organization.objects.get_or_create(
|
|
251
|
-
slug=token.active_org
|
|
252
|
-
)
|
|
253
|
-
|
|
254
|
-
user.active_organization = current_org
|
|
255
|
-
|
|
256
|
-
user.save()
|
|
257
|
-
|
|
258
|
-
return user
|
|
259
|
-
|
|
260
250
|
except models.User.DoesNotExist:
|
|
261
|
-
|
|
262
251
|
user = models.User(
|
|
263
252
|
sub=token.sub,
|
|
264
253
|
username=(token_to_username(token)),
|
|
@@ -268,6 +257,29 @@ def expand_user_from_token(
|
|
|
268
257
|
user.first_name = token.preferred_username
|
|
269
258
|
user.changed_hash = token.changed_hash
|
|
270
259
|
|
|
260
|
+
if token.active_org:
|
|
261
|
+
current_org, _ = models.Organization.objects.get_or_create(
|
|
262
|
+
slug=token.active_org
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
user.active_organization = current_org
|
|
266
|
+
|
|
267
|
+
try:
|
|
268
|
+
user.save()
|
|
269
|
+
set_user_groups(user, token.roles)
|
|
270
|
+
return user
|
|
271
|
+
except IntegrityError:
|
|
272
|
+
# Lost a concurrent create race: another request authenticating the
|
|
273
|
+
# same token already inserted this (sub, iss) user. Fall through to
|
|
274
|
+
# treat the winner's row as an existing user instead of propagating
|
|
275
|
+
# the IntegrityError.
|
|
276
|
+
user = models.User.objects.get(sub=token.sub, iss=token.iss)
|
|
277
|
+
|
|
278
|
+
if user.changed_hash != token.changed_hash:
|
|
279
|
+
# The token's user metadata changed since we last saw it: sync it across.
|
|
280
|
+
user.first_name = token.preferred_username
|
|
281
|
+
user.changed_hash = token.changed_hash
|
|
282
|
+
|
|
271
283
|
if token.active_org:
|
|
272
284
|
current_org, _ = models.Organization.objects.get_or_create(
|
|
273
285
|
slug=token.active_org
|
|
@@ -277,7 +289,8 @@ def expand_user_from_token(
|
|
|
277
289
|
|
|
278
290
|
user.save()
|
|
279
291
|
set_user_groups(user, token.roles)
|
|
280
|
-
|
|
292
|
+
|
|
293
|
+
return user
|
|
281
294
|
|
|
282
295
|
|
|
283
296
|
async def aexpand_client_from_token(
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Generated by Django 6.0.5 on 2026-06-29 15:00
|
|
2
|
+
|
|
3
|
+
from django.db import migrations, models
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Migration(migrations.Migration):
|
|
7
|
+
|
|
8
|
+
dependencies = [
|
|
9
|
+
("authentikate", "0005_alter_client_client_id"),
|
|
10
|
+
]
|
|
11
|
+
|
|
12
|
+
operations = [
|
|
13
|
+
migrations.AlterField(
|
|
14
|
+
model_name="app",
|
|
15
|
+
name="identifier",
|
|
16
|
+
field=models.CharField(max_length=2000, unique=True),
|
|
17
|
+
),
|
|
18
|
+
migrations.AlterUniqueTogether(
|
|
19
|
+
name="release",
|
|
20
|
+
unique_together={("app", "version")},
|
|
21
|
+
),
|
|
22
|
+
]
|
|
@@ -82,7 +82,7 @@ class Device(models.Model):
|
|
|
82
82
|
class App(models.Model):
|
|
83
83
|
"""An App model to represent an application in the system"""
|
|
84
84
|
|
|
85
|
-
identifier = models.CharField(max_length=2000)
|
|
85
|
+
identifier = models.CharField(max_length=2000, unique=True)
|
|
86
86
|
"""The application identifier (from the token's client_app claim)"""
|
|
87
87
|
|
|
88
88
|
def __str__(self) -> str:
|
|
@@ -101,6 +101,8 @@ class Release(models.Model):
|
|
|
101
101
|
class Meta:
|
|
102
102
|
"""Meta class for Release"""
|
|
103
103
|
|
|
104
|
+
unique_together = ("app", "version")
|
|
105
|
+
|
|
104
106
|
|
|
105
107
|
class Client(models.Model):
|
|
106
108
|
"""An Oauth2 Client
|
|
@@ -7,7 +7,7 @@ verification entrypoint.
|
|
|
7
7
|
"""
|
|
8
8
|
|
|
9
9
|
import datetime
|
|
10
|
-
from typing import Any
|
|
10
|
+
from typing import Any
|
|
11
11
|
|
|
12
12
|
from pydantic import BaseModel, ConfigDict, field_validator
|
|
13
13
|
|
|
@@ -73,23 +73,20 @@ class ProvenanceToken(BaseModel):
|
|
|
73
73
|
"""The raw original token string."""
|
|
74
74
|
|
|
75
75
|
@field_validator("aud", mode="before")
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
) -> list[str] | None:
|
|
76
|
+
@classmethod
|
|
77
|
+
def aud_to_list(cls, v: str | list[str] | None) -> list[str] | None:
|
|
79
78
|
"""Convert the aud to a list"""
|
|
80
79
|
return coerce_aud_to_list(v)
|
|
81
80
|
|
|
82
81
|
@field_validator("iat", mode="before")
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
) -> datetime.datetime | None:
|
|
82
|
+
@classmethod
|
|
83
|
+
def iat_to_datetime(cls, v: int) -> datetime.datetime | None:
|
|
86
84
|
"""Convert the iat to a datetime object"""
|
|
87
85
|
return coerce_unix_to_datetime(v)
|
|
88
86
|
|
|
89
87
|
@field_validator("exp", mode="before")
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
) -> datetime.datetime | None:
|
|
88
|
+
@classmethod
|
|
89
|
+
def exp_to_datetime(cls, v: int) -> datetime.datetime | None:
|
|
93
90
|
"""Convert the exp to a datetime object"""
|
|
94
91
|
return coerce_unix_to_datetime(v)
|
|
95
92
|
|
|
@@ -144,7 +144,10 @@ class AuthentikateExtension(SchemaExtension):
|
|
|
144
144
|
dict(context.headers), settings
|
|
145
145
|
)
|
|
146
146
|
if provenance is not None:
|
|
147
|
-
|
|
147
|
+
# ProvenanceToken structurally satisfies kante's
|
|
148
|
+
# Provenance protocol at runtime; the nested Actor
|
|
149
|
+
# types differ only by protocol invariance.
|
|
150
|
+
context.request.set_provenance(provenance) # pyright: ignore[reportArgumentType]
|
|
148
151
|
context.request.set_extension("provenance", provenance)
|
|
149
152
|
else:
|
|
150
153
|
raise ValueError(
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "authentikate"
|
|
3
|
-
version = "2.2.
|
|
3
|
+
version = "2.2.1"
|
|
4
4
|
description = ""
|
|
5
5
|
authors = [{ name = "jhnnsrs", email = "jhnnsrs@gmail.com" }]
|
|
6
6
|
requires-python = ">=3.12, <4.0"
|
|
@@ -23,7 +23,8 @@ dev-dependencies = [
|
|
|
23
23
|
"cryptography>=45.0.1",
|
|
24
24
|
"ruff>=0.0.282,<0.0.283",
|
|
25
25
|
"black>=22",
|
|
26
|
-
"
|
|
26
|
+
"basedpyright>=1.10.0",
|
|
27
|
+
"django-types>=0.19.1",
|
|
27
28
|
"python-semantic-release>=9.21.1",
|
|
28
29
|
"daphne>=4.1.2",
|
|
29
30
|
"pytest-asyncio>=0.23.8",
|
|
@@ -34,11 +35,16 @@ omit = [
|
|
|
34
35
|
"test_project/*"
|
|
35
36
|
]
|
|
36
37
|
|
|
37
|
-
[tool.
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
38
|
+
[tool.basedpyright]
|
|
39
|
+
include = ["authentikate"]
|
|
40
|
+
exclude = ["**/migrations", "venv", ".venv", "tests", "examples"]
|
|
41
|
+
pythonVersion = "3.12"
|
|
42
|
+
typeCheckingMode = "standard"
|
|
43
|
+
reportMissingTypeStubs = false
|
|
44
|
+
# Pydantic models intentionally narrow inherited fields (e.g. a discriminator
|
|
45
|
+
# `kind: str` -> `Literal[...]`, or an optional base field made required in a
|
|
46
|
+
# subclass). Pyright flags these because mutable attributes are invariant.
|
|
47
|
+
reportIncompatibleVariableOverride = false
|
|
42
48
|
|
|
43
49
|
[tool.hatch.build.targets.sdist]
|
|
44
50
|
include = ["authentikate"]
|
|
@@ -50,9 +56,6 @@ include = ["authentikate"]
|
|
|
50
56
|
requires = ["hatchling"]
|
|
51
57
|
build-backend = "hatchling.build"
|
|
52
58
|
|
|
53
|
-
[tool.django-stubs]
|
|
54
|
-
django_settings_module = "test_project.settings"
|
|
55
|
-
|
|
56
59
|
[tool.pytest.ini_options]
|
|
57
60
|
DJANGO_SETTINGS_MODULE = "test_project.settings"
|
|
58
61
|
|
|
@@ -61,7 +64,7 @@ DJANGO_SETTINGS_MODULE = "test_project.settings"
|
|
|
61
64
|
# Unlike Flake8, Ruff doesn't enable pycodestyle warnings (`W`) or
|
|
62
65
|
# McCabe complexity (`C901`) by default.
|
|
63
66
|
extend-select = ["ANN", "D1"]
|
|
64
|
-
extend-ignore = [ "ANN002", "ANN003", "D100", "ANN401", "ANN101"]
|
|
67
|
+
extend-ignore = [ "ANN002", "ANN003", "D100", "ANN401", "ANN101", "ANN102"]
|
|
65
68
|
|
|
66
69
|
# Exclude a variety of commonly ignored directories.
|
|
67
70
|
exclude = [
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{authentikate-2.2.0 → authentikate-2.2.1}/authentikate/migrations/0003_app_release_client_release.py
RENAMED
|
File without changes
|
{authentikate-2.2.0 → authentikate-2.2.1}/authentikate/migrations/0004_device_client_device.py
RENAMED
|
File without changes
|
{authentikate-2.2.0 → authentikate-2.2.1}/authentikate/migrations/0005_alter_client_client_id.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|