plain.auth 0.23.0__py3-none-any.whl → 0.25.0__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.
plain/auth/CHANGELOG.md CHANGED
@@ -1,5 +1,25 @@
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
+
13
+ ## [0.24.0](https://github.com/dropseed/plain/releases/plain-auth@0.24.0) (2026-01-13)
14
+
15
+ ### What's changed
16
+
17
+ - HTTP exceptions moved from `plain.exceptions` to `plain.http.exceptions` (exported via `plain.http`) ([b61f909](https://github.com/dropseed/plain/commit/b61f909e29))
18
+
19
+ ### Upgrade instructions
20
+
21
+ - Update imports of HTTP exceptions from `plain.exceptions` to `plain.http` (e.g., `from plain.exceptions import ForbiddenError403` becomes `from plain.http import ForbiddenError403`)
22
+
3
23
  ## [0.23.0](https://github.com/dropseed/plain/releases/plain-auth@0.23.0) (2026-01-13)
4
24
 
5
25
  ### What's changed
plain/auth/README.md 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 provides user authentication and authorization for Plain applications. Here's a basic example of checking if a user is logged in:
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
- And restricting a view to logged-in users:
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
- Create your own user model using `plain create users` or manually:
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'll need to pair this package with an authentication method:
87
+ To log users in, you need to pair this package with an authentication method:
87
88
 
88
- - `plain-passwords` - Username/password authentication
89
- - `plain-oauth` - OAuth provider authentication
90
- - `plain-passkeys` (TBD) - WebAuthn/passkey authentication
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
- Example with password authentication:
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,11 +135,11 @@ else:
135
135
 
136
136
  ## Restricting views
137
137
 
138
- Use the [`AuthViewMixin`](./views.py#AuthViewMixin) to restrict views to logged-in users, admin users, or custom logic:
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
142
- from plain.exceptions import ForbiddenError403
142
+ from plain.http import ForbiddenError403
143
143
  from plain.views import View
144
144
 
145
145
 
@@ -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/):
plain/auth/sessions.py CHANGED
@@ -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
- USER_ID_SESSION_KEY = "_auth_user_id"
20
- USER_HASH_SESSION_KEY = "_auth_user_hash"
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[USER_HASH_SESSION_KEY] = get_session_auth_hash(user)
43
+ session[_USER_HASH_SESSION_KEY] = get_session_auth_hash(user)
44
44
 
45
45
 
46
- def get_session_auth_fallback_hash(user: Any) -> Generator[str, None, None]:
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 USER_ID_SESSION_KEY in session:
75
- if int(session[USER_ID_SESSION_KEY]) != user.id:
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(USER_HASH_SESSION_KEY, "")),
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[USER_ID_SESSION_KEY] = user.id
93
- session[USER_HASH_SESSION_KEY] = session_auth_hash
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 USER_ID_SESSION_KEY not in session:
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[USER_ID_SESSION_KEY])
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(USER_HASH_SESSION_KEY)
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 get_session_auth_fallback_hash(user)
164
+ for fallback_auth_hash in _get_session_auth_fallback_hash(user)
165
165
  ):
166
166
  session.cycle_key()
167
- session[USER_HASH_SESSION_KEY] = session_auth_hash
167
+ session[_USER_HASH_SESSION_KEY] = session_auth_hash
168
168
  else:
169
169
  session.flush()
170
170
  user = None
plain/auth/views.py CHANGED
@@ -4,8 +4,9 @@ from functools import cached_property
4
4
  from typing import Any
5
5
  from urllib.parse import urlparse, urlunparse
6
6
 
