django-auth-kit 0.1.0__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.
Files changed (41) hide show
  1. django_auth_kit-0.1.0/PKG-INFO +240 -0
  2. django_auth_kit-0.1.0/README.md +210 -0
  3. django_auth_kit-0.1.0/django_auth_kit/__init__.py +0 -0
  4. django_auth_kit-0.1.0/django_auth_kit/admin.py +29 -0
  5. django_auth_kit-0.1.0/django_auth_kit/apps.py +7 -0
  6. django_auth_kit-0.1.0/django_auth_kit/channels.py +147 -0
  7. django_auth_kit-0.1.0/django_auth_kit/jwt/__init__.py +0 -0
  8. django_auth_kit-0.1.0/django_auth_kit/jwt/service.py +90 -0
  9. django_auth_kit-0.1.0/django_auth_kit/middleware.py +52 -0
  10. django_auth_kit-0.1.0/django_auth_kit/migrations/0001_initial.py +51 -0
  11. django_auth_kit-0.1.0/django_auth_kit/migrations/__init__.py +0 -0
  12. django_auth_kit-0.1.0/django_auth_kit/models.py +70 -0
  13. django_auth_kit-0.1.0/django_auth_kit/otp/__init__.py +0 -0
  14. django_auth_kit-0.1.0/django_auth_kit/otp/backends/__init__.py +0 -0
  15. django_auth_kit-0.1.0/django_auth_kit/otp/backends/base.py +37 -0
  16. django_auth_kit-0.1.0/django_auth_kit/otp/backends/console.py +28 -0
  17. django_auth_kit-0.1.0/django_auth_kit/otp/service.py +150 -0
  18. django_auth_kit-0.1.0/django_auth_kit/schema/__init__.py +0 -0
  19. django_auth_kit-0.1.0/django_auth_kit/schema/inputs.py +67 -0
  20. django_auth_kit-0.1.0/django_auth_kit/schema/mutations/__init__.py +0 -0
  21. django_auth_kit-0.1.0/django_auth_kit/schema/mutations/auth.py +199 -0
  22. django_auth_kit-0.1.0/django_auth_kit/schema/mutations/password.py +102 -0
  23. django_auth_kit-0.1.0/django_auth_kit/schema/mutations/profile.py +36 -0
  24. django_auth_kit-0.1.0/django_auth_kit/schema/mutations/social.py +190 -0
  25. django_auth_kit-0.1.0/django_auth_kit/schema/queries.py +45 -0
  26. django_auth_kit-0.1.0/django_auth_kit/schema/schema.py +30 -0
  27. django_auth_kit-0.1.0/django_auth_kit/schema/types.py +49 -0
  28. django_auth_kit-0.1.0/django_auth_kit/schema/utils.py +59 -0
  29. django_auth_kit-0.1.0/django_auth_kit/settings.py +92 -0
  30. django_auth_kit-0.1.0/django_auth_kit/social/__init__.py +0 -0
  31. django_auth_kit-0.1.0/django_auth_kit/urls.py +35 -0
  32. django_auth_kit-0.1.0/django_auth_kit.egg-info/PKG-INFO +240 -0
  33. django_auth_kit-0.1.0/django_auth_kit.egg-info/SOURCES.txt +39 -0
  34. django_auth_kit-0.1.0/django_auth_kit.egg-info/dependency_links.txt +1 -0
  35. django_auth_kit-0.1.0/django_auth_kit.egg-info/requires.txt +16 -0
  36. django_auth_kit-0.1.0/django_auth_kit.egg-info/top_level.txt +1 -0
  37. django_auth_kit-0.1.0/pyproject.toml +87 -0
  38. django_auth_kit-0.1.0/setup.cfg +4 -0
  39. django_auth_kit-0.1.0/tests/test_jwt.py +51 -0
  40. django_auth_kit-0.1.0/tests/test_models.py +41 -0
  41. django_auth_kit-0.1.0/tests/test_otp.py +74 -0
