simple-module-auth 0.0.32__tar.gz → 0.0.34__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.
Files changed (24) hide show
  1. {simple_module_auth-0.0.32 → simple_module_auth-0.0.34}/PKG-INFO +3 -3
  2. {simple_module_auth-0.0.32 → simple_module_auth-0.0.34}/auth/middleware.py +17 -3
  3. {simple_module_auth-0.0.32 → simple_module_auth-0.0.34}/pyproject.toml +3 -3
  4. {simple_module_auth-0.0.32 → simple_module_auth-0.0.34}/tests/test_auth_middleware.py +70 -0
  5. {simple_module_auth-0.0.32 → simple_module_auth-0.0.34}/.gitignore +0 -0
  6. {simple_module_auth-0.0.32 → simple_module_auth-0.0.34}/LICENSE +0 -0
  7. {simple_module_auth-0.0.32 → simple_module_auth-0.0.34}/README.md +0 -0
  8. {simple_module_auth-0.0.32 → simple_module_auth-0.0.34}/auth/__init__.py +0 -0
  9. {simple_module_auth-0.0.32 → simple_module_auth-0.0.34}/auth/contracts/__init__.py +0 -0
  10. {simple_module_auth-0.0.32 → simple_module_auth-0.0.34}/auth/contracts/provider.py +0 -0
  11. {simple_module_auth-0.0.32 → simple_module_auth-0.0.34}/auth/contracts/resolver.py +0 -0
  12. {simple_module_auth-0.0.32 → simple_module_auth-0.0.34}/auth/contracts/schemas.py +0 -0
  13. {simple_module_auth-0.0.32 → simple_module_auth-0.0.34}/auth/deps.py +0 -0
  14. {simple_module_auth-0.0.32 → simple_module_auth-0.0.34}/auth/locales/en.json +0 -0
  15. {simple_module_auth-0.0.32 → simple_module_auth-0.0.34}/auth/locales/es.json +0 -0
  16. {simple_module_auth-0.0.32 → simple_module_auth-0.0.34}/auth/module.py +0 -0
  17. {simple_module_auth-0.0.32 → simple_module_auth-0.0.34}/auth/py.typed +0 -0
  18. {simple_module_auth-0.0.32 → simple_module_auth-0.0.34}/auth/state.py +0 -0
  19. {simple_module_auth-0.0.32 → simple_module_auth-0.0.34}/package.json +0 -0
  20. {simple_module_auth-0.0.32 → simple_module_auth-0.0.34}/tests/test_auth_provider_protocol.py +0 -0
  21. {simple_module_auth-0.0.32 → simple_module_auth-0.0.34}/tests/test_deps.py +0 -0
  22. {simple_module_auth-0.0.32 → simple_module_auth-0.0.34}/tests/test_module.py +0 -0
  23. {simple_module_auth-0.0.32 → simple_module_auth-0.0.34}/tests/test_resolver_registry.py +0 -0
  24. {simple_module_auth-0.0.32 → simple_module_auth-0.0.34}/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.32
3
+ Version: 0.0.34
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.32
26
- Requires-Dist: simple-module-db==0.0.32
25
+ Requires-Dist: simple-module-core==0.0.34
26
+ Requires-Dist: simple-module-db==0.0.34
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
- _SESSION_NEXT_KEY = "next"
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
- session[_SESSION_NEXT_KEY] = str(request.url)
97
- response = RedirectResponse(provider.get_login_url(request), status_code=302)
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.32"
3
+ version = "0.0.34"
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.32",
26
- "simple_module_db==0.0.32",
25
+ "simple_module_core==0.0.34",
26
+ "simple_module_db==0.0.34",
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 == []