FastAPI-UI-Auth 0.0.1a0__py3-none-any.whl → 0.1.0__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.0.1a0
3
+ Version: 0.1.0
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
@@ -10,7 +10,6 @@ Requires-Dist: jinja2==3.1.*
10
10
  Requires-Dist: pydantic==2.11.*
11
11
  Requires-Dist: python-dotenv==1.1.*
12
12
  Provides-Extra: dev
13
- Requires-Dist: websockets==15.0.*; extra == "dev"
14
13
  Requires-Dist: pre-commit==4.2.*; extra == "dev"
15
14
  Requires-Dist: uvicorn==0.34.*; extra == "dev"
16
15
  Dynamic: license-file
@@ -38,30 +37,24 @@ pip install "git+https://github.com/${repo}.git@${latest}"
38
37
  ## Usage
39
38
 
40
39
  ```python
41
- import fastapiauthenticator as api
40
+ import fastapiauthenticator
42
41
 
43
42
  from fastapi import FastAPI
44
43
 
45
44
  app = FastAPI()
46
45
 
46
+
47
47
  @app.get("/public")
48
- async def public_route():
48
+ def public_route():
49
49
  return {"message": "This is a public route"}
50
50
 
51
- async def private_route():
51
+
52
+ def private_route():
52
53
  return {"message": "This is a private route"}
53
54
 
54
- api.protect(
55
- app=app,
56
- params=api.Parameters(
57
- path="/private",
58
- function=private_route
59
- )
60
- )
61
- ```
62
55
 
63
- > `FastAPIAuthenticator` supports both `APIRoute` and `APIWebSocketRoute` routes.<br>
64
- > Refer [samples] directory for different use-cases.
56
+ fastapiauthenticator.Authenticator(app=app, secure_function=private_route)
57
+ ```
65
58
 
66
59
  ## Coding Standards
67
60
  Docstring format: [`Google`][google-docs] <br>
@@ -103,7 +96,6 @@ Licensed under the [MIT License][license]
103
96
  [google-docs]: https://google.github.io/styleguide/pyguide.html#38-comments-and-docstrings
104
97
  [pep8]: https://www.python.org/dev/peps/pep-0008/
105
98
  [isort]: https://pycqa.github.io/isort/
106
- [samples]: https://github.com/thevickypedia/FastAPIAuthenticator/tree/main/samples
107
99
 
108
100
  [label-pyversion]: https://img.shields.io/badge/python-3.11%20%7C%203.12-blue
109
101
  [label-platform]: https://img.shields.io/badge/Platform-Linux|macOS|Windows-1f425f.svg
@@ -1,16 +1,16 @@
1
- fastapi_ui_auth-0.0.1a0.dist-info/licenses/LICENSE,sha256=_sOIKJWdD2o1WwwDIwYB2qTP2nlSWqT5Tyg9jr1Xa4w,1070
1
+ fastapi_ui_auth-0.1.0.dist-info/licenses/LICENSE,sha256=_sOIKJWdD2o1WwwDIwYB2qTP2nlSWqT5Tyg9jr1Xa4w,1070
2
2
  fastapiauthenticator/__init__.py,sha256=H1tJUJp4FWweEu2JeMw5vmqjtMhMwR4kUd11ek8UmwQ,320
3
3
  fastapiauthenticator/endpoints.py,sha256=PF2qu6XQ3MQStFEprRYYxiS6Dl6ukYaMqEkSlO-F3Ls,2104
4
4
  fastapiauthenticator/enums.py,sha256=WO0eBv3l9HHr1I_ZXtAifCgdL-db_tZj9ka7jnjiS5k,547
5
5
  fastapiauthenticator/models.py,sha256=GxmQfSvg70OTsvswJ3QFq_lxq-Yz1fIfzW6x8d4Sj40,2726
6
6
  fastapiauthenticator/secure.py,sha256=ZOH6kT4BD56VqwaKdKocX7eSE8tqZcu-tK0QOmjY58k,1089
7
- fastapiauthenticator/service.py,sha256=7E474kwPIDsbtZPMGJTSo0thKc2FP2n-MUogsH0ZrZI,6247
8
- fastapiauthenticator/utils.py,sha256=Is7oaaHYsZ97aPmDWTcIqStW693HRIDUKtkGBdfyMVg,6731
9
- fastapiauthenticator/version.py,sha256=46ZIiyGr-IoNupXhFr7nG1zjiaiYIVZlcFZ7vFnP-F4,20
7
+ fastapiauthenticator/service.py,sha256=1UCCUf7yaH11BHB2vlaNYehVn5YyWbGK6bnrN7wJlDM,6485
8
+ fastapiauthenticator/utils.py,sha256=geO78AL-nqv4EANQzQaWI2mGkkZiLZY8wm2LH_EDye0,7145
9
+ fastapiauthenticator/version.py,sha256=aOHawL1zuHMfBWKXqwUkXcW96oXLNCY-CXdHDqkz4g4,18
10
10
  fastapiauthenticator/templates/index.html,sha256=mA2R6gk6lvibq_AmPgGHBFQijYtNUD7IIfeBSJWQrM4,9078
11
11
  fastapiauthenticator/templates/session.html,sha256=LUCvcEdQOjfIXjRZ2gPx2s5wyzNuCve4OMge0hXaBLM,3053
12
12
  fastapiauthenticator/templates/unauthorized.html,sha256=UZo1Jt64-CFfjwWTGicUMdHVWkYkXCJBRxvit4QTiQM,3015
13
- fastapi_ui_auth-0.0.1a0.dist-info/METADATA,sha256=o-8MPbPQOrEFLoxC7cKtK4XJaYw4epzZNsXZ8JjBllQ,2735
14
- fastapi_ui_auth-0.0.1a0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
15
- fastapi_ui_auth-0.0.1a0.dist-info/top_level.txt,sha256=EpDRP7uLM0f-Vd5rUtLBh4MTMAnpXzw1pr0DSknW_Ds,21
16
- fastapi_ui_auth-0.0.1a0.dist-info/RECORD,,
13
+ fastapi_ui_auth-0.1.0.dist-info/METADATA,sha256=taS1d_w3xCMVJB5ymIbhjlM7GpWIfCPEf8PyR9u4pRQ,2402
14
+ fastapi_ui_auth-0.1.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
15
+ fastapi_ui_auth-0.1.0.dist-info/top_level.txt,sha256=EpDRP7uLM0f-Vd5rUtLBh4MTMAnpXzw1pr0DSknW_Ds,21
16
+ fastapi_ui_auth-0.1.0.dist-info/RECORD,,
@@ -42,7 +42,7 @@ class Authenticator:
42
42
 
43
43
  Args:
44
44
  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`.