@@ -0,0 +1,240 @@
1
+ Metadata-Version: 2.4
2
+ Name: django-auth-kit
3
+ Version: 0.1.0
4
+ Summary: A Django authentication kit with GraphQL (Strawberry), JWT, OTP, and social login support.
5
+ Author: Django Auth Kit Contributors
6
+ License: MIT
7
+ Classifier: Development Status :: 3 - Alpha
8
+ Classifier: Framework :: Django
9
+ Classifier: Framework :: Django :: 5.2
10
+ Classifier: Framework :: Django :: 6.0
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.14
15
+ Requires-Python: >=3.14
16
+ Description-Content-Type: text/markdown
17
+ Requires-Dist: django>=5.2
18
+ Requires-Dist: strawberry-graphql-django>=0.80.0
19
+ Requires-Dist: PyJWT>=2.8.0
20
+ Provides-Extra: channels
21
+ Requires-Dist: channels>=4.0.0; extra == "channels"
22
+ Provides-Extra: social
23
+ Requires-Dist: django-allauth>=64.0.0; extra == "social"
24
+ Provides-Extra: dev
25
+ Requires-Dist: pytest; extra == "dev"
26
+ Requires-Dist: pytest-django; extra == "dev"
27
+ Requires-Dist: pytest-cov; extra == "dev"
28
+ Requires-Dist: ruff; extra == "dev"
29
+ Requires-Dist: pre-commit; extra == "dev"
30
+
31
+ # Django Auth Kit
32
+
33
+ A batteries-included Django authentication package with Strawberry GraphQL, JWT tokens, OTP verification, and optional social login via django-allauth.
34
+
35
+ ## Features
36
+
37
+ - **UserEmail & UserMobile models** with `is_verified` / `is_primary` support (bring your own User model)
38
+ - **OTP verification** via email (Django email backend) and SMS (pluggable backend)
39
+ - **JWT authentication** with access + refresh token pairs
40
+ - **Strawberry GraphQL API** for all auth operations
41
+ - **Social login** via django-allauth (Google, Facebook, Apple, Microsoft, Azure)
42
+ - **WSGI & ASGI** support, including Django Channels consumers
43
+ - **Fully configurable** via a single `AUTH_KIT` dict in Django settings
44
+
45
+ ## Installation
46
+
47
+ ```bash
48
+ pip install django-auth-kit
49
+
50
+ # With social login support
51
+ pip install django-auth-kit[social]
52
+
53
+ # With Django Channels support
54
+ pip install django-auth-kit[channels]
55
+ ```
56
+
57
+ ## Quick Start
58
+
59
+ ### 1. Add to `INSTALLED_APPS`
60
+
61
+ ```python
62
+ INSTALLED_APPS = [
63
+ # ...
64
+ "django_auth_kit",
65
+ ]
66
+ ```
67
+
68
+ ### 2. Add middleware and include URLs
69
+
70
+ **Option A: WSGI (or ASGI without Channels)**
71
+
72
+ ```python
73
+ MIDDLEWARE = [
74
+ # ...
75
+ "django_auth_kit.middleware.JWTAuthenticationMiddleware",
76
+ ]
77
+ ```
78
+
79
+ ```python
80
+ # urls.py — WSGI
81
+ urlpatterns = [
82
+ path("auth/", include("django_auth_kit.urls")),
83
+ ]
84
+
85
+ # urls.py — ASGI (AsyncGraphQLView, no Channels)
86
+ from django_auth_kit.urls import async_urlpatterns
87
+
88
+ urlpatterns = [
89
+ path("auth/", include((async_urlpatterns, "django_auth_kit"))),
90
+ ]
91
+ ```
92
+
93
+ **Option B: Django Channels (recommended for ASGI)**
94
+
95
+ No Django middleware needed — authentication happens at the consumer level.
96
+
97
+ ```python
98
+ # asgi.py
99
+ from channels.routing import ProtocolTypeRouter, URLRouter
100
+ from django.urls import re_path
101
+ from django_auth_kit.channels import GraphQLHTTPConsumer
102
+ from myproject.schema import schema
103
+
104
+ application = ProtocolTypeRouter({
105
+ "http": URLRouter([
106
+ re_path(r"^graphql", GraphQLHTTPConsumer.as_asgi(schema=schema)),
107
+ re_path(r"^", django_asgi_application),
108
+ ]),
109
+ })
110
+ ```
111
+
112
+ See [docs/channels.md](docs/channels.md) for the full Channels setup guide.
113
+
114
+ ### 3. Run migrations
115
+
116
+ ```bash
117
+ python manage.py migrate
118
+ ```
119
+
120
+ ### 4. Configure (optional)
121
+
122
+ ```python
123
+ from datetime import timedelta
124
+
125
+ AUTH_KIT = {
126
+ # JWT
127
+ "JWT_SECRET_KEY": SECRET_KEY, # default: SECRET_KEY
128
+ "JWT_ALGORITHM": "HS256", # default: "HS256"
129
+ "JWT_ACCESS_TOKEN_LIFETIME": timedelta(hours=1),
130
+ "JWT_REFRESH_TOKEN_LIFETIME": timedelta(days=7),
131
+ "JWT_ISSUER": "django-auth-kit",
132
+
133
+ # OTP
134
+ "OTP_LENGTH": 6,
135
+ "OTP_TIMEOUT": 300, # seconds
136
+ "OTP_MAX_ATTEMPTS": 5,
137
+ "OTP_COOLDOWN": 60, # seconds between sends
138
+
139
+ # SMS backend
140
+ "SMS_BACKEND": "django_auth_kit.otp.backends.console.ConsoleSmsBackend",
141
+
142
+ # Email
143
+ "OTP_EMAIL_SUBJECT": "Your verification code",
144
+ "OTP_EMAIL_FROM": "noreply@example.com",
145
+
146
+ # Social (requires django-auth-kit[social])
147
+ "SOCIAL_PROVIDERS": [], # e.g. ["google", "facebook", "apple"]
148
+ }
149
+ ```
150
+
151
+ ## GraphQL API
152
+
153
+ The GraphQL endpoint is available at `/auth/graphql/` (or wherever you mount the URLs). Open it in a browser to access the GraphiQL IDE.
154
+
155
+ ### Queries
156
+
157
+ | Query | Auth Required | Description |
158
+ |-------|---------------|-------------|
159
+ | `me` | Yes | Returns the authenticated user's profile |
160
+
161
+ ### Mutations
162
+
163
+ | Mutation | Auth Required | Description |
164
+ |----------|---------------|-------------|
165
+ | `sendOtp` | No | Send an OTP code to an email or mobile |
166
+ | `verifyOtp` | No | Verify an OTP code |
167
+ | `register` | No | Register with verified OTP + password |
168
+ | `login` | No | Login with email/mobile + password |
169
+ | `refreshToken` | No | Get a new token pair from a refresh token |
170
+ | `changePassword` | Yes | Change password (requires current password) |
171
+ | `forgotPassword` | No | Reset password with verified OTP |
172
+ | `updateProfile` | Yes | Update first/last name |
173
+ | `socialLogin` | No | Authenticate via a social provider |
174
+
175
+ ### Auth Flows
176
+
177
+ **Registration:**
178
+ ```graphql
179
+ # 1. Send OTP
180
+ mutation { sendOtp(input: { identifier: "user@example.com", purpose: "register", channel: "email" }) { success message } }
181
+
182
+ # 2. Verify OTP
183
+ mutation { verifyOtp(input: { identifier: "user@example.com", purpose: "register", code: "123456" }) { success message } }
184
+
185
+ # 3. Register
186
+ mutation { register(input: { identifier: "user@example.com", channel: "email", code: "123456", password1: "securepass", password2: "securepass" }) { success tokens { accessToken refreshToken } } }
187
+ ```
188
+
189
+ **Login:**
190
+ ```graphql
191
+ mutation { login(input: { identifier: "user@example.com", password: "securepass" }) { success tokens { accessToken refreshToken } } }
192
+ ```
193
+
194
+ **Forgot Password:**
195
+ ```graphql
196
+ # 1. Send OTP
197
+ mutation { sendOtp(input: { identifier: "user@example.com", purpose: "forgot_password", channel: "email" }) { success } }
198
+
199
+ # 2. Verify OTP
200
+ mutation { verifyOtp(input: { identifier: "user@example.com", purpose: "forgot_password", code: "123456" }) { success } }
201
+
202
+ # 3. Reset password
203
+ mutation { forgotPassword(input: { identifier: "user@example.com", code: "123456", newPassword1: "newpass123", newPassword2: "newpass123" }) { success } }
204
+ ```
205
+
206
+ ## Custom SMS Backend
207
+
208
+ Create a backend by subclassing `BaseSmsBackend`:
209
+
210
+ ```python
211
+ from django_auth_kit.otp.backends.base import BaseSmsBackend
212
+
213
+ class TwilioSmsBackend(BaseSmsBackend):
214
+ def send_messages(self, messages):
215
+ sent = 0
216
+ for message in messages:
217
+ for recipient in message.to:
218
+ # Send via Twilio API
219
+ sent += 1
220
+ return sent
221
+ ```
222
+
223
+ Then configure:
224
+
225
+ ```python
226
+ AUTH_KIT = {
227
+ "SMS_BACKEND": "myapp.sms.TwilioSmsBackend",
228
+ }
229
+ ```
230
+
231
+ ## Development
232
+
233
+ ```bash
234
+ uv sync
235
+ uv run pytest
236
+ ```
237
+
238
+ ## License
239
+
240
+ MIT
@@ -0,0 +1,210 @@
1
+ # Django Auth Kit
2
+
3
+ A batteries-included Django authentication package with Strawberry GraphQL, JWT tokens, OTP verification, and optional social login via django-allauth.
4
+
5
+ ## Features
6
+
7
+ - **UserEmail & UserMobile models** with `is_verified` / `is_primary` support (bring your own User model)
8
+ - **OTP verification** via email (Django email backend) and SMS (pluggable backend)
9
+ - **JWT authentication** with access + refresh token pairs
10
+ - **Strawberry GraphQL API** for all auth operations
11
+ - **Social login** via django-allauth (Google, Facebook, Apple, Microsoft, Azure)
12
+ - **WSGI & ASGI** support, including Django Channels consumers
13
+ - **Fully configurable** via a single `AUTH_KIT` dict in Django settings
14
+
15
+ ## Installation
16
+
17
+ ```bash
18
+ pip install django-auth-kit
19
+
20
+ # With social login support
21
+ pip install django-auth-kit[social]
22
+
23
+ # With Django Channels support
24
+ pip install django-auth-kit[channels]
25
+ ```
26
+
27
+ ## Quick Start
28
+
29
+ ### 1. Add to `INSTALLED_APPS`
30
+
31
+ ```python
32
+ INSTALLED_APPS = [
33
+ # ...
34
+ "django_auth_kit",
35
+ ]
36
+ ```
37
+
38
+ ### 2. Add middleware and include URLs
39
+
40
+ **Option A: WSGI (or ASGI without Channels)**
41
+
42
+ ```python
43
+ MIDDLEWARE = [
44
+ # ...
45
+ "django_auth_kit.middleware.JWTAuthenticationMiddleware",
46
+ ]
47
+ ```
48
+
49
+ ```python
50
+ # urls.py — WSGI
51
+ urlpatterns = [
52
+ path("auth/", include("django_auth_kit.urls")),
53
+ ]
54
+
55
+ # urls.py — ASGI (AsyncGraphQLView, no Channels)
56
+ from django_auth_kit.urls import async_urlpatterns
57
+
58
+ urlpatterns = [
59
+ path("auth/", include((async_urlpatterns, "django_auth_kit"))),
60
+ ]
61
+ ```
62
+
63
+ **Option B: Django Channels (recommended for ASGI)**
64
+
65
+ No Django middleware needed — authentication happens at the consumer level.
66
+
67
+ ```python
68
+ # asgi.py
69
+ from channels.routing import ProtocolTypeRouter, URLRouter
70
+ from django.urls import re_path
71
+ from django_auth_kit.channels import GraphQLHTTPConsumer
72
+ from myproject.schema import schema
73
+
74
+ application = ProtocolTypeRouter({
75
+ "http": URLRouter([
76
+ re_path(r"^graphql", GraphQLHTTPConsumer.as_asgi(schema=schema)),
77
+ re_path(r"^", django_asgi_application),
78
+ ]),
79
+ })
80
+ ```
81
+
82
+ See [docs/channels.md](docs/channels.md) for the full Channels setup guide.
83
+
84
+ ### 3. Run migrations
85
+
86
+ ```bash
87
+ python manage.py migrate
88
+ ```
89
+
90
+ ### 4. Configure (optional)
91
+
92
+ ```python
93
+ from datetime import timedelta
94
+
95
+ AUTH_KIT = {
96
+ # JWT
97
+ "JWT_SECRET_KEY": SECRET_KEY, # default: SECRET_KEY
98
+ "JWT_ALGORITHM": "HS256", # default: "HS256"
99
+ "JWT_ACCESS_TOKEN_LIFETIME": timedelta(hours=1),
100
+ "JWT_REFRESH_TOKEN_LIFETIME": timedelta(days=7),
101
+ "JWT_ISSUER": "django-auth-kit",
102
+
103
+ # OTP
104
+ "OTP_LENGTH": 6,
105
+ "OTP_TIMEOUT": 300, # seconds
106
+ "OTP_MAX_ATTEMPTS": 5,
107
+ "OTP_COOLDOWN": 60, # seconds between sends
108
+
109
+ # SMS backend
110
+ "SMS_BACKEND": "django_auth_kit.otp.backends.console.ConsoleSmsBackend",
111
+
112
+ # Email
113
+ "OTP_EMAIL_SUBJECT": "Your verification code",
114
+ "OTP_EMAIL_FROM": "noreply@example.com",
115
+
116
+ # Social (requires django-auth-kit[social])
117
+ "SOCIAL_PROVIDERS": [], # e.g. ["google", "facebook", "apple"]
118
+ }
119
+ ```
120
+
121
+ ## GraphQL API
122
+
123
+ The GraphQL endpoint is available at `/auth/graphql/` (or wherever you mount the URLs). Open it in a browser to access the GraphiQL IDE.
124
+
125
+ ### Queries
126
+
127
+ | Query | Auth Required | Description |
128
+ |-------|---------------|-------------|
129
+ | `me` | Yes | Returns the authenticated user's profile |
130
+
131
+ ### Mutations
132
+
133
+ | Mutation | Auth Required | Description |
134
+ |----------|---------------|-------------|
135
+ | `sendOtp` | No | Send an OTP code to an email or mobile |
136
+ | `verifyOtp` | No | Verify an OTP code |
137
+ | `register` | No | Register with verified OTP + password |
138
+ | `login` | No | Login with email/mobile + password |
139
+ | `refreshToken` | No | Get a new token pair from a refresh token |
140
+ | `changePassword` | Yes | Change password (requires current password) |
141
+ | `forgotPassword` | No | Reset password with verified OTP |
142
+ | `updateProfile` | Yes | Update first/last name |
143
+ | `socialLogin` | No | Authenticate via a social provider |
144
+
145
+ ### Auth Flows
146
+
147
+ **Registration:**
148
+ ```graphql
149
+ # 1. Send OTP
150
+ mutation { sendOtp(input: { identifier: "user@example.com", purpose: "register", channel: "email" }) { success message } }
151
+
152
+ # 2. Verify OTP
153
+ mutation { verifyOtp(input: { identifier: "user@example.com", purpose: "register", code: "123456" }) { success message } }
154
+
155
+ # 3. Register
156
+ mutation { register(input: { identifier: "user@example.com", channel: "email", code: "123456", password1: "securepass", password2: "securepass" }) { success tokens { accessToken refreshToken } } }
157
+ ```
158
+
159
+ **Login:**
160
+ ```graphql
161
+ mutation { login(input: { identifier: "user@example.com", password: "securepass" }) { success tokens { accessToken refreshToken } } }
162
+ ```
163
+
164
+ **Forgot Password:**
165
+ ```graphql
166
+ # 1. Send OTP
167
+ mutation { sendOtp(input: { identifier: "user@example.com", purpose: "forgot_password", channel: "email" }) { success } }
168
+
169
+ # 2. Verify OTP
170
+ mutation { verifyOtp(input: { identifier: "user@example.com", purpose: "forgot_password", code: "123456" }) { success } }
171
+
172
+ # 3. Reset password
173
+ mutation { forgotPassword(input: { identifier: "user@example.com", code: "123456", newPassword1: "newpass123", newPassword2: "newpass123" }) { success } }
174
+ ```
175
+
176
+ ## Custom SMS Backend
177
+
178
+ Create a backend by subclassing `BaseSmsBackend`:
179
+
180
+ ```python
181
+ from django_auth_kit.otp.backends.base import BaseSmsBackend
182
+
183
+ class TwilioSmsBackend(BaseSmsBackend):
184
+ def send_messages(self, messages):
185
+ sent = 0
186
+ for message in messages:
187
+ for recipient in message.to:
188
+ # Send via Twilio API
189
+ sent += 1
190
+ return sent
191
+ ```
192
+
193
+ Then configure:
194
+
195
+ ```python
196
+ AUTH_KIT = {
197
+ "SMS_BACKEND": "myapp.sms.TwilioSmsBackend",
198
+ }
199
+ ```
200
+
201
+ ## Development
202
+
203
+ ```bash
204
+ uv sync
205
+ uv run pytest
206
+ ```
207
+
208
+ ## License
209
+
210
+ MIT
File without changes
@@ -0,0 +1,29 @@
1
+ from django.contrib import admin
2
+
3
+ from django_auth_kit.models import UserEmail, UserMobile
4
+
5
+
6
+ class UserEmailInline(admin.TabularInline):
7
+ model = UserEmail
8
+ extra = 0
9
+ readonly_fields = ("created_at", "updated_at")
10
+
11
+
12
+ class UserMobileInline(admin.TabularInline):
13
+ model = UserMobile
14
+ extra = 0
15
+ readonly_fields = ("created_at", "updated_at")
16
+
17
+
18
+ @admin.register(UserEmail)
19
+ class UserEmailAdmin(admin.ModelAdmin):
20
+ list_display = ("email", "user", "is_verified", "is_primary", "created_at")
21
+ list_filter = ("is_verified", "is_primary")
22
+ search_fields = ("email", "user__username")
23
+
24
+
25
+ @admin.register(UserMobile)
26
+ class UserMobileAdmin(admin.ModelAdmin):
27
+ list_display = ("mobile", "user", "is_verified", "is_primary", "created_at")
28
+ list_filter = ("is_verified", "is_primary")
29
+ search_fields = ("mobile", "user__username")
@@ -0,0 +1,7 @@
1
+ from django.apps import AppConfig
2
+
3
+
4
+ class DjangoAuthKitConfig(AppConfig):
5
+ default_auto_field = "django.db.models.BigAutoField"
6
+ name = "django_auth_kit"
7
+ verbose_name = "Django Auth Kit"
@@ -0,0 +1,147 @@
1
+ """
2
+ Channels integration for django-auth-kit.
3
+
4
+ Provides:
5
+ - ``GraphQLHTTPConsumer``: Strawberry Channels HTTP consumer with JWT auth.
6
+ - ``channels_jwt_middleware``: ASGI scope-level JWT middleware for Channels.
7
+
8
+ Usage (consumer-level auth — recommended)::
9
+
10
+ from django_auth_kit.channels import GraphQLHTTPConsumer
11
+
12
+ application = ProtocolTypeRouter({
13
+ "http": URLRouter([
14
+ re_path(r"^graphql", GraphQLHTTPConsumer.as_asgi(schema=schema)),
15
+ re_path(r"^", django_asgi_application),
16
+ ]),
17
+ })
18
+
19
+ Usage (scope-level middleware)::
20
+
21
+ from django_auth_kit.channels import channels_jwt_middleware
22
+
23
+ application = ProtocolTypeRouter({
24
+ "http": URLRouter([
25
+ re_path(
26
+ r"^graphql",
27
+ channels_jwt_middleware(
28
+ GraphQLHTTPConsumer.as_asgi(schema=schema),
29
+ ),
30
+ ),
31
+ ]),
32
+ })
33
+ """
34
+
35
+ from __future__ import annotations
36
+
37
+ from channels.db import database_sync_to_async
38
+ from django.contrib.auth import get_user_model
39
+ from django.contrib.auth.models import AnonymousUser
40
+ from strawberry.channels import GraphQLHTTPConsumer as _GraphQLHTTPConsumer
41
+ from strawberry.channels.handlers.http_handler import ChannelsRequest
42
+ from strawberry.http.typevars import Context, RootValue, SubResponse
43
+ from strawberry.types import ExecutionResult, SubscriptionExecutionResult
44
+
45
+ from django_auth_kit.jwt.service import JWTService
46
+
47
+
48
+ @database_sync_to_async
49
+ def _get_user_from_token(token: str):
50
+ """Decode a JWT access token and return the user instance."""
51
+ payload = JWTService.decode_token(token)
52
+ if payload.get("type") != "access":
53
+ return None
54
+ User = get_user_model()
55
+ try:
56
+ return User.objects.get(pk=payload["sub"], is_active=True)
57
+ except User.DoesNotExist:
58
+ return None
59
+
60
+
61
+ def _extract_bearer_token(headers: dict[str, str]) -> str | None:
62
+ """Extract Bearer token from authorization header."""
63
+ auth = headers.get("authorization", "")
64
+ if auth.startswith("Bearer "):
65
+ token = auth[7:].strip()
66
+ return token or None
67
+ return None
68
+
69
+
70
+ class GraphQLHTTPConsumer(_GraphQLHTTPConsumer):
71
+ """
72
+ Strawberry Channels HTTP consumer with built-in JWT authentication.
73
+
74
+ Authenticates at ``execute_operation`` time and injects the user into
75
+ ``self.scope["user"]``, ``request.user``, and ``context["user"]``.
76
+ """
77
+
78
+ async def execute_operation(
79
+ self,
80
+ request: ChannelsRequest,
81
+ context: Context,
82
+ root_value: RootValue | None,
83
+ sub_response: SubResponse,
84
+ ) -> ExecutionResult | list[ExecutionResult] | SubscriptionExecutionResult:
85
+ token = _extract_bearer_token(request.headers)
86
+ if token:
87
+ try:
88
+ user = await _get_user_from_token(token)
89
+ except Exception:
90
+ user = None
91
+
92
+ if user is not None:
93
+ self.scope["user"] = user
94
+ request.user = user
95
+
96
+ if isinstance(context, dict):
97
+ context["user"] = user
98
+ context["request"] = request
99
+
100
+ # If no auth succeeded, propagate scope user (e.g. from AuthMiddlewareStack)
101
+ if isinstance(context, dict) and "user" not in context:
102
+ if "user" in self.scope:
103
+ context["user"] = self.scope["user"]
104
+ elif hasattr(request, "scope") and "user" in request.scope:
105
+ context["user"] = request.scope["user"]
106
+
107
+ return await super().execute_operation(
108
+ request, context, root_value, sub_response
109
+ )
110
+
111
+
112
+ class channels_jwt_middleware:
113
+ """
114
+ ASGI scope-level middleware that decodes a JWT Bearer token from
115
+ the request headers and injects the user into ``scope["user"]``.
116
+
117
+ Runs before the consumer, so the user is available in
118
+ ``AuthMiddlewareStack``-style scope access.
119
+
120
+ Usage::
121
+
122
+ channels_jwt_middleware(consumer_or_app)
123
+ """
124
+
125
+ def __init__(self, app):
126
+ self.app = app
127
+
128
+ async def __call__(self, scope, receive, send):
129
+ if scope["type"] in ("http", "websocket"):
130
+ headers = {
131
+ k.decode(): v.decode()
132
+ for k, v in scope.get("headers", [])
133
+ }
134
+ token = _extract_bearer_token(headers)
135
+ if token:
136
+ try:
137
+ user = await _get_user_from_token(token)
138
+ except Exception:
139
+ user = None
140
+
141
+ if user is not None:
142
+ scope["user"] = user
143
+
144
+ if "user" not in scope:
145
+ scope["user"] = AnonymousUser()
146
+
147
+ return await self.app(scope, receive, send)
File without changes