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.
- django_auth_kit-0.1.0/PKG-INFO +240 -0
- django_auth_kit-0.1.0/README.md +210 -0
- django_auth_kit-0.1.0/django_auth_kit/__init__.py +0 -0
- django_auth_kit-0.1.0/django_auth_kit/admin.py +29 -0
- django_auth_kit-0.1.0/django_auth_kit/apps.py +7 -0
- django_auth_kit-0.1.0/django_auth_kit/channels.py +147 -0
- django_auth_kit-0.1.0/django_auth_kit/jwt/__init__.py +0 -0
- django_auth_kit-0.1.0/django_auth_kit/jwt/service.py +90 -0
- django_auth_kit-0.1.0/django_auth_kit/middleware.py +52 -0
- django_auth_kit-0.1.0/django_auth_kit/migrations/0001_initial.py +51 -0
- django_auth_kit-0.1.0/django_auth_kit/migrations/__init__.py +0 -0
- django_auth_kit-0.1.0/django_auth_kit/models.py +70 -0
- django_auth_kit-0.1.0/django_auth_kit/otp/__init__.py +0 -0
- django_auth_kit-0.1.0/django_auth_kit/otp/backends/__init__.py +0 -0
- django_auth_kit-0.1.0/django_auth_kit/otp/backends/base.py +37 -0
- django_auth_kit-0.1.0/django_auth_kit/otp/backends/console.py +28 -0
- django_auth_kit-0.1.0/django_auth_kit/otp/service.py +150 -0
- django_auth_kit-0.1.0/django_auth_kit/schema/__init__.py +0 -0
- django_auth_kit-0.1.0/django_auth_kit/schema/inputs.py +67 -0
- django_auth_kit-0.1.0/django_auth_kit/schema/mutations/__init__.py +0 -0
- django_auth_kit-0.1.0/django_auth_kit/schema/mutations/auth.py +199 -0
- django_auth_kit-0.1.0/django_auth_kit/schema/mutations/password.py +102 -0
- django_auth_kit-0.1.0/django_auth_kit/schema/mutations/profile.py +36 -0
- django_auth_kit-0.1.0/django_auth_kit/schema/mutations/social.py +190 -0
- django_auth_kit-0.1.0/django_auth_kit/schema/queries.py +45 -0
- django_auth_kit-0.1.0/django_auth_kit/schema/schema.py +30 -0
- django_auth_kit-0.1.0/django_auth_kit/schema/types.py +49 -0
- django_auth_kit-0.1.0/django_auth_kit/schema/utils.py +59 -0
- django_auth_kit-0.1.0/django_auth_kit/settings.py +92 -0
- django_auth_kit-0.1.0/django_auth_kit/social/__init__.py +0 -0
- django_auth_kit-0.1.0/django_auth_kit/urls.py +35 -0
- django_auth_kit-0.1.0/django_auth_kit.egg-info/PKG-INFO +240 -0
- django_auth_kit-0.1.0/django_auth_kit.egg-info/SOURCES.txt +39 -0
- django_auth_kit-0.1.0/django_auth_kit.egg-info/dependency_links.txt +1 -0
- django_auth_kit-0.1.0/django_auth_kit.egg-info/requires.txt +16 -0
- django_auth_kit-0.1.0/django_auth_kit.egg-info/top_level.txt +1 -0
- django_auth_kit-0.1.0/pyproject.toml +87 -0
- django_auth_kit-0.1.0/setup.cfg +4 -0
- django_auth_kit-0.1.0/tests/test_jwt.py +51 -0
- django_auth_kit-0.1.0/tests/test_models.py +41 -0
- 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,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
|