FastAPI-UI-Auth 0.2.3__py3-none-any.whl → 0.3.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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: FastAPI-UI-Auth
3
- Version: 0.2.3
3
+ Version: 0.3.1
4
4
  Summary: Python module to add username and password authentication to specific FastAPI routes
5
5
  Requires-Python: >=3.11
6
6
  Description-Content-Type: text/markdown
@@ -12,6 +12,10 @@ Provides-Extra: dev
12
12
  Requires-Dist: websockets==15.0.*; extra == "dev"
13
13
  Requires-Dist: pre-commit==4.2.*; extra == "dev"
14
14
  Requires-Dist: uvicorn==0.34.*; extra == "dev"
15
+ Provides-Extra: test
16
+ Requires-Dist: pytest; extra == "test"
17
+ Requires-Dist: pytest-cov; extra == "test"
18
+ Requires-Dist: httpx; extra == "test"
15
19
  Dynamic: license-file
16
20
 
17
21
  # FastAPIUIAuth
@@ -44,6 +48,7 @@ pip install FastAPI-UI-Auth
44
48
  import uiauth
45
49
 
46
50
  from fastapi import FastAPI
51
+ from fastapi.routing import APIRoute
47
52
 
48
53
  app = FastAPI()
49
54
 
@@ -56,9 +61,9 @@ async def private_route():
56
61
 
