plain.auth 0.24.0__tar.gz → 0.25.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.
- {plain_auth-0.24.0 → plain_auth-0.25.0}/PKG-INFO +79 -14
- {plain_auth-0.24.0 → plain_auth-0.25.0}/plain/auth/CHANGELOG.md +10 -0
- {plain_auth-0.24.0 → plain_auth-0.25.0}/plain/auth/README.md +78 -13
- {plain_auth-0.24.0 → plain_auth-0.25.0}/plain/auth/sessions.py +14 -14
- {plain_auth-0.24.0 → plain_auth-0.25.0}/pyproject.toml +1 -1
- {plain_auth-0.24.0 → plain_auth-0.25.0}/.gitignore +0 -0
- {plain_auth-0.24.0 → plain_auth-0.25.0}/LICENSE +0 -0
- {plain_auth-0.24.0 → plain_auth-0.25.0}/README.md +0 -0
- {plain_auth-0.24.0 → plain_auth-0.25.0}/plain/auth/__init__.py +0 -0
- {plain_auth-0.24.0 → plain_auth-0.25.0}/plain/auth/default_settings.py +0 -0
- {plain_auth-0.24.0 → plain_auth-0.25.0}/plain/auth/requests.py +0 -0
- {plain_auth-0.24.0 → plain_auth-0.25.0}/plain/auth/templates.py +0 -0
- {plain_auth-0.24.0 → plain_auth-0.25.0}/plain/auth/test.py +0 -0
- {plain_auth-0.24.0 → plain_auth-0.25.0}/plain/auth/utils.py +0 -0
- {plain_auth-0.24.0 → plain_auth-0.25.0}/plain/auth/views.py +0 -0
- {plain_auth-0.24.0 → plain_auth-0.25.0}/tests/app/settings.py +0 -0
- {plain_auth-0.24.0 → plain_auth-0.25.0}/tests/app/urls.py +0 -0
- {plain_auth-0.24.0 → plain_auth-0.25.0}/tests/app/users/migrations/0001_initial.py +0 -0
- {plain_auth-0.24.0 → plain_auth-0.25.0}/tests/app/users/migrations/__init__.py +0 -0
- {plain_auth-0.24.0 → plain_auth-0.25.0}/tests/app/users/models.py +0 -0
- {plain_auth-0.24.0 → plain_auth-0.25.0}/tests/test_views.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: plain.auth
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.25.0
|
|
4
4
|
Summary: Add users to your app and decide what they can access.
|
|
5
5
|
Author-email: Dave Gaeddert <dave.gaeddert@dropseed.dev>
|
|
6
6
|
License-Expression: BSD-3-Clause
|
|
@@ -22,14 +22,15 @@ Description-Content-Type: text/markdown
|
|
|
22
22
|
- [Login views](#login-views)
|
|
23
23
|
- [Checking if a user is logged in](#checking-if-a-user-is-logged-in)
|
|
24
24
|
- [Restricting views](#restricting-views)
|
|
25
|
+
- [Testing with authenticated users](#testing-with-authenticated-users)
|
|
26
|
+
- [FAQs](#faqs)
|
|
25
27
|
- [Installation](#installation)
|
|
26
28
|
|
|
27
29
|
## Overview
|
|
28
30
|
|
|
29
|
-
The `plain.auth` package
|
|
31
|
+
The `plain.auth` package handles user authentication and authorization for Plain applications. You can check if a user is logged in like this:
|
|
30
32
|
|
|
31
33
|
```python
|
|
32
|
-
# In a view
|
|
33
34
|
from plain.auth import get_request_user
|
|
34
35
|
|
|
35
36
|
user = get_request_user(request)
|
|
@@ -39,7 +40,7 @@ else:
|
|
|
39
40
|
print("You are not logged in.")
|
|
40
41
|
```
|
|
41
42
|
|
|
42
|
-
|
|
43
|
+
You can restrict a view to logged-in users using [`AuthViewMixin`](./views.py#AuthViewMixin):
|
|
43
44
|
|
|
44
45
|
```python
|
|
45
46
|
from plain.auth.views import AuthViewMixin
|
|
@@ -76,7 +77,7 @@ AUTH_LOGIN_URL = "login"
|
|
|
76
77
|
|
|
77
78
|
### Creating a user model
|
|
78
79
|
|
|
79
|
-
|
|
80
|
+
You can create your own user model using `plain create users` or manually:
|
|
80
81
|
|
|
81
82
|
```python
|
|
82
83
|
# app/users/models.py
|
|
@@ -96,14 +97,13 @@ class User(models.Model):
|
|
|
96
97
|
|
|
97
98
|
### Login views
|
|
98
99
|
|
|
99
|
-
To log users in, you
|
|
100
|
+
To log users in, you need to pair this package with an authentication method:
|
|
100
101
|
|
|
101
|
-
-
|
|
102
|
-
-
|
|
103
|
-
-
|
|
104
|
-
- `plain-passlinks` (TBD) - Magic link authentication
|
|
102
|
+
- [plain.passwords](../../plain-passwords/plain/passwords/README.md) - Username/password authentication
|
|
103
|
+
- [plain.oauth](../../plain-oauth/plain/oauth/README.md) - OAuth provider authentication
|
|
104
|
+
- [plain.loginlink](../../plain-loginlink/plain/loginlink/README.md) - Magic link authentication
|
|
105
105
|
|
|
106
|
-
|
|
106
|
+
Here's an example with password authentication:
|
|
107
107
|
|
|
108
108
|
```python
|
|
109
109
|
# app/urls.py
|
|
@@ -124,7 +124,7 @@ urlpatterns = [
|
|
|
124
124
|
|
|
125
125
|
## Checking if a user is logged in
|
|
126
126
|
|
|
127
|
-
In templates, use the `get_current_user()` function:
|
|
127
|
+
In templates, you can use the `get_current_user()` function:
|
|
128
128
|
|
|
129
129
|
```html
|
|
130
130
|
{% if get_current_user() %}
|
|
@@ -134,7 +134,7 @@ In templates, use the `get_current_user()` function:
|
|
|
134
134
|
{% endif %}
|
|
135
135
|
```
|
|
136
136
|
|
|
137
|
-
In Python code, use `get_request_user()
|
|
137
|
+
In Python code, use [`get_request_user()`](./requests.py#get_request_user):
|
|
138
138
|
|
|
139
139
|
```python
|
|
140
140
|
from plain.auth import get_request_user
|
|
@@ -148,7 +148,7 @@ else:
|
|
|
148
148
|
|
|
149
149
|
## Restricting views
|
|
150
150
|
|
|
151
|
-
|
|
151
|
+
You can use [`AuthViewMixin`](./views.py#AuthViewMixin) to restrict views to logged-in users, admin users, or custom logic:
|
|
152
152
|
|
|
153
153
|
```python
|
|
154
154
|
from plain.auth.views import AuthViewMixin
|
|
@@ -178,6 +178,71 @@ The [`AuthViewMixin`](./views.py#AuthViewMixin) provides:
|
|
|
178
178
|
- `admin_required` - Requires `user.is_admin` to be True
|
|
179
179
|
- `check_auth()` - Override for custom authorization logic
|
|
180
180
|
|
|
181
|
+
## Testing with authenticated users
|
|
182
|
+
|
|
183
|
+
When writing tests, you can use [`login_client()`](./test.py#login_client) to simulate an authenticated user:
|
|
184
|
+
|
|
185
|
+
```python
|
|
186
|
+
from plain.auth.test import login_client
|
|
187
|
+
from plain.test import Client
|
|
188
|
+
|
|
189
|
+
from app.users.models import User
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def test_profile_view():
|
|
193
|
+
user = User.objects.create(email="test@example.com")
|
|
194
|
+
client = Client()
|
|
195
|
+
login_client(client, user)
|
|
196
|
+
|
|
197
|
+
response = client.get("/profile/")
|
|
198
|
+
assert response.status_code == 200
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
You can also log out a test user with [`logout_client()`](./test.py#logout_client):
|
|
202
|
+
|
|
203
|
+
```python
|
|
204
|
+
from plain.auth.test import login_client, logout_client
|
|
205
|
+
|
|
206
|
+
# ... after logging in
|
|
207
|
+
logout_client(client)
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
## FAQs
|
|
211
|
+
|
|
212
|
+
#### How do I log in a user programmatically?
|
|
213
|
+
|
|
214
|
+
You can use the [`login()`](./sessions.py#login) function to log in a user:
|
|
215
|
+
|
|
216
|
+
```python
|
|
217
|
+
from plain.auth.sessions import login
|
|
218
|
+
|
|
219
|
+
login(request, user)
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
#### How do I log out a user programmatically?
|
|
223
|
+
|
|
224
|
+
You can use the [`logout()`](./sessions.py#logout) function:
|
|
225
|
+
|
|
226
|
+
```python
|
|
227
|
+
from plain.auth.sessions import logout
|
|
228
|
+
|
|
229
|
+
logout(request)
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
#### How do I invalidate sessions when a user changes their password?
|
|
233
|
+
|
|
234
|
+
By default, if you have [plain.passwords](../../plain-passwords/plain/passwords/README.md) installed, sessions are automatically invalidated when the `password` field changes. This is controlled by the `AUTH_USER_SESSION_HASH_FIELD` setting. You can change this to a different field name, or set it to an empty string to disable this feature.
|
|
235
|
+
|
|
236
|
+
#### How do I get the user model class?
|
|
237
|
+
|
|
238
|
+
You can use the [`get_user_model()`](./sessions.py#get_user_model) function:
|
|
239
|
+
|
|
240
|
+
```python
|
|
241
|
+
from plain.auth.sessions import get_user_model
|
|
242
|
+
|
|
243
|
+
User = get_user_model()
|
|
244
|
+
```
|
|
245
|
+
|
|
181
246
|
## Installation
|
|
182
247
|
|
|
183
248
|
Install the `plain.auth` package from [PyPI](https://pypi.org/project/plain.auth/):
|
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# plain-auth changelog
|
|
2
2
|
|
|
3
|
+
## [0.25.0](https://github.com/dropseed/plain/releases/plain-auth@0.25.0) (2026-01-13)
|
|
4
|
+
|
|
5
|
+
### What's changed
|
|
6
|
+
|
|
7
|
+
- Improved README documentation with better examples and consistent structure ([da37a78](https://github.com/dropseed/plain/commit/da37a78fbb))
|
|
8
|
+
|
|
9
|
+
### Upgrade instructions
|
|
10
|
+
|
|
11
|
+
- No changes required
|
|
12
|
+
|
|
3
13
|
## [0.24.0](https://github.com/dropseed/plain/releases/plain-auth@0.24.0) (2026-01-13)
|
|
4
14
|
|
|
5
15
|
### What's changed
|
|
@@ -9,14 +9,15 @@
|
|
|
9
9
|
- [Login views](#login-views)
|
|
10
10
|
- [Checking if a user is logged in](#checking-if-a-user-is-logged-in)
|
|
11
11
|
- [Restricting views](#restricting-views)
|
|
12
|
+
- [Testing with authenticated users](#testing-with-authenticated-users)
|
|
13
|
+
- [FAQs](#faqs)
|
|
12
14
|
- [Installation](#installation)
|
|
13
15
|
|
|
14
16
|
## Overview
|
|
15
17
|
|
|
16
|
-
The `plain.auth` package
|
|
18
|
+
The `plain.auth` package handles user authentication and authorization for Plain applications. You can check if a user is logged in like this:
|
|
17
19
|
|
|
18
20
|
```python
|
|
19
|
-
# In a view
|
|
20
21
|
from plain.auth import get_request_user
|
|
21
22
|
|
|
22
23
|
user = get_request_user(request)
|
|
@@ -26,7 +27,7 @@ else:
|
|
|
26
27
|
print("You are not logged in.")
|
|
27
28
|
```
|
|
28
29
|
|
|
29
|
-
|
|
30
|
+
You can restrict a view to logged-in users using [`AuthViewMixin`](./views.py#AuthViewMixin):
|
|
30
31
|
|
|
31
32
|
```python
|
|
32
33
|
from plain.auth.views import AuthViewMixin
|
|
@@ -63,7 +64,7 @@ AUTH_LOGIN_URL = "login"
|
|
|
63
64
|
|
|
64
65
|
### Creating a user model
|
|
65
66
|
|
|
66
|
-
|
|
67
|
+
You can create your own user model using `plain create users` or manually:
|
|
67
68
|
|
|
68
69
|
```python
|
|
69
70
|
# app/users/models.py
|
|
@@ -83,14 +84,13 @@ class User(models.Model):
|
|
|
83
84
|
|
|
84
85
|
### Login views
|
|
85
86
|
|
|
86
|
-
To log users in, you
|
|
87
|
+
To log users in, you need to pair this package with an authentication method:
|
|
87
88
|
|
|
88
|
-
-
|
|
89
|
-
-
|
|
90
|
-
-
|
|
91
|
-
- `plain-passlinks` (TBD) - Magic link authentication
|
|
89
|
+
- [plain.passwords](../../plain-passwords/plain/passwords/README.md) - Username/password authentication
|
|
90
|
+
- [plain.oauth](../../plain-oauth/plain/oauth/README.md) - OAuth provider authentication
|
|
91
|
+
- [plain.loginlink](../../plain-loginlink/plain/loginlink/README.md) - Magic link authentication
|
|
92
92
|
|
|
93
|
-
|
|
93
|
+
Here's an example with password authentication:
|
|
94
94
|
|
|
95
95
|
```python
|
|
96
96
|
# app/urls.py
|
|
@@ -111,7 +111,7 @@ urlpatterns = [
|
|
|
111
111
|
|
|
112
112
|
## Checking if a user is logged in
|
|
113
113
|
|
|
114
|
-
In templates, use the `get_current_user()` function:
|
|
114
|
+
In templates, you can use the `get_current_user()` function:
|
|
115
115
|
|
|
116
116
|
```html
|
|
117
117
|
{% if get_current_user() %}
|
|
@@ -121,7 +121,7 @@ In templates, use the `get_current_user()` function:
|
|
|
121
121
|
{% endif %}
|
|
122
122
|
```
|
|
123
123
|
|
|
124
|
-
In Python code, use `get_request_user()
|
|
124
|
+
In Python code, use [`get_request_user()`](./requests.py#get_request_user):
|
|
125
125
|
|
|
126
126
|
```python
|
|
127
127
|
from plain.auth import get_request_user
|
|
@@ -135,7 +135,7 @@ else:
|
|
|
135
135
|
|
|
136
136
|
## Restricting views
|
|
137
137
|
|
|
138
|
-
|
|
138
|
+
You can use [`AuthViewMixin`](./views.py#AuthViewMixin) to restrict views to logged-in users, admin users, or custom logic:
|
|
139
139
|
|
|
140
140
|
```python
|
|
141
141
|
from plain.auth.views import AuthViewMixin
|
|
@@ -165,6 +165,71 @@ The [`AuthViewMixin`](./views.py#AuthViewMixin) provides:
|
|
|
165
165
|
- `admin_required` - Requires `user.is_admin` to be True
|
|
166
166
|
- `check_auth()` - Override for custom authorization logic
|
|
167
167
|
|
|
168
|
+
## Testing with authenticated users
|
|
169
|
+
|
|
170
|
+
When writing tests, you can use [`login_client()`](./test.py#login_client) to simulate an authenticated user:
|
|
171
|
+
|
|
172
|
+
```python
|
|
173
|
+
from plain.auth.test import login_client
|
|
174
|
+
from plain.test import Client
|
|
175
|
+
|
|
176
|
+
from app.users.models import User
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def test_profile_view():
|
|
180
|
+
user = User.objects.create(email="test@example.com")
|
|
181
|
+
client = Client()
|
|
182
|
+
login_client(client, user)
|
|
183
|
+
|
|
184
|
+
response = client.get("/profile/")
|
|
185
|
+
assert response.status_code == 200
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
You can also log out a test user with [`logout_client()`](./test.py#logout_client):
|
|
189
|
+
|
|
190
|
+
```python
|
|
191
|
+
from plain.auth.test import login_client, logout_client
|
|
192
|
+
|
|
193
|
+
# ... after logging in
|
|
194
|
+
logout_client(client)
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
## FAQs
|
|
198
|
+
|
|
199
|
+
#### How do I log in a user programmatically?
|
|
200
|
+
|
|
201
|
+
You can use the [`login()`](./sessions.py#login) function to log in a user:
|
|
202
|
+
|
|
203
|
+
```python
|
|
204
|
+
from plain.auth.sessions import login
|
|
205
|
+
|
|
206
|
+
login(request, user)
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
#### How do I log out a user programmatically?
|
|
210
|
+
|
|
211
|
+
You can use the [`logout()`](./sessions.py#logout) function:
|
|
212
|
+
|
|
213
|
+
```python
|
|
214
|
+
from plain.auth.sessions import logout
|
|
215
|
+
|
|
216
|
+
logout(request)
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
#### How do I invalidate sessions when a user changes their password?
|
|
220
|
+
|
|
221
|
+
By default, if you have [plain.passwords](../../plain-passwords/plain/passwords/README.md) installed, sessions are automatically invalidated when the `password` field changes. This is controlled by the `AUTH_USER_SESSION_HASH_FIELD` setting. You can change this to a different field name, or set it to an empty string to disable this feature.
|
|
222
|
+
|
|
223
|
+
#### How do I get the user model class?
|
|
224
|
+
|
|
225
|
+
You can use the [`get_user_model()`](./sessions.py#get_user_model) function:
|
|
226
|
+
|
|
227
|
+
```python
|
|
228
|
+
from plain.auth.sessions import get_user_model
|
|
229
|
+
|
|
230
|
+
User = get_user_model()
|
|
231
|
+
```
|
|
232
|
+
|
|
168
233
|
## Installation
|
|
169
234
|
|
|
170
235
|
Install the `plain.auth` package from [PyPI](https://pypi.org/project/plain.auth/):
|
|
@@ -16,8 +16,8 @@ from .requests import get_request_user, set_request_user
|
|
|
16
16
|
if TYPE_CHECKING:
|
|
17
17
|
from plain.http import Request
|
|
18
18
|
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
_USER_ID_SESSION_KEY = "_auth_user_id"
|
|
20
|
+
_USER_HASH_SESSION_KEY = "_auth_user_hash"
|
|
21
21
|
|
|
22
22
|
|
|
23
23
|
def get_session_auth_hash(user: Any) -> str:
|
|
@@ -40,10 +40,10 @@ def update_session_auth_hash(request: Request, user: Any) -> None:
|
|
|
40
40
|
session = get_request_session(request)
|
|
41
41
|
session.cycle_key()
|
|
42
42
|
if get_request_user(request) == user:
|
|
43
|
-
session[
|
|
43
|
+
session[_USER_HASH_SESSION_KEY] = get_session_auth_hash(user)
|
|
44
44
|
|
|
45
45
|
|
|
46
|
-
def
|
|
46
|
+
def _get_session_auth_fallback_hash(user: Any) -> Generator[str, None, None]:
|
|
47
47
|
for fallback_secret in settings.SECRET_KEY_FALLBACKS:
|
|
48
48
|
yield _get_session_auth_hash(user, secret=fallback_secret)
|
|
49
49
|
|
|
@@ -71,14 +71,14 @@ def login(request: Request, user: Any) -> None:
|
|
|
71
71
|
else:
|
|
72
72
|
session_auth_hash = ""
|
|
73
73
|
|
|
74
|
-
if
|
|
75
|
-
if int(session[
|
|
74
|
+
if _USER_ID_SESSION_KEY in session:
|
|
75
|
+
if int(session[_USER_ID_SESSION_KEY]) != user.id:
|
|
76
76
|
# To avoid reusing another user's session, create a new, empty
|
|
77
77
|
# session if the existing session corresponds to a different
|
|
78
78
|
# authenticated user.
|
|
79
79
|
session.flush()
|
|
80
80
|
elif session_auth_hash and not hmac.compare_digest(
|
|
81
|
-
force_bytes(session.get(
|
|
81
|
+
force_bytes(session.get(_USER_HASH_SESSION_KEY, "")),
|
|
82
82
|
force_bytes(session_auth_hash),
|
|
83
83
|
):
|
|
84
84
|
# If the session hash does not match the current hash, reset the
|
|
@@ -89,8 +89,8 @@ def login(request: Request, user: Any) -> None:
|
|
|
89
89
|
# typically done after user login to prevent session fixation attacks.
|
|
90
90
|
session.cycle_key()
|
|
91
91
|
|
|
92
|
-
session[
|
|
93
|
-
session[
|
|
92
|
+
session[_USER_ID_SESSION_KEY] = user.id
|
|
93
|
+
session[_USER_HASH_SESSION_KEY] = session_auth_hash
|
|
94
94
|
set_request_user(request, user)
|
|
95
95
|
|
|
96
96
|
|
|
@@ -129,12 +129,12 @@ def get_user(request: Request) -> Any | None:
|
|
|
129
129
|
"""
|
|
130
130
|
session = get_request_session(request)
|
|
131
131
|
|
|
132
|
-
if
|
|
132
|
+
if _USER_ID_SESSION_KEY not in session:
|
|
133
133
|
return None
|
|
134
134
|
|
|
135
135
|
UserModel = get_user_model()
|
|
136
136
|
try:
|
|
137
|
-
user = UserModel.query.get(id=session[
|
|
137
|
+
user = UserModel.query.get(id=session[_USER_ID_SESSION_KEY])
|
|
138
138
|
except UserModel.DoesNotExist:
|
|
139
139
|
return None
|
|
140
140
|
|
|
@@ -145,7 +145,7 @@ def get_user(request: Request) -> Any | None:
|
|
|
145
145
|
# If it has changed (i.e. password changed), then the session
|
|
146
146
|
# is no longer valid and cleared out.
|
|
147
147
|
if settings.AUTH_USER_SESSION_HASH_FIELD:
|
|
148
|
-
session_hash = session.get(
|
|
148
|
+
session_hash = session.get(_USER_HASH_SESSION_KEY)
|
|
149
149
|
if not session_hash:
|
|
150
150
|
session_hash_verified = False
|
|
151
151
|
else:
|
|
@@ -161,10 +161,10 @@ def get_user(request: Request) -> Any | None:
|
|
|
161
161
|
hmac.compare_digest(
|
|
162
162
|
force_bytes(session_hash), force_bytes(fallback_auth_hash)
|
|
163
163
|
)
|
|
164
|
-
for fallback_auth_hash in
|
|
164
|
+
for fallback_auth_hash in _get_session_auth_fallback_hash(user)
|
|
165
165
|
):
|
|
166
166
|
session.cycle_key()
|
|
167
|
-
session[
|
|
167
|
+
session[_USER_HASH_SESSION_KEY] = session_auth_hash
|
|
168
168
|
else:
|
|
169
169
|
session.flush()
|
|
170
170
|
user = None
|
|
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
|
|
File without changes
|
|
File without changes
|