plain.auth 0.24.0__py3-none-any.whl → 0.25.1__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.1](https://github.com/dropseed/plain/releases/plain-auth@0.25.1) (2026-01-28)
4
+
5
+ ### What's changed
6
+
7
+ - Added Settings section to README ([803fee1ad5](https://github.com/dropseed/plain/commit/803fee1ad5))
8
+
9
+ ### Upgrade instructions
10
+
11
+ - No changes required.
12
+
13
+ ## [0.25.0](https://github.com/dropseed/plain/releases/plain-auth@0.25.0) (2026-01-13)
14
+
15
+ ### What's changed
16
+
17
+ - Improved README documentation with better examples and consistent structure ([da37a78](https://github.com/dropseed/plain/commit/da37a78fbb))
18
+
19
+ ### Upgrade instructions
20
+
21
+ - No changes required
22
+
3
23
  ## [0.24.0](https://github.com/dropseed/plain/releases/plain-auth@0.24.0) (2026-01-13)
4
24
 
5
25
  ### What's changed
plain/auth/README.md CHANGED
@@ -9,14 +9,16 @@
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
+ - [Settings](#settings)
14
+ - [FAQs](#faqs)
12
15
  - [Installation](#installation)
13
16
 
14
17
  ## Overview
15
18
 
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:
19
+ The `plain.auth` package handles user authentication and authorization for Plain applications. You can check if a user is logged in like this:
17
20
 
18
21
  ```python
19
- # In a view
20
22
  from plain.auth import get_request_user
21
23
 
22
24
  user = get_request_user(request)
@@ -26,7 +28,7 @@ else:
26
28
  print("You are not logged in.")
27
29
  ```
28
30
 
29
- And restricting a view to logged-in users:
31
+ You can restrict a view to logged-in users using [`AuthViewMixin`](./views.py#AuthViewMixin):
30
32
 
31
33
  ```python
32
34
  from plain.auth.views import AuthViewMixin
@@ -63,7 +65,7 @@ AUTH_LOGIN_URL = "login"
63
65
 
64
66
  ### Creating a user model
65
67
 
66
- Create your own user model using `plain create users` or manually:
68
+ You can create your own user model using `plain create users` or manually:
67
69
 
68
70
  ```python
69
71
  # app/users/models.py
@@ -83,14 +85,13 @@ class User(models.Model):
83
85
 
84
86
  ### Login views
85
87
 
86
- To log users in, you'll need to pair this package with an authentication method:
88
+ To log users in, you need to pair this package with an authentication method:
87
89
 
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
90
+ - [plain.passwords](../../plain-passwords/plain/passwords/README.md) - Username/password authentication
91
+ - [plain.oauth](../../plain-oauth/plain/oauth/README.md) - OAuth provider authentication
92
+ - [plain.loginlink](../../plain-loginlink/plain/loginlink/README.md) - Magic link authentication
92
93
 
93
- Example with password authentication:
94
+ Here's an example with password authentication:
94
95
 
95
96
  ```python
96
97
  # app/urls.py
@@ -111,7 +112,7 @@ urlpatterns = [
111
112
 
112
113
  ## Checking if a user is logged in
113
114
 
114
- In templates, use the `get_current_user()` function:
115
+ In templates, you can use the `get_current_user()` function:
115
116
 
116
117
  ```html
117
118
  {% if get_current_user() %}
@@ -121,7 +122,7 @@ In templates, use the `get_current_user()` function:
121
122
  {% endif %}
122
123
  ```
123
124
 
124
- In Python code, use `get_request_user()`:
125
+ In Python code, use [`get_request_user()`](./requests.py#get_request_user):
125
126
 
126
127
  ```python
127
128
  from plain.auth import get_request_user
@@ -135,7 +136,7 @@ else:
135
136
 
136
137
  ## Restricting views
137
138
 
138
- Use the [`AuthViewMixin`](./views.py#AuthViewMixin) to restrict views to logged-in users, admin users, or custom logic:
139
+ You can use [`AuthViewMixin`](./views.py#AuthViewMixin) to restrict views to logged-in users, admin users, or custom logic:
139
140
 
140
141
  ```python
141
142
  from plain.auth.views import AuthViewMixin
@@ -165,6 +166,81 @@ The [`AuthViewMixin`](./views.py#AuthViewMixin) provides:
165
166
  - `admin_required` - Requires `user.is_admin` to be True
166
167
  - `check_auth()` - Override for custom authorization logic
167
168
 
169
+ ## Testing with authenticated users
170
+
171
+ When writing tests, you can use [`login_client()`](./test.py#login_client) to simulate an authenticated user:
172
+
173
+ ```python
174
+ from plain.auth.test import login_client
175
+ from plain.test import Client
176
+
177
+ from app.users.models import User
178
+
179
+
180
+ def test_profile_view():
181
+ user = User.objects.create(email="test@example.com")
182
+ client = Client()
183
+ login_client(client, user)
184
+
185
+ response = client.get("/profile/")
186
+ assert response.status_code == 200
187
+ ```
188
+
189
+ You can also log out a test user with [`logout_client()`](./test.py#logout_client):
190
+
191
+ ```python
192
+ from plain.auth.test import login_client, logout_client
193
+
194
+ # ... after logging in
195
+ logout_client(client)
196
+ ```
197
+
198
+ ## Settings
199
+
200
+ | Setting | Default | Env var |
201
+ | ------------------------------ | -------------------- | ------------------------------------ |
202
+ | `AUTH_USER_MODEL` | Required | `PLAIN_AUTH_USER_MODEL` |
203
+ | `AUTH_LOGIN_URL` | Required | `PLAIN_AUTH_LOGIN_URL` |
204
+ | `AUTH_USER_SESSION_HASH_FIELD` | `"password"` or `""` | `PLAIN_AUTH_USER_SESSION_HASH_FIELD` |
205
+
206
+ See [`default_settings.py`](./default_settings.py) for more details.
207
+
208
+ ## FAQs
209
+
210
+ #### How do I log in a user programmatically?
211
+
212
+ You can use the [`login()`](./sessions.py#login) function to log in a user:
213
+
214
+ ```python
215
+ from plain.auth.sessions import login
216
+
217
+ login(request, user)
218
+ ```
219
+
220
+ #### How do I log out a user programmatically?
221
+
222
+ You can use the [`logout()`](./sessions.py#logout) function:
223
+
224
+ ```python
225
+ from plain.auth.sessions import logout
226
+
227
+ logout(request)
228
+ ```
229
+
230
+ #### How do I invalidate sessions when a user changes their password?
231
+
232
+ 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.
233
+
234
+ #### How do I get the user model class?
235
+
236
+ You can use the [`get_user_model()`](./sessions.py#get_user_model) function:
237
+
238
+ ```python
239
+ from plain.auth.sessions import get_user_model
240
+
241
+ User = get_user_model()
242
+ ```
243
+
168
244
  ## Installation
169
245
 
170
246
  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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: plain.auth
3
- Version: 0.24.0
3
+ Version: 0.25.1
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,16 @@ 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
+ - [Settings](#settings)
27
+ - [FAQs](#faqs)
25
28
  - [Installation](#installation)
26
29
 
27
30
  ## Overview
28
31
 
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:
32
+ The `plain.auth` package handles user authentication and authorization for Plain applications. You can check if a user is logged in like this:
30
33
 
31
34
  ```python
32
- # In a view
33
35
  from plain.auth import get_request_user
34
36
 
35
37
  user = get_request_user(request)
@@ -39,7 +41,7 @@ else:
39
41
  print("You are not logged in.")
40
42
  ```
41
43
 
42
- And restricting a view to logged-in users:
44
+ You can restrict a view to logged-in users using [`AuthViewMixin`](./views.py#AuthViewMixin):
43
45
 
44
46
  ```python
45
47
  from plain.auth.views import AuthViewMixin
@@ -76,7 +78,7 @@ AUTH_LOGIN_URL = "login"
76
78
 
77
79
  ### Creating a user model
78
80
 
79
- Create your own user model using `plain create users` or manually:
81
+ You can create your own user model using `plain create users` or manually:
80
82
 
81
83
  ```python
82
84
  # app/users/models.py
@@ -96,14 +98,13 @@ class User(models.Model):
96
98
 
97
99
  ### Login views
98
100
 
99
- To log users in, you'll need to pair this package with an authentication method:
101
+ To log users in, you need to pair this package with an authentication method:
100
102
 
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
103
+ - [plain.passwords](../../plain-passwords/plain/passwords/README.md) - Username/password authentication
104
+ - [plain.oauth](../../plain-oauth/plain/oauth/README.md) - OAuth provider authentication
105
+ - [plain.loginlink](../../plain-loginlink/plain/loginlink/README.md) - Magic link authentication
105
106
 
106
- Example with password authentication:
107
+ Here's an example with password authentication:
107
108
 
108
109
  ```python
109
110
  # app/urls.py
@@ -124,7 +125,7 @@ urlpatterns = [
124
125
 
125
126
  ## Checking if a user is logged in
126
127
 
127
- In templates, use the `get_current_user()` function:
128
+ In templates, you can use the `get_current_user()` function:
128
129
 
129
130
  ```html
130
131
  {% if get_current_user() %}
@@ -134,7 +135,7 @@ In templates, use the `get_current_user()` function:
134
135
  {% endif %}
135
136
  ```
136
137
 
137
- In Python code, use `get_request_user()`:
138
+ In Python code, use [`get_request_user()`](./requests.py#get_request_user):
138
139
 
139
140
  ```python
140
141
  from plain.auth import get_request_user
@@ -148,7 +149,7 @@ else:
148
149
 
149
150
  ## Restricting views
150
151
 
151
- Use the [`AuthViewMixin`](./views.py#AuthViewMixin) to restrict views to logged-in users, admin users, or custom logic:
152
+ You can use [`AuthViewMixin`](./views.py#AuthViewMixin) to restrict views to logged-in users, admin users, or custom logic:
152
153
 
153
154
  ```python
154
155
  from plain.auth.views import AuthViewMixin
@@ -178,6 +179,81 @@ The [`AuthViewMixin`](./views.py#AuthViewMixin) provides:
178
179
  - `admin_required` - Requires `user.is_admin` to be True
179
180
  - `check_auth()` - Override for custom authorization logic
180
181
 
182
+ ## Testing with authenticated users
183
+
184
+ When writing tests, you can use [`login_client()`](./test.py#login_client) to simulate an authenticated user:
185
+
186
+ ```python
187
+ from plain.auth.test import login_client
188
+ from plain.test import Client
189
+
190
+ from app.users.models import User
191
+
192
+
193
+ def test_profile_view():
194
+ user = User.objects.create(email="test@example.com")
195
+ client = Client()
196
+ login_client(client, user)
197
+
198
+ response = client.get("/profile/")
199
+ assert response.status_code == 200
200
+ ```
201
+
202
+ You can also log out a test user with [`logout_client()`](./test.py#logout_client):
203
+
204
+ ```python
205
+ from plain.auth.test import login_client, logout_client
206
+
207
+ # ... after logging in
208
+ logout_client(client)
209
+ ```
210
+
211
+ ## Settings
212
+
213
+ | Setting | Default | Env var |
214
+ | ------------------------------ | -------------------- | ------------------------------------ |
215
+ | `AUTH_USER_MODEL` | Required | `PLAIN_AUTH_USER_MODEL` |
216
+ | `AUTH_LOGIN_URL` | Required | `PLAIN_AUTH_LOGIN_URL` |
217
+ | `AUTH_USER_SESSION_HASH_FIELD` | `"password"` or `""` | `PLAIN_AUTH_USER_SESSION_HASH_FIELD` |
218
+
219
+ See [`default_settings.py`](./default_settings.py) for more details.
220
+
221
+ ## FAQs
222
+
223
+ #### How do I log in a user programmatically?
224
+
225
+ You can use the [`login()`](./sessions.py#login) function to log in a user:
226
+
227
+ ```python
228
+ from plain.auth.sessions import login
229
+
230
+ login(request, user)
231
+ ```
232
+
233
+ #### How do I log out a user programmatically?
234
+
235
+ You can use the [`logout()`](./sessions.py#logout) function:
236
+
237
+ ```python
238
+ from plain.auth.sessions import logout
239
+
240
+ logout(request)
241
+ ```
242
+
243
+ #### How do I invalidate sessions when a user changes their password?
244
+
245
+ 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.
246
+
247
+ #### How do I get the user model class?
248
+
249
+ You can use the [`get_user_model()`](./sessions.py#get_user_model) function:
250
+
251
+ ```python
252
+ from plain.auth.sessions import get_user_model
253
+
254
+ User = get_user_model()
255
+ ```
256
+
181
257
  ## Installation
182
258
 
183
259
  Install the `plain.auth` package from [PyPI](https://pypi.org/project/plain.auth/):
@@ -1,14 +1,14 @@
1
- plain/auth/CHANGELOG.md,sha256=VeYgWu5r6BNrYZjcPfSSif2vs7WM8wFRBHNQEINk6KY,8901
2
- plain/auth/README.md,sha256=wL184DYe9n6FqYR4IzBlN6h-EQsSy-gZA-c9x1w7e7U,4026
1
+ plain/auth/CHANGELOG.md,sha256=t2aRedYSsMgZ-zW_Iq8H_bPmesQpYO21HLBWcGuOL7U,9464
2
+ plain/auth/README.md,sha256=nq4CJ43yBNkeKWUNdajx7PCNr4i6qxKb8dkfqo0bxG4,6566
3
3
  plain/auth/__init__.py,sha256=CrOsS74CPGN1nPTTfie13mPgdyVLRyZ1YwDPIA77uaA,179
4
4
  plain/auth/default_settings.py,sha256=65VzDn3j61OMn78Lg6Zuds4A8QKzJJ_0G9KoFqAOIRo,466
5
5
  plain/auth/requests.py,sha256=jUlMTOWFt0qfDF_uVkOqaP9rZiCLrAofsmirCwyRGEE,880
6
- plain/auth/sessions.py,sha256=xDp1EiB0cV5w3L5XioqO8vs77wnF8cvreExms3-e744,6015
6
+ plain/auth/sessions.py,sha256=p3aW_3FsjFLZ1Nk2OHejqL-rhAX5NHSE7aigr_z8L3U,6029
7
7
  plain/auth/templates.py,sha256=CVtuIdBgOgYB2o61zpOPJb6nMx_gU61i_3PDQx8FjVs,770
8
8
  plain/auth/test.py,sha256=SHawhwarJEMVfaLkjpiuFVSWZoGIMwhzXreU_T1zvCE,1599
9
9
  plain/auth/utils.py,sha256=9kKWh1QqxA8Esct-jBvTCdjBYOHpO_Tg1YeV9WxYmxg,1362
10
10
  plain/auth/views.py,sha256=Nu4adJ9bhFdiPkGOVqt7BbIA150HG1ojSFEqAnIO3P8,4923
11
- plain_auth-0.24.0.dist-info/METADATA,sha256=6tF2zaZLavS1aiZN6FVw3keWoBakjmxPRRtEQdcHxjw,4419
12
- plain_auth-0.24.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
13
- plain_auth-0.24.0.dist-info/licenses/LICENSE,sha256=m0D5O7QoH9l5Vz_rrX_9r-C8d9UNr_ciK6Qwac7o6yo,3175
14
- plain_auth-0.24.0.dist-info/RECORD,,
11
+ plain_auth-0.25.1.dist-info/METADATA,sha256=Xmt3JziZGcZnLH6BQG9-duU8OEmx-6JYXxSZf-29cAo,6959
12
+ plain_auth-0.25.1.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
13
+ plain_auth-0.25.1.dist-info/licenses/LICENSE,sha256=m0D5O7QoH9l5Vz_rrX_9r-C8d9UNr_ciK6Qwac7o6yo,3175
14
+ plain_auth-0.25.1.dist-info/RECORD,,