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.
- {fastapi_ui_auth-0.0.1a0.dist-info → fastapi_ui_auth-0.1.0.dist-info}/METADATA +8 -16
- {fastapi_ui_auth-0.0.1a0.dist-info → fastapi_ui_auth-0.1.0.dist-info}/RECORD +8 -8
- fastapiauthenticator/service.py +12 -7
- fastapiauthenticator/utils.py +22 -15
- fastapiauthenticator/version.py +1 -1
- {fastapi_ui_auth-0.0.1a0.dist-info → fastapi_ui_auth-0.1.0.dist-info}/WHEEL +0 -0
- {fastapi_ui_auth-0.0.1a0.dist-info → fastapi_ui_auth-0.1.0.dist-info}/licenses/LICENSE +0 -0
- {fastapi_ui_auth-0.0.1a0.dist-info → fastapi_ui_auth-0.1.0.dist-info}/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: FastAPI-UI-Auth
|
|
3
|
-
Version: 0.0
|
|
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
|
|
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
|
-
|
|
48
|
+
def public_route():
|
|
49
49
|
return {"message": "This is a public route"}
|
|
50
50
|
|
|
51
|
-
|
|
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
|
-
|
|
64
|
-
|
|
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.
|
|
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=
|
|
8
|
-
fastapiauthenticator/utils.py,sha256=
|
|
9
|
-
fastapiauthenticator/version.py,sha256=
|
|
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.
|
|
14
|
-
fastapi_ui_auth-0.0.
|
|
15
|
-
fastapi_ui_auth-0.0.
|
|
16
|
-
fastapi_ui_auth-0.0.
|
|
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,,
|
fastapiauthenticator/service.py
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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=
|
|
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":
|
|
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.
|
|
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.
|
|
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])
|
fastapiauthenticator/utils.py
CHANGED
|
@@ -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(
|
|
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
|
|
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
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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
|
-
|
|
122
|
-
|
|
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
|
|
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
|
fastapiauthenticator/version.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
version = "0.0
|
|
1
|
+
version = "0.1.0"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|