45
+ params: Parameters for the secure routes, can be a single `Parameters` object or a list of `Parameters`.
46
46
  timeout: Session timeout in seconds, default is 300 seconds (5 minutes).
47
47
  username: Username for authentication, can be set via environment variable 'USERNAME'.
48
48
  password: Password for authentication, can be set via environment variable 'PASSWORD'.
@@ -59,6 +59,10 @@ class Authenticator:
59
59
  elif isinstance(params, models.Parameters):
60
60
  self.params = [params]
61
61
 
62
+ self.route_map: Dict[str, models.Parameters] = {
63
+ param.path: param for param in self.params if param.route is APIRoute
64
+ }
65
+
62
66
  models.fallback.path = fallback_path
63
67
  models.fallback.button = fallback_button
64
68
 
@@ -91,18 +95,19 @@ class Authenticator:
91
95
  Dict[str, str]:
92
96
  A dictionary containing the redirect URL to the secure path.
93
97
  """
94
- session_token = utils.verify_login(
98
+ utils.verify_login(
95
99
  authorization=authorization,
96
100
  request=request,
97
101
  env_username=self.username,
98
102
  env_password=self.password,
99
103
  )
100
- if destination := request.cookies.get("X-Requested-By"):
104
+ destination = request.cookies.get("X-Requested-By")
105
+ if parameter := self.route_map.get(destination):
101
106
  LOGGER.info("Setting session timeout for %s seconds", self.timeout)
102
107
  # Set session_token cookie with a timeout, to be used for session validation when redirected
103
108
  response.set_cookie(
104
109
  key="session_token",
105
- value=session_token,
110
+ value=models.ws_session.client_auth[request.client.host].get("token"),
106
111
  httponly=True,
107
112
  samesite="strict",
108
113
  max_age=self.timeout,
@@ -113,7 +118,7 @@ class Authenticator:
113
118
  args=(request.client.host,),
114
119
  interval=self.timeout,
115
120
  ).start()
116
- return {"redirect_url": destination}
121
+ return {"redirect_url": parameter.path}
117
122
  raise HTTPException(
118
123
  status_code=status.HTTP_417_EXPECTATION_FAILED,
119
124
  detail="Unable to find secure route for the requested path.\n"
@@ -149,14 +154,14 @@ class Authenticator:
149
154
  secure_route = APIWebSocketRoute(
150
155
  path=param.path,
151
156
  endpoint=param.function,
152
- dependencies=[Depends(utils.verify_session)],
157
+ dependencies=[Depends(utils.session_check)],
153
158
  )
154
159
  else:
155
160
  secure_route = APIRoute(
156
161
  path=param.path,
157
162
  endpoint=param.function,
158
163
  methods=["GET"],
159
- dependencies=[Depends(utils.verify_session)],
164
+ dependencies=[Depends(utils.session_check)],
160
165
  )
161
166
  self.app.routes.append(secure_route)
162
167
  self.app.routes.extend([login_route, session_route, verify_route, error_route])
@@ -1,6 +1,6 @@
1
1
  import logging
2
2
  import secrets
3
- from typing import List, NoReturn
3
+ from typing import Dict, List, NoReturn, Union
4
4
 
5
5
  from fastapi import status
6
6
  from fastapi.exceptions import HTTPException
@@ -73,12 +73,17 @@ def raise_error(request: Request) -> NoReturn:
73
73
  )
74
74
 
75
75
 
76
- def extract_credentials(authorization: HTTPAuthorizationCredentials) -> List[str]:
76
+ def extract_credentials(
77
+ authorization: HTTPAuthorizationCredentials, host: str
78
+ ) -> List[str]:
77
79
  """Extract the credentials from ``Authorization`` headers and decode it before returning as a list of strings.
