plain.auth 0.24.0__tar.gz → 0.25.1__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.1}/.gitignore +1 -1
- {plain_auth-0.24.0 → plain_auth-0.25.1}/PKG-INFO +90 -14
- {plain_auth-0.24.0 → plain_auth-0.25.1}/plain/auth/CHANGELOG.md +20 -0
- plain_auth-0.25.1/plain/auth/README.md +250 -0
- {plain_auth-0.24.0 → plain_auth-0.25.1}/plain/auth/sessions.py +14 -14
- {plain_auth-0.24.0 → plain_auth-0.25.1}/pyproject.toml +1 -1
- plain_auth-0.24.0/plain/auth/README.md +0 -174
- {plain_auth-0.24.0 → plain_auth-0.25.1}/LICENSE +0 -0
- {plain_auth-0.24.0 → plain_auth-0.25.1}/README.md +0 -0
- {plain_auth-0.24.0 → plain_auth-0.25.1}/plain/auth/__init__.py +0 -0
- {plain_auth-0.24.0 → plain_auth-0.25.1}/plain/auth/default_settings.py +0 -0
- {plain_auth-0.24.0 → plain_auth-0.25.1}/plain/auth/requests.py +0 -0
- {plain_auth-0.24.0 → plain_auth-0.25.1}/plain/auth/templates.py +0 -0
- {plain_auth-0.24.0 → plain_auth-0.25.1}/plain/auth/test.py +0 -0
- {plain_auth-0.24.0 → plain_auth-0.25.1}/plain/auth/utils.py +0 -0
- {plain_auth-0.24.0 → plain_auth-0.25.1}/plain/auth/views.py +0 -0
- {plain_auth-0.24.0 → plain_auth-0.25.1}/tests/app/settings.py +0 -0
- {plain_auth-0.24.0 → plain_auth-0.25.1}/tests/app/urls.py +0 -0
- {plain_auth-0.24.0 → plain_auth-0.25.1}/tests/app/users/migrations/0001_initial.py +0 -0
- {plain_auth-0.24.0 → plain_auth-0.25.1}/tests/app/users/migrations/__init__.py +0 -0
- {plain_auth-0.24.0 → plain_auth-0.25.1}/tests/app/users/models.py +0 -0
- {plain_auth-0.24.0 → plain_auth-0.25.1}/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.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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
101
|
+
To log users in, you need to pair this package with an authentication method:
|
|
100
102
|
|
|
101
|
-
-
|
|
102
|
-
-
|
|
103
|
-
-
|
|
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
|
-
|
|
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
|
-
|
|
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,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
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
# plain.auth
|
|
2
|
+
|
|
3
|
+
**Add users to your app and decide what they can access.**
|
|
4
|
+
|
|
5
|
+
- [Overview](#overview)
|
|
6
|
+
- [Authentication setup](#authentication-setup)
|
|
7
|
+
- [Settings configuration](#settings-configuration)
|
|
8
|
+
- [Creating a user model](#creating-a-user-model)
|
|
9
|
+
- [Login views](#login-views)
|
|
10
|
+
- [Checking if a user is logged in](#checking-if-a-user-is-logged-in)
|
|
11
|
+
- [Restricting views](#restricting-views)
|
|
12
|
+
- [Testing with authenticated users](#testing-with-authenticated-users)
|
|
13
|
+
- [Settings](#settings)
|
|
14
|
+
- [FAQs](#faqs)
|
|
15
|
+
- [Installation](#installation)
|
|
16
|
+
|
|
17
|
+
## Overview
|
|
18
|
+
|
|
19
|
+
The `plain.auth` package handles user authentication and authorization for Plain applications. You can check if a user is logged in like this:
|
|
20
|
+
|
|
21
|
+
```python
|
|
22
|
+
from plain.auth import get_request_user
|
|
23
|
+
|
|
24
|
+
user = get_request_user(request)
|
|
25
|
+
if user:
|
|
26
|
+
print(f"Hello, {user.email}!")
|
|
27
|
+
else:
|
|
28
|
+
print("You are not logged in.")
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
You can restrict a view to logged-in users using [`AuthViewMixin`](./views.py#AuthViewMixin):
|
|
32
|
+
|
|
33
|
+
```python
|
|
34
|
+
from plain.auth.views import AuthViewMixin
|
|
35
|
+
from plain.views import View
|
|
36
|
+
|
|
37
|
+
class ProfileView(AuthViewMixin, View):
|
|
38
|
+
login_required = True
|
|
39
|
+
|
|
40
|
+
def get(self):
|
|
41
|
+
return f"Welcome, {self.user.email}!"
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Authentication setup
|
|
45
|
+
|
|
46
|
+
### Settings configuration
|
|
47
|
+
|
|
48
|
+
Configure your authentication settings in `app/settings.py`:
|
|
49
|
+
|
|
50
|
+
```python
|
|
51
|
+
INSTALLED_PACKAGES = [
|
|
52
|
+
# ...
|
|
53
|
+
"plain.auth",
|
|
54
|
+
"plain.sessions",
|
|
55
|
+
"plain.passwords", # Or another auth method
|
|
56
|
+
]
|
|
57
|
+
|
|
58
|
+
MIDDLEWARE = [
|
|
59
|
+
"plain.sessions.middleware.SessionMiddleware",
|
|
60
|
+
]
|
|
61
|
+
|
|
62
|
+
AUTH_USER_MODEL = "users.User"
|
|
63
|
+
AUTH_LOGIN_URL = "login"
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### Creating a user model
|
|
67
|
+
|
|
68
|
+
You can create your own user model using `plain create users` or manually:
|
|
69
|
+
|
|
70
|
+
```python
|
|
71
|
+
# app/users/models.py
|
|
72
|
+
from plain import models
|
|
73
|
+
from plain.passwords.models import PasswordField
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class User(models.Model):
|
|
77
|
+
email = models.EmailField()
|
|
78
|
+
password = PasswordField()
|
|
79
|
+
is_admin = models.BooleanField(default=False)
|
|
80
|
+
created_at = models.DateTimeField(auto_now_add=True)
|
|
81
|
+
|
|
82
|
+
def __str__(self):
|
|
83
|
+
return self.email
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### Login views
|
|
87
|
+
|
|
88
|
+
To log users in, you need to pair this package with an authentication method:
|
|
89
|
+
|
|
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
|
|
93
|
+
|
|
94
|
+
Here's an example with password authentication:
|
|
95
|
+
|
|
96
|
+
```python
|
|
97
|
+
# app/urls.py
|
|
98
|
+
from plain.auth.views import LogoutView
|
|
99
|
+
from plain.urls import path
|
|
100
|
+
from plain.passwords.views import PasswordLoginView
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
class LoginView(PasswordLoginView):
|
|
104
|
+
template_name = "login.html"
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
urlpatterns = [
|
|
108
|
+
path("logout/", LogoutView, name="logout"),
|
|
109
|
+
path("login/", LoginView, name="login"),
|
|
110
|
+
]
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## Checking if a user is logged in
|
|
114
|
+
|
|
115
|
+
In templates, you can use the `get_current_user()` function:
|
|
116
|
+
|
|
117
|
+
```html
|
|
118
|
+
{% if get_current_user() %}
|
|
119
|
+
<p>Hello, {{ get_current_user().email }}!</p>
|
|
120
|
+
{% else %}
|
|
121
|
+
<p>You are not logged in.</p>
|
|
122
|
+
{% endif %}
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
In Python code, use [`get_request_user()`](./requests.py#get_request_user):
|
|
126
|
+
|
|
127
|
+
```python
|
|
128
|
+
from plain.auth import get_request_user
|
|
129
|
+
|
|
130
|
+
user = get_request_user(request)
|
|
131
|
+
if user:
|
|
132
|
+
print(f"Hello, {user.email}!")
|
|
133
|
+
else:
|
|
134
|
+
print("You are not logged in.")
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
## Restricting views
|
|
138
|
+
|
|
139
|
+
You can use [`AuthViewMixin`](./views.py#AuthViewMixin) to restrict views to logged-in users, admin users, or custom logic:
|
|
140
|
+
|
|
141
|
+
```python
|
|
142
|
+
from plain.auth.views import AuthViewMixin
|
|
143
|
+
from plain.http import ForbiddenError403
|
|
144
|
+
from plain.views import View
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
class LoggedInView(AuthViewMixin, View):
|
|
148
|
+
login_required = True
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
class AdminOnlyView(AuthViewMixin, View):
|
|
152
|
+
login_required = True
|
|
153
|
+
admin_required = True
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
class CustomPermissionView(AuthViewMixin, View):
|
|
157
|
+
def check_auth(self):
|
|
158
|
+
super().check_auth()
|
|
159
|
+
if not self.user.is_special:
|
|
160
|
+
raise ForbiddenError403("You're not special!")
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
The [`AuthViewMixin`](./views.py#AuthViewMixin) provides:
|
|
164
|
+
|
|
165
|
+
- `login_required` - Requires a logged-in user
|
|
166
|
+
- `admin_required` - Requires `user.is_admin` to be True
|
|
167
|
+
- `check_auth()` - Override for custom authorization logic
|
|
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
|
+
|
|
244
|
+
## Installation
|
|
245
|
+
|
|
246
|
+
Install the `plain.auth` package from [PyPI](https://pypi.org/project/plain.auth/):
|
|
247
|
+
|
|
248
|
+
```bash
|
|
249
|
+
uv add plain.auth
|
|
250
|
+
```
|
|
@@ -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
|
|
@@ -1,174 +0,0 @@
|
|
|
1
|
-
# plain.auth
|
|
2
|
-
|
|
3
|
-
**Add users to your app and decide what they can access.**
|
|
4
|
-
|
|
5
|
-
- [Overview](#overview)
|
|
6
|
-
- [Authentication setup](#authentication-setup)
|
|
7
|
-
- [Settings configuration](#settings-configuration)
|
|
8
|
-
- [Creating a user model](#creating-a-user-model)
|
|
9
|
-
- [Login views](#login-views)
|
|
10
|
-
- [Checking if a user is logged in](#checking-if-a-user-is-logged-in)
|
|
11
|
-
- [Restricting views](#restricting-views)
|
|
12
|
-
- [Installation](#installation)
|
|
13
|
-
|
|
14
|
-
## Overview
|
|
15
|
-
|
|
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:
|
|
17
|
-
|
|
18
|
-
```python
|
|
19
|
-
# In a view
|
|
20
|
-
from plain.auth import get_request_user
|
|
21
|
-
|
|
22
|
-
user = get_request_user(request)
|
|
23
|
-
if user:
|
|
24
|
-
print(f"Hello, {user.email}!")
|
|
25
|
-
else:
|
|
26
|
-
print("You are not logged in.")
|
|
27
|
-
```
|
|
28
|
-
|
|
29
|
-
And restricting a view to logged-in users:
|
|
30
|
-
|
|
31
|
-
```python
|
|
32
|
-
from plain.auth.views import AuthViewMixin
|
|
33
|
-
from plain.views import View
|
|
34
|
-
|
|
35
|
-
class ProfileView(AuthViewMixin, View):
|
|
36
|
-
login_required = True
|
|
37
|
-
|
|
38
|
-
def get(self):
|
|
39
|
-
return f"Welcome, {self.user.email}!"
|
|
40
|
-
```
|
|
41
|
-
|
|
42
|
-
## Authentication setup
|
|
43
|
-
|
|
44
|
-
### Settings configuration
|
|
45
|
-
|
|
46
|
-
Configure your authentication settings in `app/settings.py`:
|
|
47
|
-
|
|
48
|
-
```python
|
|
49
|
-
INSTALLED_PACKAGES = [
|
|
50
|
-
# ...
|
|
51
|
-
"plain.auth",
|
|
52
|
-
"plain.sessions",
|
|
53
|
-
"plain.passwords", # Or another auth method
|
|
54
|
-
]
|
|
55
|
-
|
|
56
|
-
MIDDLEWARE = [
|
|
57
|
-
"plain.sessions.middleware.SessionMiddleware",
|
|
58
|
-
]
|
|
59
|
-
|
|
60
|
-
AUTH_USER_MODEL = "users.User"
|
|
61
|
-
AUTH_LOGIN_URL = "login"
|
|
62
|
-
```
|
|
63
|
-
|
|
64
|
-
### Creating a user model
|
|
65
|
-
|
|
66
|
-
Create your own user model using `plain create users` or manually:
|
|
67
|
-
|
|
68
|
-
```python
|
|
69
|
-
# app/users/models.py
|
|
70
|
-
from plain import models
|
|
71
|
-
from plain.passwords.models import PasswordField
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
class User(models.Model):
|
|
75
|
-
email = models.EmailField()
|
|
76
|
-
password = PasswordField()
|
|
77
|
-
is_admin = models.BooleanField(default=False)
|
|
78
|
-
created_at = models.DateTimeField(auto_now_add=True)
|
|
79
|
-
|
|
80
|
-
def __str__(self):
|
|
81
|
-
return self.email
|
|
82
|
-
```
|
|
83
|
-
|
|
84
|
-
### Login views
|
|
85
|
-
|
|
86
|
-
To log users in, you'll need to pair this package with an authentication method:
|
|
87
|
-
|
|
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
|
|
92
|
-
|
|
93
|
-
Example with password authentication:
|
|
94
|
-
|
|
95
|
-
```python
|
|
96
|
-
# app/urls.py
|
|
97
|
-
from plain.auth.views import LogoutView
|
|
98
|
-
from plain.urls import path
|
|
99
|
-
from plain.passwords.views import PasswordLoginView
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
class LoginView(PasswordLoginView):
|
|
103
|
-
template_name = "login.html"
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
urlpatterns = [
|
|
107
|
-
path("logout/", LogoutView, name="logout"),
|
|
108
|
-
path("login/", LoginView, name="login"),
|
|
109
|
-
]
|
|
110
|
-
```
|
|
111
|
-
|
|
112
|
-
## Checking if a user is logged in
|
|
113
|
-
|
|
114
|
-
In templates, use the `get_current_user()` function:
|
|
115
|
-
|
|
116
|
-
```html
|
|
117
|
-
{% if get_current_user() %}
|
|
118
|
-
<p>Hello, {{ get_current_user().email }}!</p>
|
|
119
|
-
{% else %}
|
|
120
|
-
<p>You are not logged in.</p>
|
|
121
|
-
{% endif %}
|
|
122
|
-
```
|
|
123
|
-
|
|
124
|
-
In Python code, use `get_request_user()`:
|
|
125
|
-
|
|
126
|
-
```python
|
|
127
|
-
from plain.auth import get_request_user
|
|
128
|
-
|
|
129
|
-
user = get_request_user(request)
|
|
130
|
-
if user:
|
|
131
|
-
print(f"Hello, {user.email}!")
|
|
132
|
-
else:
|
|
133
|
-
print("You are not logged in.")
|
|
134
|
-
```
|
|
135
|
-
|
|
136
|
-
## Restricting views
|
|
137
|
-
|
|
138
|
-
Use the [`AuthViewMixin`](./views.py#AuthViewMixin) to restrict views to logged-in users, admin users, or custom logic:
|
|
139
|
-
|
|
140
|
-
```python
|
|
141
|
-
from plain.auth.views import AuthViewMixin
|
|
142
|
-
from plain.http import ForbiddenError403
|
|
143
|
-
from plain.views import View
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
class LoggedInView(AuthViewMixin, View):
|
|
147
|
-
login_required = True
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
class AdminOnlyView(AuthViewMixin, View):
|
|
151
|
-
login_required = True
|
|
152
|
-
admin_required = True
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
class CustomPermissionView(AuthViewMixin, View):
|
|
156
|
-
def check_auth(self):
|
|
157
|
-
super().check_auth()
|
|
158
|
-
if not self.user.is_special:
|
|
159
|
-
raise ForbiddenError403("You're not special!")
|
|
160
|
-
```
|
|
161
|
-
|
|
162
|
-
The [`AuthViewMixin`](./views.py#AuthViewMixin) provides:
|
|
163
|
-
|
|
164
|
-
- `login_required` - Requires a logged-in user
|
|
165
|
-
- `admin_required` - Requires `user.is_admin` to be True
|
|
166
|
-
- `check_auth()` - Override for custom authorization logic
|
|
167
|
-
|
|
168
|
-
## Installation
|
|
169
|
-
|
|
170
|
-
Install the `plain.auth` package from [PyPI](https://pypi.org/project/plain.auth/):
|
|
171
|
-
|
|
172
|
-
```bash
|
|
173
|
-
uv add plain.auth
|
|
174
|
-
```
|
|
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
|