simple-module-auth 0.0.31__tar.gz → 0.0.33__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.
- {simple_module_auth-0.0.31 → simple_module_auth-0.0.33}/PKG-INFO +3 -3
- {simple_module_auth-0.0.31 → simple_module_auth-0.0.33}/auth/middleware.py +17 -3
- {simple_module_auth-0.0.31 → simple_module_auth-0.0.33}/pyproject.toml +3 -3
- {simple_module_auth-0.0.31 → simple_module_auth-0.0.33}/tests/test_auth_middleware.py +70 -0
- {simple_module_auth-0.0.31 → simple_module_auth-0.0.33}/.gitignore +0 -0
- {simple_module_auth-0.0.31 → simple_module_auth-0.0.33}/LICENSE +0 -0
- {simple_module_auth-0.0.31 → simple_module_auth-0.0.33}/README.md +0 -0
- {simple_module_auth-0.0.31 → simple_module_auth-0.0.33}/auth/__init__.py +0 -0
- {simple_module_auth-0.0.31 → simple_module_auth-0.0.33}/auth/contracts/__init__.py +0 -0
- {simple_module_auth-0.0.31 → simple_module_auth-0.0.33}/auth/contracts/provider.py +0 -0
- {simple_module_auth-0.0.31 → simple_module_auth-0.0.33}/auth/contracts/resolver.py +0 -0
- {simple_module_auth-0.0.31 → simple_module_auth-0.0.33}/auth/contracts/schemas.py +0 -0
- {simple_module_auth-0.0.31 → simple_module_auth-0.0.33}/auth/deps.py +0 -0
- {simple_module_auth-0.0.31 → simple_module_auth-0.0.33}/auth/locales/en.json +0 -0
- {simple_module_auth-0.0.31 → simple_module_auth-0.0.33}/auth/locales/es.json +0 -0
- {simple_module_auth-0.0.31 → simple_module_auth-0.0.33}/auth/module.py +0 -0
- {simple_module_auth-0.0.31 → simple_module_auth-0.0.33}/auth/py.typed +0 -0
- {simple_module_auth-0.0.31 → simple_module_auth-0.0.33}/auth/state.py +0 -0
- {simple_module_auth-0.0.31 → simple_module_auth-0.0.33}/package.json +0 -0
- {simple_module_auth-0.0.31 → simple_module_auth-0.0.33}/tests/test_auth_provider_protocol.py +0 -0
- {simple_module_auth-0.0.31 → simple_module_auth-0.0.33}/tests/test_deps.py +0 -0
- {simple_module_auth-0.0.31 → simple_module_auth-0.0.33}/tests/test_module.py +0 -0
- {simple_module_auth-0.0.31 → simple_module_auth-0.0.33}/tests/test_resolver_registry.py +0 -0
- {simple_module_auth-0.0.31 → simple_module_auth-0.0.33}/tests/test_user_context.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: simple_module_auth
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.33
|
|
4
4
|
Summary: Session-cookie authentication primitives — middleware, login/logout, redirect helpers for simple_module
|
|
5
5
|
Project-URL: Homepage, https://github.com/antosubash/simple_module_python
|
|
6
6
|
Project-URL: Repository, https://github.com/antosubash/simple_module_python
|
|
@@ -22,8 +22,8 @@ Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
|
|
|
22
22
|
Classifier: Typing :: Typed
|
|
23
23
|
Requires-Python: >=3.12
|
|
24
24
|
Requires-Dist: itsdangerous>=2.2
|
|
25
|
-
Requires-Dist: simple-module-core==0.0.
|
|
26
|
-
Requires-Dist: simple-module-db==0.0.
|
|
25
|
+
Requires-Dist: simple-module-core==0.0.33
|
|
26
|
+
Requires-Dist: simple-module-db==0.0.33
|
|
27
27
|
Description-Content-Type: text/markdown
|
|
28
28
|
|
|
29
29
|
# simple_module_auth
|
|
@@ -10,6 +10,7 @@ from __future__ import annotations
|
|
|
10
10
|
|
|
11
11
|
import logging
|
|
12
12
|
|
|
13
|
+
from simple_module_core.redirect_safety import SESSION_NEXT_KEY, safe_next_or_none
|
|
13
14
|
from simple_module_db.listeners import current_user_id
|
|
14
15
|
from starlette.requests import Request
|
|
15
16
|
from starlette.responses import JSONResponse, RedirectResponse
|
|
@@ -26,7 +27,10 @@ _FRAMEWORK_PUBLIC_PREFIXES = (
|
|
|
26
27
|
"/i18n/",
|
|
27
28
|
)
|
|
28
29
|
_FRAMEWORK_PUBLIC_EXACT = ("/",)
|
|
29
|
-
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _query_suffix(request: Request) -> str:
|
|
33
|
+
return f"?{request.url.query}" if request.url.query else ""
|
|
30
34
|
|
|
31
35
|
|
|
32
36
|
class AuthMiddleware:
|
|
@@ -92,9 +96,19 @@ class AuthMiddleware:
|
|
|
92
96
|
if path.startswith("/api/") or provider.is_bearer_request(request):
|
|
93
97
|
response = JSONResponse({"detail": "Not authenticated"}, status_code=401)
|
|
94
98
|
else:
|
|
99
|
+
# Stash where they were heading so the login flow can return
|
|
100
|
+
# them there. Relative, not ``str(request.url)``: the value is
|
|
101
|
+
# replayed into a ``Location`` header, and an absolute URL is
|
|
102
|
+
# both needless and an open-redirect shape. ``next_url`` also
|
|
103
|
+
# goes to the provider — session-based providers ignore it,
|
|
104
|
+
# but a redirect-based one (OIDC) needs it in the auth URL.
|
|
95
105
|
session = scope.get("session", {})
|
|
96
|
-
|
|
97
|
-
|
|
106
|
+
next_url = safe_next_or_none(request.url.path + _query_suffix(request))
|
|
107
|
+
if next_url is not None:
|
|
108
|
+
session[SESSION_NEXT_KEY] = next_url
|
|
109
|
+
response = RedirectResponse(
|
|
110
|
+
provider.get_login_url(request, next_url), status_code=302
|
|
111
|
+
)
|
|
98
112
|
await response(scope, receive, send)
|
|
99
113
|
return
|
|
100
114
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "simple_module_auth"
|
|
3
|
-
version = "0.0.
|
|
3
|
+
version = "0.0.33"
|
|
4
4
|
description = "Session-cookie authentication primitives — middleware, login/logout, redirect helpers for simple_module"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = "MIT"
|
|
@@ -22,8 +22,8 @@ classifiers = [
|
|
|
22
22
|
]
|
|
23
23
|
dependencies = [
|
|
24
24
|
"itsdangerous>=2.2",
|
|
25
|
-
"simple_module_core==0.0.
|
|
26
|
-
"simple_module_db==0.0.
|
|
25
|
+
"simple_module_core==0.0.33",
|
|
26
|
+
"simple_module_db==0.0.33",
|
|
27
27
|
]
|
|
28
28
|
|
|
29
29
|
[project.entry-points.simple_module]
|
|
@@ -203,3 +203,73 @@ async def test_resolver_exception_is_logged_and_skipped():
|
|
|
203
203
|
) as c:
|
|
204
204
|
resp = await c.get("/protected/page")
|
|
205
205
|
assert resp.status_code == 302
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
class TestDeepLinkPreservation:
|
|
209
|
+
"""AuthMiddleware stashes where an anonymous visitor was heading.
|
|
210
|
+
|
|
211
|
+
The value is replayed into a ``Location`` header after login, so it is
|
|
212
|
+
stored relative and sanitised on the way in — see
|
|
213
|
+
``simple_module_core.redirect_safety``.
|
|
214
|
+
"""
|
|
215
|
+
|
|
216
|
+
async def _get(self, app, path: str):
|
|
217
|
+
transport = httpx.ASGITransport(app=app)
|
|
218
|
+
async with httpx.AsyncClient(
|
|
219
|
+
transport=transport, base_url="http://test", follow_redirects=False
|
|
220
|
+
) as c:
|
|
221
|
+
return await c.get(path)
|
|
222
|
+
|
|
223
|
+
async def test_target_is_offered_to_the_provider(self):
|
|
224
|
+
"""The provider is handed the target, not left to guess it."""
|
|
225
|
+
seen: list[str | None] = []
|
|
226
|
+
|
|
227
|
+
class _RecordingProvider(_StubProvider):
|
|
228
|
+
def get_login_url(self, request, next_url=None):
|
|
229
|
+
seen.append(next_url)
|
|
230
|
+
return "/stub/login"
|
|
231
|
+
|
|
232
|
+
app = _build_app(_RecordingProvider())
|
|
233
|
+
app.add_middleware(AuthMiddleware)
|
|
234
|
+
app.add_middleware(SessionMiddleware, secret_key=SECRET)
|
|
235
|
+
|
|
236
|
+
await self._get(app, "/protected/page?tab=2")
|
|
237
|
+
|
|
238
|
+
assert seen == ["/protected/page?tab=2"]
|
|
239
|
+
|
|
240
|
+
async def test_target_is_relative_not_absolute(self):
|
|
241
|
+
"""``str(request.url)`` would hand the provider an absolute URL —
|
|
242
|
+
needless, and the wrong shape for a redirect target."""
|
|
243
|
+
seen: list[str | None] = []
|
|
244
|
+
|
|
245
|
+
class _RecordingProvider(_StubProvider):
|
|
246
|
+
def get_login_url(self, request, next_url=None):
|
|
247
|
+
seen.append(next_url)
|
|
248
|
+
return "/stub/login"
|
|
249
|
+
|
|
250
|
+
app = _build_app(_RecordingProvider())
|
|
251
|
+
app.add_middleware(AuthMiddleware)
|
|
252
|
+
app.add_middleware(SessionMiddleware, secret_key=SECRET)
|
|
253
|
+
|
|
254
|
+
await self._get(app, "/protected/page")
|
|
255
|
+
|
|
256
|
+
assert seen == ["/protected/page"]
|
|
257
|
+
assert not seen[0].startswith("http")
|
|
258
|
+
|
|
259
|
+
async def test_authenticated_request_stashes_nothing(self):
|
|
260
|
+
"""Only the anonymous branch records a target."""
|
|
261
|
+
seen: list[str | None] = []
|
|
262
|
+
|
|
263
|
+
class _RecordingProvider(_StubProvider):
|
|
264
|
+
def get_login_url(self, request, next_url=None):
|
|
265
|
+
seen.append(next_url)
|
|
266
|
+
return "/stub/login"
|
|
267
|
+
|
|
268
|
+
app = _build_app(_RecordingProvider(user=_TEST_USER))
|
|
269
|
+
app.add_middleware(AuthMiddleware)
|
|
270
|
+
app.add_middleware(SessionMiddleware, secret_key=SECRET)
|
|
271
|
+
|
|
272
|
+
resp = await self._get(app, "/protected/page")
|
|
273
|
+
|
|
274
|
+
assert resp.status_code == 200
|
|
275
|
+
assert seen == []
|
|
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
|
{simple_module_auth-0.0.31 → simple_module_auth-0.0.33}/tests/test_auth_provider_protocol.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|