78
80
 
79
81
  Args:
80
82
  authorization: Authorization header from the request.
83
+ host: Host header from the request.
81
84
  """
85
+ if not authorization:
86
+ raise_error(host)
82
87
  decoded_auth = secure.base64_decode(authorization.credentials)
83
88
  # convert hex to a string
84
89
  auth = secure.hex_decode(decoded_auth)
@@ -90,7 +95,7 @@ def verify_login(
90
95
  request: Request,
91
96
  env_username: str,
92
97
  env_password: str,
93
- ) -> str | NoReturn:
98
+ ) -> Dict[str, Union[str, int]]:
94
99
  """Verifies authentication and generates session token for each user.
95
100
 
96
101
  Args:
@@ -100,13 +105,12 @@ def verify_login(
100
105
  env_password: Environment variable for the password.
101
106
 
102
107
  Returns:
103
- str:
104
- Returns the session token.
108
+ Dict[str, str]:
109
+ Returns a dictionary with the payload required to create the session token.
105
110
  """
106
- if authorization:
107
- username, signature, timestamp = extract_credentials(authorization)
108
- else:
109
- raise_error(request)
111
+ username, signature, timestamp = extract_credentials(
112
+ authorization, request.client.host
113
+ )
110
114
  if secrets.compare_digest(username, env_username):
111
115
  hex_user = secure.hex_encode(env_username)
112
116
  hex_pass = secure.hex_encode(env_password)
@@ -118,14 +122,15 @@ def verify_login(
118
122
  if secrets.compare_digest(signature, expected_signature):
119
123
  models.ws_session.invalid[request.client.host] = 0
120
124
  key = secrets.token_urlsafe(64)
121
- models.ws_session.client_auth[request.client.host] = key
122
- return key
125
+ # fixme: By setting a path instead of timestamp, this can handle path specific sessions
126
+ models.ws_session.client_auth[request.client.host] = dict(
127
+ username=username, token=key, timestamp=int(timestamp)
128
+ )
129
+ return models.ws_session.client_auth[request.client.host]
123
130
  raise_error(request)
124
131
 
125
132
 
126
- def verify_session(
127
- api_request: Request = None, api_websocket: WebSocket = None
128
- ) -> None:
133
+ def session_check(api_request: Request = None, api_websocket: WebSocket = None) -> None:
129
134
  """Check if the session is still valid.
130
135
 
131
136
  Args:
@@ -145,7 +150,9 @@ def verify_session(
145
150
  detail="Request or WebSocket connection is required for session check.",
146
151
  )
147
152
  session_token = request.cookies.get("session_token")
148
- stored_token = models.ws_session.client_auth.get(request.client.host)
153
+ stored_token = models.ws_session.client_auth.get(request.client.host, {}).get(
154
+ "token"
155
+ )
149
156
  if (
150
157
  stored_token
151
158
  and session_token
@@ -1 +1 @@
1
- version = "0.0.1-a"
1
+ version = "0.1.0"