7
- from plain.exceptions import ForbiddenError403, NotFoundError404
8
7
  from plain.http import (
8
+ ForbiddenError403,
9
+ NotFoundError404,
9
10
  QueryDict,
10
11
  RedirectResponse,
11
12
  ResponseBase,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: plain.auth
3
- Version: 0.23.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 provides user authentication and authorization for Plain applications. Here's a basic example of checking if a user is logged in:
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
- And restricting a view to logged-in users:
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
- Create your own user model using `plain create users` or manually:
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'll need to pair this package with an authentication method:
100
+ To log users in, you need to pair this package with an authentication method:
100
101
 
101
- - `plain-passwords` - Username/password authentication
102
- - `plain-oauth` - OAuth provider authentication
103
- - `plain-passkeys` (TBD) - WebAuthn/passkey authentication
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
- Example with password authentication:
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,11 +148,11 @@ else:
148
148
 
149
149
  ## Restricting views
150
150
 
151
- Use the [`AuthViewMixin`](./views.py#AuthViewMixin) to restrict views to logged-in users, admin users, or custom logic:
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
155
- from plain.exceptions import ForbiddenError403
155
+ from plain.http import ForbiddenError403
156
156
  from plain.views import View
157
157
 
158
158
 
@@ -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/):
@@ -0,0 +1,14 @@
1
+ plain/auth/CHANGELOG.md,sha256=Wny_rlZoVBpoKzwLfbir7k2_Kx2ZgGwIiQqhIETsGw4,9202
2
+ plain/auth/README.md,sha256=qWfAAjLw_VlJ-fwMdgTWtUEmb5vLPwENt7cTGnxkbcQ,5973
3
+ plain/auth/__init__.py,sha256=CrOsS74CPGN1nPTTfie13mPgdyVLRyZ1YwDPIA77uaA,179
4
+ plain/auth/default_settings.py,sha256=65VzDn3j61OMn78Lg6Zuds4A8QKzJJ_0G9KoFqAOIRo,466
5
+ plain/auth/requests.py,sha256=jUlMTOWFt0qfDF_uVkOqaP9rZiCLrAofsmirCwyRGEE,880
6
+ plain/auth/sessions.py,sha256=p3aW_3FsjFLZ1Nk2OHejqL-rhAX5NHSE7aigr_z8L3U,6029
7
+ plain/auth/templates.py,sha256=CVtuIdBgOgYB2o61zpOPJb6nMx_gU61i_3PDQx8FjVs,770
8
+ plain/auth/test.py,sha256=SHawhwarJEMVfaLkjpiuFVSWZoGIMwhzXreU_T1zvCE,1599
9
+ plain/auth/utils.py,sha256=9kKWh1QqxA8Esct-jBvTCdjBYOHpO_Tg1YeV9WxYmxg,1362
10
+ plain/auth/views.py,sha256=Nu4adJ9bhFdiPkGOVqt7BbIA150HG1ojSFEqAnIO3P8,4923
11
+ plain_auth-0.25.0.dist-info/METADATA,sha256=YzLxmVS7JZV6B4ZuGf6XX9slkKMeJntxOGa1bWUleRM,6366
12
+ plain_auth-0.25.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
13
+ plain_auth-0.25.0.dist-info/licenses/LICENSE,sha256=m0D5O7QoH9l5Vz_rrX_9r-C8d9UNr_ciK6Qwac7o6yo,3175
14
+ plain_auth-0.25.0.dist-info/RECORD,,
@@ -1,14 +0,0 @@
1
- plain/auth/CHANGELOG.md,sha256=5Qmh-kOTzUDAuT7ncQs2L6PGsmGh7ToBlwZQE1ZBlUg,8413
2
- plain/auth/README.md,sha256=397DaiamLW8JHoVwNrrIO8NAgRS9jWOdeAZB_Oq135A,4032
3
- plain/auth/__init__.py,sha256=CrOsS74CPGN1nPTTfie13mPgdyVLRyZ1YwDPIA77uaA,179
4
- plain/auth/default_settings.py,sha256=65VzDn3j61OMn78Lg6Zuds4A8QKzJJ_0G9KoFqAOIRo,466
5
- plain/auth/requests.py,sha256=jUlMTOWFt0qfDF_uVkOqaP9rZiCLrAofsmirCwyRGEE,880
6
- plain/auth/sessions.py,sha256=xDp1EiB0cV5w3L5XioqO8vs77wnF8cvreExms3-e744,6015
7
- plain/auth/templates.py,sha256=CVtuIdBgOgYB2o61zpOPJb6nMx_gU61i_3PDQx8FjVs,770
8
- plain/auth/test.py,sha256=SHawhwarJEMVfaLkjpiuFVSWZoGIMwhzXreU_T1zvCE,1599
9
- plain/auth/utils.py,sha256=9kKWh1QqxA8Esct-jBvTCdjBYOHpO_Tg1YeV9WxYmxg,1362
10
- plain/auth/views.py,sha256=zQO0LFioYIv35xXe1RBx77MLIJjog1neuX7aIT0wLhE,4943
11
- plain_auth-0.23.0.dist-info/METADATA,sha256=8CCcWoKfBjxlFTsykBTgexSgSqDSvoUR8ZnHONQBdwg,4425
12
- plain_auth-0.23.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
13
- plain_auth-0.23.0.dist-info/licenses/LICENSE,sha256=m0D5O7QoH9l5Vz_rrX_9r-C8d9UNr_ciK6Qwac7o6yo,3175
14
- plain_auth-0.23.0.dist-info/RECORD,,