57
62
  uiauth.protect(
58
63
  app=app,
59
- params=uiauth.Parameters(
64
+ routes=APIRoute(
60
65
  path="/private",
61
- function=private_route
66
+ endpoint=private_route
62
67
  )
63
68
  )
64
69
  ```
@@ -0,0 +1,18 @@
1
+ fastapi_ui_auth-0.3.1.dist-info/licenses/LICENSE,sha256=_sOIKJWdD2o1WwwDIwYB2qTP2nlSWqT5Tyg9jr1Xa4w,1070
2
+ uiauth/__init__.py,sha256=hbHN-Vv4xTxDqpQW2lmgdl-OlEkAtL6JXAGL-nucaOU,211
3
+ uiauth/endpoints.py,sha256=CJteXsGQWfn--U6_VdVN8VhnPsf3HSefYX1NOzAhacM,2837
4
+ uiauth/enums.py,sha256=W_9U2luXbscyBEROXqEhKY5DHpJRV1J4VUOpkyUOOzU,334
5
+ uiauth/logger.py,sha256=z67PBMs4zWOfy-Gfm_41dj5Uulm-ChvZxB_jmYKKXeI,391
6
+ uiauth/models.py,sha256=56d8O9bExxwPZcOzMYL0IN9LOnVTJyfOSvD58kzTklc,3210
7
+ uiauth/secure.py,sha256=ZOH6kT4BD56VqwaKdKocX7eSE8tqZcu-tK0QOmjY58k,1089
8
+ uiauth/service.py,sha256=XeVFxWR5k7QIdgxjRBiUaE0oYpSlX3HE-RadJq-7HW4,7827
9
+ uiauth/utils.py,sha256=Ga8RivN3PJX8zg2uu3RfEtJLGKaT1_iwphqvhh2XrPY,7007
10
+ uiauth/version.py,sha256=sEAhGxRzEBE5t0VjAcJ-336II62pGIQ0eLrs42I-sGU,18
11
+ uiauth/templates/index.html,sha256=n8tOiKXEUI4zBh1YOQNlH5MKNMRTQ2adH0QIuvrEcv4,9071
12
+ uiauth/templates/logout.html,sha256=JrWBJCbK1E4NfrNipMsLzfJ_-Fs2C6D4S0B6O7JNoek,3504
13
+ uiauth/templates/session.html,sha256=EL4gajOED3IcOnrALMiJ2SzJl2at8GFfruTuExhgOVI,3040
14
+ uiauth/templates/unauthorized.html,sha256=ahv78zLM04_Lu83LdX0Ua_toKeP5JZkYsTCWCrfCvHA,3002
15
+ fastapi_ui_auth-0.3.1.dist-info/METADATA,sha256=UbiHi5QjWiG7VqUYCbRQGpVj6_TEW3ozAYBQi4cNOpU,3662
16
+ fastapi_ui_auth-0.3.1.dist-info/WHEEL,sha256=YCfwYGOYMi5Jhw2fU4yNgwErybb2IX5PEwBKV4ZbdBo,91
17
+ fastapi_ui_auth-0.3.1.dist-info/top_level.txt,sha256=ra3nGTbDTgQ7eChlkngJ7xGXhSCeFTWMvb_b6q8uPVA,7
18
+ fastapi_ui_auth-0.3.1.dist-info/RECORD,,
uiauth/__init__.py CHANGED
@@ -1,6 +1,5 @@
1
- from uiauth.enums import APIEndpoints, APIMethods # noqa: F401,E402
2
- from uiauth.models import Parameters # noqa: F401,E402
3
- from uiauth.service import FastAPIUIAuth # noqa: F401,E402
1
+ from uiauth.enums import APIEndpoints # noqa: F401,E402
2
+ from uiauth.service import FastAPIUIAuth as _authProduct # noqa: F401,E402
4
3
  from uiauth.version import version # noqa: F401,E402
5
4
 
6
- protect = FastAPIUIAuth
5
+ protect = _authProduct
uiauth/enums.py CHANGED
@@ -1,21 +1,6 @@
1
1
  from enum import StrEnum
2
2
 
3
3
 
4
- class APIMethods(StrEnum):
5
- """HTTP methods for API requests.
6
-
7
- >>> APIMethods
8
-
9
- """
10
-
11
- GET = "GET"
12
- POST = "POST"
13
- PUT = "PUT"
14
- DELETE = "DELETE"
15
- PATCH = "PATCH"
16
- OPTIONS = "OPTIONS"
17
-
18
-
19
4
  class APIEndpoints(StrEnum):
20
5
  """API endpoints for all the routes.
21
6
 
uiauth/models.py CHANGED
@@ -1,13 +1,10 @@
1
1
  import os
2
2
  import pathlib
3
- from typing import Callable, Dict, Iterable, List, Optional, Type
3
+ from typing import Dict, Iterable, Optional
4
4
 
5
- from fastapi.routing import APIRoute, APIWebSocketRoute
6
5
  from fastapi.templating import Jinja2Templates
7
6
  from pydantic import BaseModel, Field
8
7
 
9
- from uiauth.enums import APIMethods
10
-
11
8
  templates = Jinja2Templates(directory=pathlib.Path(__file__).parent / "templates")
12
9
 
13
10
 
@@ -58,26 +55,6 @@ def env_loader(**kwargs) -> EnvConfig:
58
55
  env = EnvConfig
59
56
 
60
57
 
61
- class Parameters(BaseModel):
62
- """Parameters for the Authenticator class.
63
-
64
- >>> Parameters
65
-
66
- Attributes:
67
- path: Path for the secure route, must start with '/'.
68
- function: Function to be called for secure routes after authentication.
69
- methods: List of HTTP methods that the secure function will handle.
70
- route: Type of route to be used for secure routes, either APIWebSocketRoute or APIRoute.
71
- """
72
-
73
- path: str = Field(
74
- pattern="^/.*$", description="Path for the secure route, must start with '/'"
75
- )
76
- function: Callable
77
- methods: List[APIMethods] = [APIMethods.GET]
78
- route: Type[APIWebSocketRoute] | Type[APIRoute] = APIRoute
79
-
80
-
81
58
  class WSSession(BaseModel):
82
59
  """Object to store websocket session information.
83
60
 
uiauth/service.py CHANGED
@@ -1,13 +1,9 @@
1
+ import inspect
1
2
  import logging
2
- from threading import Timer
3
+ import time
3
4
  from typing import Dict, List
4
5
 
5
- from fastapi import status
6
- from fastapi.applications import FastAPI
7
- from fastapi.exceptions import HTTPException
8
- from fastapi.params import Depends
9
- from fastapi.requests import Request
10
- from fastapi.responses import Response
6
+ from fastapi import Depends, FastAPI, HTTPException, Request, Response, status
11
7
  from fastapi.routing import APIRoute, APIWebSocketRoute
12
8
  from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
13
9
 
@@ -24,13 +20,10 @@ class FastAPIUIAuth:
24
20
 
25
21
  """
26
22
 
27
- # TODO: Consume APIRoute or APIWebSocketRoute directly in params instead of creating them in the _secure method
28
- # Include stricter type validation (if that works)
29
- # Update samples and readme - major version change
30
23
  def __init__(
31
24
  self,
32
25
  app: FastAPI,
33
- params: models.Parameters | List[models.Parameters],
26
+ routes: APIRoute | APIWebSocketRoute | List[APIRoute] | List[APIWebSocketRoute],
34
27
  timeout: int = 300,
35
28
  username: str = None,
36
29
  password: str = None,
@@ -42,7 +35,7 @@ class FastAPIUIAuth:
42
35
 
43
36
  Args:
44
37
  app: FastAPI application instance to which the authenticator will be added.
45
- params: Parameters for the secure routes can be a single `Parameters` object or a list of `Parameters`.
38
+ routes: APIRoute or APIWebSocketRoute instance(s) representing the routes to be protected by authentication.
46
39
  timeout: Session timeout in seconds, default is 300 seconds (5 minutes).
47
40
  username: Username for authentication, can be set via environment variable 'USERNAME'.
48
41
  password: Password for authentication, can be set via environment variable 'PASSWORD'.
@@ -50,7 +43,10 @@ class FastAPIUIAuth:
50
43
  fallback_path: Fallback path to redirect to in case of session timeout or invalid session.
51
44
  custom_logger: Custom logger instance, defaults to the custom logger.
52
45
  """
53
- models.env = models.EnvConfig(username=username, password=password)
46
+ assert (
47
+ isinstance(timeout, int) and timeout > 29
48
+ ), "Timeout must be an integer at least 30 seconds"
49
+ models.env = models.env_loader(username=username, password=password)
54
50
  assert (
55
51
  models.env.username and models.env.password
56
52
  ), "Username and password must be provided either as arguments or environment variables"
@@ -58,18 +54,18 @@ class FastAPIUIAuth:
58
54
 
59
55
  self.app = app
60
56
 
61
- if isinstance(params, list):
62
- assert len(params) > 0, "No endpoints to register"
63
- for param in params:
64
- assert isinstance(
65
- param, models.Parameters
66
- ), f"{param} must be an instance of models.Parameters"
67
- self.params = params
68
- elif isinstance(params, models.Parameters):
69
- self.params = [params]
57
+ if isinstance(routes, list):
58
+ assert len(routes) > 0, "No endpoints to register"
59
+ for route in routes:
60
+ assert isinstance(route, APIRoute) or isinstance(
61
+ route, APIWebSocketRoute
62
+ ), f"{route} must be an instance of APIRoute or APIWebSocketRoute"
63
+ self.routes = routes
64
+ elif isinstance(routes, APIRoute) or isinstance(routes, APIWebSocketRoute):
65
+ self.routes = [routes]
70
66
  else:
71
67
  raise ValueError(
72
- "Params must be an instance of Parameters or a list of Parameters"
68
+ "Routes must be an instance of APIRoute or APIWebSocketRoute or a list of them"
73
69
  )
74
70
 
75
71
  assert fallback_path.startswith("/"), "Fallback path must start with '/'"
@@ -90,7 +86,7 @@ class FastAPIUIAuth:
90
86
  self.timeout = timeout
91
87
 
92
88
  self._secure()
93
- logger.CUSTOM_LOGGER.debug("Endpoints registered: %s", len(self.params))
89
+ logger.CUSTOM_LOGGER.debug("Endpoints registered: %s", len(self.routes))
94
90
 
95
91
  def _verify_auth(
96
92
  self,
@@ -125,12 +121,11 @@ class FastAPIUIAuth:
125
121
  samesite="strict",
126
122
  max_age=self.timeout,
127
123
  )
124
+ models.ws_session.client_auth[request.client.host] = {
125
+ "token": session_token,
126
+ "expires_at": time.time() + self.timeout,
127
+ }
128
128
  response.delete_cookie(key="X-Requested-By")
129
- Timer(
130
- function=utils.clear_session,
131
- args=(request.client.host,),
132
- interval=self.timeout,
133
- ).start()
134
129
  return {"redirect_url": destination}
135
130
  raise HTTPException(
136
131
  status_code=status.HTTP_417_EXPECTATION_FAILED,
@@ -145,42 +140,55 @@ class FastAPIUIAuth:
145
140
  path=enums.APIEndpoints.fastapi_login,
146
141
  endpoint=endpoints.login,
147
142
  methods=["GET"],
143
+ include_in_schema=False,
148
144
  )
149
145
  logout_route = APIRoute(
150
146
  path=enums.APIEndpoints.fastapi_logout,
151
147
  endpoint=endpoints.logout,
152
148
  methods=["GET"],
149
+ include_in_schema=False,
153
150
  )
154
151
  error_route = APIRoute(
155
152
  path=enums.APIEndpoints.fastapi_error,
156
153
  endpoint=endpoints.error,
157
154
  methods=["GET"],
155
+ include_in_schema=False,
158
156
  )
159
157
  session_route = APIRoute(
160
158
  path=enums.APIEndpoints.fastapi_session,
161
159
  endpoint=endpoints.session,
162
160
  methods=["GET"],
161
+ include_in_schema=False,
163
162
  )
164
163
  verify_route = APIRoute(
165
164
  path=enums.APIEndpoints.fastapi_verify_login,
166
165
  endpoint=self._verify_auth,
167
166
  methods=["POST"],
167
+ include_in_schema=False,
168
168
  )
169
- for param in self.params:
170
- if param.route is APIWebSocketRoute:
171
- # WebSocket routes will not have a login path, they will be protected by session check
172
- secure_route = APIWebSocketRoute(
173
- path=param.path,
174
- endpoint=param.function,
175
- dependencies=[Depends(utils.verify_session)],
176
- )
177
- else:
178
- secure_route = APIRoute(
179
- path=param.path,
180
- endpoint=param.function,
181
- methods=["GET"],
182
- dependencies=[Depends(utils.verify_session)],
183
- )
169
+ protected_paths = {route.path for route in self.routes}
170
+ conflicting = [
171
+ route
172
+ for route in self.app.routes
173
+ if isinstance(route, (APIRoute, APIWebSocketRoute))
174
+ and route.path in protected_paths
175
+ ]
176
+ for existing in conflicting:
177
+ logger.CUSTOM_LOGGER.warning(
178
+ "Route %s already registered in the app, removing and re-registering with authentication",
179
+ existing.path,
180
+ )
181
+ self.app.routes.remove(existing)
182
+ for route in self.routes:
183
+ kwargs = {
184
+ name: getattr(route, name)
185
+ for name in inspect.signature(route.__class__.__init__).parameters
186
+ if name != "self" and hasattr(route, name)
187
+ }
188
+ kwargs["dependencies"] = list(route.dependencies) + [
189
+ Depends(utils.verify_session)
190
+ ]
191
+ secure_route = route.__class__(**kwargs)
184
192
  self.app.routes.append(secure_route)
185
193
  self.app.routes.extend(
186
194
  [login_route, logout_route, session_route, verify_route, error_route]
uiauth/utils.py CHANGED
@@ -1,4 +1,5 @@
1
1
  import secrets
2
+ import time
2
3
  from typing import List, NoReturn
3
4
 
4
5
  from fastapi import status
@@ -111,7 +112,6 @@ def verify_login(
111
112
  if secrets.compare_digest(signature, expected_signature):
112
113
  models.ws_session.invalid[request.client.host] = 0
113
114
  key = secrets.token_urlsafe(64)
114
- models.ws_session.client_auth[request.client.host] = key
115
115
  return key
116
116
  raise_error(request)
117
117
 
@@ -138,14 +138,25 @@ def verify_session(
138
138
  detail="Request or WebSocket connection is required for session check.",
139
139
  )
140
140
  session_token = request.cookies.get("session_token")
141
- stored_token = models.ws_session.client_auth.get(request.client.host)
141
+ stored = models.ws_session.client_auth.get(request.client.host, {})
142
142
  if (
143
- stored_token
143
+ stored.get("token")
144
144
  and session_token
145
- and secrets.compare_digest(session_token, stored_token)
145
+ and secrets.compare_digest(session_token, stored["token"])
146
146
  ):
147
- logger.CUSTOM_LOGGER.info("Session is valid for host: %s", request.client.host)
148
- return
147
+ if time.time() < stored["expires_at"]:
148
+ logger.CUSTOM_LOGGER.debug(
149
+ "Session is valid for host: %s", request.client.host
150
+ )
151
+ return
152
+ models.ws_session.client_auth.pop(request.client.host, None)
153
+ logger.CUSTOM_LOGGER.warning(
154
+ "Session expired for host: %s", request.client.host
155
+ )
156
+ raise models.RedirectException(
157
+ source=request.url.path,
158
+ destination=enums.APIEndpoints.fastapi_login,
159
+ )
149
160
  elif not session_token:
150
161
  logger.CUSTOM_LOGGER.warning(
151
162
  "Session is invalid or expired for host: %s", request.client.host
@@ -158,7 +169,7 @@ def verify_session(
158
169
  logger.CUSTOM_LOGGER.warning(
159
170
  "Session token mismatch for host: %s. Expected: %s, Received: %s",
160
171
  request.client.host,
161
- stored_token,
172
+ stored.get("token", "None"),
162
173
  session_token,
163
174
  )
164
175
  raise models.RedirectException(
uiauth/version.py CHANGED
@@ -1 +1 @@
1
- version = "0.2.3"
1
+ version = "0.3.1"
@@ -1,18 +0,0 @@
1
- fastapi_ui_auth-0.2.3.dist-info/licenses/LICENSE,sha256=_sOIKJWdD2o1WwwDIwYB2qTP2nlSWqT5Tyg9jr1Xa4w,1070
2
- uiauth/__init__.py,sha256=s8r2Z0O9w3cuw7GcmRTOWY0NZC0KJXzBS9QsG9wUWsk,264
3
- uiauth/endpoints.py,sha256=CJteXsGQWfn--U6_VdVN8VhnPsf3HSefYX1NOzAhacM,2837
4
- uiauth/enums.py,sha256=WO0eBv3l9HHr1I_ZXtAifCgdL-db_tZj9ka7jnjiS5k,547
5
- uiauth/logger.py,sha256=z67PBMs4zWOfy-Gfm_41dj5Uulm-ChvZxB_jmYKKXeI,391
6
- uiauth/models.py,sha256=cU1VoPtHsB0A8DIvXSDpAZz2KtbOtQPB507cQ4MGeOw,4014
7
- uiauth/secure.py,sha256=ZOH6kT4BD56VqwaKdKocX7eSE8tqZcu-tK0QOmjY58k,1089
8
- uiauth/service.py,sha256=sJG-RbiiO-qKtvaGrSsu_UfTndRPkdRn0AXWhGnjJmQ,7443
9
- uiauth/utils.py,sha256=DzXqxLpKHUDy1bxffg1cw0izqxcgmnCybSytywiPgbQ,6625
10
- uiauth/version.py,sha256=7YVXTLSKw_SIjam_Lv65ld1ty1jiyVmclya8_CjMMqY,18
11
- uiauth/templates/index.html,sha256=n8tOiKXEUI4zBh1YOQNlH5MKNMRTQ2adH0QIuvrEcv4,9071
12
- uiauth/templates/logout.html,sha256=JrWBJCbK1E4NfrNipMsLzfJ_-Fs2C6D4S0B6O7JNoek,3504
13
- uiauth/templates/session.html,sha256=EL4gajOED3IcOnrALMiJ2SzJl2at8GFfruTuExhgOVI,3040
14
- uiauth/templates/unauthorized.html,sha256=ahv78zLM04_Lu83LdX0Ua_toKeP5JZkYsTCWCrfCvHA,3002
15
- fastapi_ui_auth-0.2.3.dist-info/METADATA,sha256=QeOE9sG3SJE5Tzqm7AoOe65z8QjgqcS_t_mSmFJHYBU,3493
16
- fastapi_ui_auth-0.2.3.dist-info/WHEEL,sha256=YCfwYGOYMi5Jhw2fU4yNgwErybb2IX5PEwBKV4ZbdBo,91
17
- fastapi_ui_auth-0.2.3.dist-info/top_level.txt,sha256=ra3nGTbDTgQ7eChlkngJ7xGXhSCeFTWMvb_b6q8uPVA,7
18
- fastapi_ui_auth-0.2.3.dist-info/RECORD,,