FastAPI-UI-Auth 0.0.1__py3-none-any.whl → 0.0.1b0__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.
@@ -0,0 +1,18 @@
1
+ Metadata-Version: 2.4
2
+ Name: FastAPI-UI-Auth
3
+ Version: 0.0.1b0
4
+ Summary: Python module to add username and password authentication to specific FastAPI routes
5
+ Requires-Python: >=3.11
6
+ Description-Content-Type: text/markdown
7
+ License-File: LICENSE
8
+ Requires-Dist: fastapi==0.115.*
9
+ Requires-Dist: jinja2==3.1.*
10
+ Requires-Dist: pydantic==2.11.*
11
+ Requires-Dist: python-dotenv==1.1.*
12
+ Provides-Extra: dev
13
+ Requires-Dist: websockets==15.0.*; extra == "dev"
14
+ Requires-Dist: pre-commit==4.2.*; extra == "dev"
15
+ Requires-Dist: uvicorn==0.34.*; extra == "dev"
16
+ Dynamic: license-file
17
+
18
+ # FastAPIAuthenticator
@@ -0,0 +1,6 @@
1
+ fastapi_ui_auth-0.0.1b0.dist-info/licenses/LICENSE,sha256=_sOIKJWdD2o1WwwDIwYB2qTP2nlSWqT5Tyg9jr1Xa4w,1070
2
+ fastapiauthenticator/version.py,sha256=2E5Gs4jtm0QsMW0wDxXSfLOzUQteQzS1wu23GRZA064,20
3
+ fastapi_ui_auth-0.0.1b0.dist-info/METADATA,sha256=hii-1Vny63Kn7jt6OFAWF_nHHYdgUdWaDHq_bmEAK8E,582
4
+ fastapi_ui_auth-0.0.1b0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
5
+ fastapi_ui_auth-0.0.1b0.dist-info/top_level.txt,sha256=EpDRP7uLM0f-Vd5rUtLBh4MTMAnpXzw1pr0DSknW_Ds,21
6
+ fastapi_ui_auth-0.0.1b0.dist-info/RECORD,,
@@ -1 +1 @@
1
- version = "0.0.1"
1
+ version = "0.0.1-b"
@@ -1,102 +0,0 @@
1
- Metadata-Version: 2.4
2
- Name: FastAPI-UI-Auth
3
- Version: 0.0.1
4
- Summary: Python module to add username and password authentication to specific FastAPI routes
5
- Requires-Python: >=3.11
6
- Description-Content-Type: text/markdown
7
- License-File: LICENSE
8
- Requires-Dist: fastapi==0.115.*
9
- Requires-Dist: jinja2==3.1.*
10
- Requires-Dist: pydantic==2.11.*
11
- Requires-Dist: python-dotenv==1.1.*
12
- Provides-Extra: dev
13
- Requires-Dist: pre-commit==4.2.*; extra == "dev"
14
- Requires-Dist: uvicorn==0.34.*; extra == "dev"
15
- Dynamic: license-file
16
-
17
- # FastAPIAuthenticator
18
-
19
- Python module to add username and password authentication to specific FastAPI routes
20
-
21
- ![Python][label-pyversion]
22
-
23
- **Platform Supported**
24
-
25
- ![Platform][label-platform]
26
-
27
- ## Installation
28
-
29
- ```shell
30
- repo="thevickypedia/FastAPIAuthenticator"
31
-
32
- latest=$(curl -s https://api.github.com/repos/${repo}/tags | jq -r '.[0].name')
33
-
34
- pip install "git+https://github.com/${repo}.git@${latest}"
35
- ```
36
-
37
- ## Usage
38
-
39
- ```python
40
- import fastapiauthenticator
41
-
42
- from fastapi import FastAPI
43
-
44
- app = FastAPI()
45
-
46
-
47
- @app.get("/public")
48
- def public_route():
49
- return {"message": "This is a public route"}
50
-
51
-
52
- def private_route():
53
- return {"message": "This is a private route"}
54
-
55
-
56
- fastapiauthenticator.Authenticator(app=app, secure_function=private_route)
57
- ```
58
-
59
- ## Coding Standards
60
- Docstring format: [`Google`][google-docs] <br>
61
- Styling conventions: [`PEP 8`][pep8] and [`isort`][isort]
62
-
63
- ## [Release Notes][release-notes]
64
- **Requirement**
65
- ```shell
66
- python -m pip install gitverse
67
- ```
68
-
69
- **Usage**
70
- ```shell
71
- gitverse-release reverse -f release_notes.rst -t 'Release Notes'
72
- ```
73
-
74
- ## Linting
75
-
76
- **Requirement**
77
- ```shell
78
- python -m pip install sphinx==5.1.1 pre-commit recommonmark
79
- ```
80
-
81
- **Usage**
82
- ```shell
83
- pre-commit run --all-files
84
- ```
85
-
86
- ## License & copyright
87
-
88
- &copy; Vignesh Rao
89
-
90
- Licensed under the [MIT License][license]
91
-
92
- [//]: # (Labels)
93
-
94
- [3.11]: https://docs.python.org/3/whatsnew/3.11.html
95
- [license]: https://github.com/thevickypedia/FastAPIAuthenticator/blob/main/LICENSE
96
- [google-docs]: https://google.github.io/styleguide/pyguide.html#38-comments-and-docstrings
97
- [pep8]: https://www.python.org/dev/peps/pep-0008/
98
- [isort]: https://pycqa.github.io/isort/
99
-
100
- [label-pyversion]: https://img.shields.io/badge/python-3.11%20%7C%203.12-blue
101
- [label-platform]: https://img.shields.io/badge/Platform-Linux|macOS|Windows-1f425f.svg
102
- [release-notes]: https://github.com/thevickypedia/FastAPIAuthenticator/blob/main/release_notes.rst
@@ -1,16 +0,0 @@
1
- fastapi_ui_auth-0.0.1.dist-info/licenses/LICENSE,sha256=_sOIKJWdD2o1WwwDIwYB2qTP2nlSWqT5Tyg9jr1Xa4w,1070
2
- fastapiauthenticator/__init__.py,sha256=b8LChxHIMiYL64em-dnSAglCgQwt0qL8NgFaaLWwFUg,213
3
- fastapiauthenticator/endpoints.py,sha256=9qGQbLny8OtkcJg4D5xT2gJO43ivuQdrY8A6xhQ7ma4,2161
4
- fastapiauthenticator/enums.py,sha256=FSKEU8QjAvW8kijMgbSATokd6Ig6EQ4G-_xW7hfz5Jk,373
5
- fastapiauthenticator/models.py,sha256=itH5YsFJa8-eR1iRm_UCkBtrLZ7ypDMNt8tnaJGiMuk,1778
6
- fastapiauthenticator/secure.py,sha256=ZOH6kT4BD56VqwaKdKocX7eSE8tqZcu-tK0QOmjY58k,1089
7
- fastapiauthenticator/service.py,sha256=mJURiebq6nugXh6PkFsZkTAbwkkvKXJJb0CJDjsENNc,7013
8
- fastapiauthenticator/utils.py,sha256=-wLLL7Qd9TSBYL_qla69Ng6gnMQtpHlrnZd_CqIxC2o,5528
9
- fastapiauthenticator/version.py,sha256=CactNZqrHHYTPrkHKccy2WKXmaiUdtTgPqSjFyVXnJk,18
10
- fastapiauthenticator/templates/index.html,sha256=mA2R6gk6lvibq_AmPgGHBFQijYtNUD7IIfeBSJWQrM4,9078
11
- fastapiauthenticator/templates/session.html,sha256=LUCvcEdQOjfIXjRZ2gPx2s5wyzNuCve4OMge0hXaBLM,3053
12
- fastapiauthenticator/templates/unauthorized.html,sha256=UZo1Jt64-CFfjwWTGicUMdHVWkYkXCJBRxvit4QTiQM,3015
13
- fastapi_ui_auth-0.0.1.dist-info/METADATA,sha256=XZDh_Vi29aIxQSGJnKEMnn1woBYkvsgTuJ3fR2DPi2c,2402
14
- fastapi_ui_auth-0.0.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
15
- fastapi_ui_auth-0.0.1.dist-info/top_level.txt,sha256=EpDRP7uLM0f-Vd5rUtLBh4MTMAnpXzw1pr0DSknW_Ds,21
16
- fastapi_ui_auth-0.0.1.dist-info/RECORD,,
@@ -1,3 +0,0 @@
1
- from fastapiauthenticator.enums import APIEndpoints # noqa: F401,E402
2
- from fastapiauthenticator.service import Authenticator # noqa: F401,E402
3
- from fastapiauthenticator.version import version # noqa: F401,E402
@@ -1,76 +0,0 @@
1
- from fastapi.requests import Request
2
- from fastapi.responses import HTMLResponse
3
-
4
- from fastapiauthenticator import enums, models, utils
5
- from fastapiauthenticator.version import version
6
-
7
-
8
- def session(request: Request) -> HTMLResponse:
9
- """Renders the session error page.
10
-
11
- Args:
12
- request: Reference to the FastAPI request object.
13
-
14
- Returns:
15
- HTMLResponse:
16
- Returns an HTML response templated using Jinja2.
17
- """
18
- return utils.clear_session(
19
- request,
20
- models.templates.TemplateResponse(
21
- name="session.html",
22
- context={
23
- "request": request,
24
- "signin": enums.APIEndpoints.fastapi_login,
25
- "reason": "Session expired or invalid.",
26
- "fallback_path": models.fallback.path,
27
- "fallback_button": models.fallback.button,
28
- "version": f"v{version}",
29
- },
30
- ),
31
- )
32
-
33
-
34
- def login(request: Request) -> HTMLResponse:
35
- """Render the login page with the verification path and version.
36
-
37
- Returns:
38
- HTMLResponse:
39
- Rendered HTML response for the login page.
40
- """
41
- return utils.clear_session(
42
- request,
43
- models.templates.TemplateResponse(
44
- name="index.html",
45
- context={
46
- "request": request,
47
- "signin": enums.APIEndpoints.fastapi_verify_login,
48
- "version": f"v{version}",
49
- },
50
- ),
51
- )
52
-
53
-
54
- def error(request: Request) -> HTMLResponse:
55
- """Error endpoint for the authenticator.
56
-
57
- Args:
58
- request: Reference to the FastAPI request object.
59
-
60
- Returns:
61
- HTMLResponse:
62
- Returns an HTML response templated using Jinja2.
63
- """
64
- return utils.clear_session(
65
- request,
66
- models.templates.TemplateResponse(
67
- name="unauthorized.html",
68
- context={
69
- "request": request,
70
- "signin": enums.APIEndpoints.fastapi_login,
71
- "fallback_path": models.fallback.path,
72
- "fallback_button": models.fallback.button,
73
- "version": f"v{version}",
74
- },
75
- ),
76
- )
@@ -1,16 +0,0 @@
1
- from enum import StrEnum
2
-
3
-
4
- class APIEndpoints(StrEnum):
5
- """API endpoints for all the routes.
6
-
7
- >>> APIEndpoints
8
-
9
- """
10
-
11
- fastapi_error = "/fastapi-error"
12
- fastapi_login = "/fastapi-login"
13
- fastapi_logout = "/fastapi-logout"
14
- fastapi_secure = "/fastapi-secure"
15
- fastapi_session = "/fastapi-session"
16
- fastapi_verify_login = "/fastapi-verify-login"
@@ -1,60 +0,0 @@
1
- import pathlib
2
- from typing import Dict, Optional
3
-
4
- from fastapi.templating import Jinja2Templates
5
- from pydantic import BaseModel, Field
6
-
7
- templates = Jinja2Templates(directory=pathlib.Path(__file__).parent / "templates")
8
-
9
-
10
- class WSSession(BaseModel):
11
- """Object to store websocket session information.
12
-
13
- >>> WSSession
14
-
15
- """
16
-
17
- invalid: Dict[str, int] = Field(default_factory=dict)
18
- client_auth: Dict[str, Dict[str, int]] = Field(default_factory=dict)
19
-
20
-
21
- class Fallback(BaseModel):
22
- """Object to store fallback information.
23
-
24
- >>> Fallback
25
-
26
- """
27
-
28
- button: str = Field(default="LOGIN", description="Title for the fallback button.")
29
- path: str = Field(
30
- default="/", description="Path to redirect when fallback button is clicked."
31
- )
32
-
33
-
34
- class RedirectException(Exception):
35
- """Custom ``RedirectException`` raised within the API since HTTPException doesn't support returning HTML content.
36
-
37
- >>> RedirectException
38
-
39
- See Also:
40
- - RedirectException allows the API to redirect on demand in cases where returning is not a solution.
41
- - There are alternatives to raise HTML content as an exception but none work with our use-case with JavaScript.
42
- - This way of exception handling comes handy for many unexpected scenarios.
43
-
44
- References:
45
- https://fastapi.tiangolo.com/tutorial/handling-errors/#install-custom-exception-handlers
46
- """
47
-
48
- def __init__(self, location: str, detail: Optional[str] = ""):
49
- """Instantiates the ``RedirectException`` object with the required parameters.
50
-
51
- Args:
52
- location: Location for redirect.
53
- detail: Reason for redirect.
54
- """
55
- self.location = location
56
- self.detail = detail
57
-
58
-
59
- ws_session = WSSession()
60
- fallback = Fallback()
@@ -1,41 +0,0 @@
1
- import base64
2
- import binascii
3
- import hashlib
4
- import string
5
- from typing import Any
6
-
7
- UNICODE_PREFIX = (
8
- base64.b64decode(b"XA==").decode(encoding="ascii")
9
- + string.ascii_letters[20]
10
- + string.digits[:1] * 2
11
- )
12
-
13
-
14
- def calculate_hash(value: Any) -> str:
15
- """Generate hash value for the given payload."""
16
- return hashlib.sha512(bytes(value, "utf-8")).hexdigest()
17
-
18
-
19
- def base64_encode(value: Any) -> str:
20
- """Base64 encode the given payload."""
21
- encoded_bytes = base64.b64encode(value.encode("utf-8"))
22
- return encoded_bytes.decode("utf-8")
23
-
24
-
25
- def base64_decode(value: Any) -> str:
26
- """Base64 decode the given payload."""
27
- return base64.b64decode(value).decode("utf-8")
28
-
29
-
30
- def hex_decode(value: Any) -> str:
31
- """Convert hex value to a string."""
32
- return bytes(value, "utf-8").decode(encoding="unicode_escape")
33
-
34
-
35
- def hex_encode(value: str):
36
- """Convert string value to hex."""
37
- return UNICODE_PREFIX + UNICODE_PREFIX.join(
38
- binascii.hexlify(data=value.encode(encoding="utf-8"), sep="-")
39
- .decode(encoding="utf-8")
40
- .split(sep="-")
41
- )
@@ -1,180 +0,0 @@
1
- import logging
2
- import os
3
- from threading import Timer
4
- from typing import Callable, Dict, List
5
-
6
- import dotenv
7
- from fastapi import FastAPI
8
- from fastapi.params import Depends
9
- from fastapi.requests import Request
10
- from fastapi.responses import Response
11
- from fastapi.routing import APIRoute
12
- from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
13
-
14
- from fastapiauthenticator import endpoints, enums, models, utils
15
-
16
- dotenv.load_dotenv(dotenv_path=dotenv.find_dotenv(), override=True)
17
- LOGGER = logging.getLogger("uvicorn.default")
18
- BEARER_AUTH = HTTPBearer()
19
-
20
-
21
- # noinspection PyDefaultArgument
22
- class Authenticator:
23
- """Authenticator is a FastAPI integration that provides authentication for secure routes.
24
-
25
- >>> Authenticator
26
-
27
- """
28
-
29
- def __init__(
30
- self,
31
- app: FastAPI,
32
- secure_function: Callable = None,
33
- secure_methods: List[str] = ["GET", "POST"],
34
- secure_path: str = enums.APIEndpoints.fastapi_secure,
35
- username: str = os.environ.get("USERNAME"),
36
- password: str = os.environ.get("PASSWORD"),
37
- session_timeout: int = 3600,
38
- fallback_button: str = models.fallback.button,
39
- fallback_path: str = models.fallback.path,
40
- ):
41
- """Initialize the APIAuthenticator with the FastAPI app and secure function.
42
-
43
- Args:
44
- app: FastAPI application instance to which the authenticator will be added.
45
- secure_function: Function to be called for secure routes after authentication.
46
- secure_methods: List of HTTP methods that the secure function will handle.
47
- secure_path: Path for the secure route, must start with '/'.
48
- username: Username for authentication, can be set via environment variable 'USERNAME'.
49
- password: Password for authentication, can be set via environment variable 'PASSWORD'.
50
- session_timeout: Duration in seconds after which the session expires.
51
- fallback_button: Title for the fallback button, defaults to "LOGIN".
52
- fallback_path: Fallback path to redirect to in case of session timeout or invalid session.
53
- """
54
- assert all((username, password)), "'username' and 'password' are mandatory."
55
- assert secure_function, "Secure function must be provided."
56
- assert secure_path.startswith("/"), "Secure path must start with '/'"
57
- assert fallback_path.startswith("/"), "Fallback path must start with '/'"
58
-
59
- self.app = app
60
- self.secure_methods = secure_methods
61
- self.secure_function = secure_function
62
- self.secure_path = secure_path
63
- self.session_timeout = session_timeout
64
- models.fallback.path = fallback_path
65
- models.fallback.button = fallback_button
66
-
67
- # noinspection PyTypeChecker
68
- self.app.add_exception_handler(
69
- exc_class_or_status_code=models.RedirectException,
70
- handler=utils.redirect_exception_handler,
71
- )
72
-
73
- self.username = username
74
- self.password = password
75
-
76
- self._secure()
77
-
78
- def _verify_auth(
79
- self,
80
- request: Request,
81
- authorization: HTTPAuthorizationCredentials = Depends(BEARER_AUTH),
82
- response: Response = None,
83
- ) -> Dict[str, str]:
84
- """Verify the authentication credentials and redirect to the secure route.
85
-
86
- Args:
87
- request: Request object containing client information.
88
- authorization: Authorization credentials from the request, provided by FastAPI's HTTPBearer.
89
- response: Response object containing the response from FastAPI's HTTPBearer.
90
-
91
- Returns:
92
- Dict[str, str]:
93
- A dictionary containing the redirect URL to the secure path.
94
- """
95
- utils.verify_login(
96
- authorization=authorization,
97
- host=request.client.host,
98
- env_username=self.username,
99
- env_password=self.password,
100
- )
101
- secure_route = APIRoute(
102
- path=self.secure_path,
103
- endpoint=self.secure_function,
104
- methods=self.secure_methods,
105
- dependencies=[Depends(utils.session_check)],
106
- )
107
- self.app.routes.append(secure_route)
108
- LOGGER.info("Setting session timeout for %s seconds", self.session_timeout)
109
- self._handle_session(
110
- response=response, request=request, secure_route=secure_route
111
- )
112
- return {"redirect_url": self.secure_path}
113
-
114
- def _setup_session_route(self, secure_route: APIRoute) -> None:
115
- """Removes the secure route and adds a routing logic for invalid sessions.
116
-
117
- Args:
118
- secure_route: Secure route to be removed from the app after the session timeout.
119
- """
120
- LOGGER.info("Session expired, removing secure route: %s", secure_route.path)
121
- self.app.routes.remove(secure_route)
122
- LOGGER.info(
123
- "Adding session route to handle expired sessions at %s", self.secure_path
124
- )
125
- self.app.routes.append(
126
- APIRoute(
127
- path=self.secure_path,
128
- endpoint=endpoints.session,
129
- methods=["GET"],
130
- )
131
- )
132
-
133
- def _handle_session(
134
- self, response: Response, request: Request, secure_route: APIRoute
135
- ) -> None:
136
- """Handle session management by setting a cookie and scheduling session removal.
137
-
138
- Args:
139
- response: Response object to set the session cookie.
140
- request: Request object containing client information.
141
- secure_route: Secure route to be removed from the app after the session timeout.
142
- """
143
- # Remove the secure route after the session timeout - backend
144
- Timer(
145
- function=self._setup_session_route,
146
- args=(secure_route,),
147
- interval=self.session_timeout,
148
- ).start()
149
- # Set the max age in session cookie to session timeout - frontend
150
- response.set_cookie(
151
- key="session_token",
152
- value=models.ws_session.client_auth[request.client.host].get("token"),
153
- httponly=True,
154
- samesite="strict",
155
- max_age=self.session_timeout,
156
- )
157
-
158
- def _secure(self) -> None:
159
- """Create the login and verification routes for the APIAuthenticator."""
160
- login_route = APIRoute(
161
- path=enums.APIEndpoints.fastapi_login,
162
- endpoint=endpoints.login,
163
- methods=["GET"],
164
- )
165
- error_route = APIRoute(
166
- path=enums.APIEndpoints.fastapi_error,
167
- endpoint=endpoints.error,
168
- methods=["GET"],
169
- )
170
- session_route = APIRoute(
171
- path=enums.APIEndpoints.fastapi_session,
172
- endpoint=endpoints.session,
173
- methods=["GET"],
174
- )
175
- verify_route = APIRoute(
176
- path=enums.APIEndpoints.fastapi_verify_login,
177
- endpoint=self._verify_auth,
178
- methods=["POST"],
179
- )
180
- self.app.routes.extend([login_route, session_route, error_route, verify_route])
@@ -1,259 +0,0 @@
1
- <!DOCTYPE html>
2
- <!--suppress JSUnresolvedLibraryURL -->
3
- <html lang="en">
4
- <head>
5
- <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
6
- <title>FastAPI - Authenticator</title>
7
- <meta property="og:type" content="Authenticator">
8
- <meta name="keywords" content="Python, fastapi, JavaScript, HTML, CSS">
9
- <meta name="author" content="Vignesh Rao">
10
- <meta content="width=device-width, initial-scale=1" name="viewport">
11
- <script src="https://code.jquery.com/jquery-3.6.4.min.js"></script>
12
- <script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/3.1.9-1/crypto-js.js"></script>
13
- <!-- Favicon.ico and Apple Touch Icon -->
14
- <link property="og:image" rel="icon" href="https://thevickypedia.github.io/open-source/images/logo/fastapi.ico">
15
- <link property="og:image" rel="apple-touch-icon"
16
- href="https://thevickypedia.github.io/open-source/images/logo/fastapi.png">
17
- <!-- Font Awesome icons -->
18
- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/fontawesome.min.css">
19
- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/solid.min.css">
20
- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/regular.min.css">
21
- <style>
22
- body {
23
- font-family: 'Arial', sans-serif;
24
- margin: 0;
25
- padding: 0;
26
- background-color: #151515;
27
- }
28
-
29
- .container {
30
- max-width: 400px;
31
- margin: 50px auto;
32
- background: #fff;
33
- padding: 20px;
34
- box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
35
- border-radius: 8px;
36
- }
37
-
38
- .header {
39
- text-align: center;
40
- margin-bottom: 20px;
41
- }
42
-
43
- .header img {
44
- max-width: 150px;
45
- }
46
-
47
- .content {
48
- display: flex;
49
- flex-direction: column;
50
- }
51
-
52
- form {
53
- display: flex;
54
- flex-direction: column;
55
- }
56
-
57
- label {
58
- margin-top: 10px;
59
- color: #000000;
60
- font-size: large;
61
- font-family: 'Courier New', sans-serif;
62
- font-weight: normal;
63
- }
64
-
65
- input {
66
- padding: 10px;
67
- margin-bottom: 15px;
68
- border: 1px solid #ddd;
69
- border-radius: 5px;
70
- }
71
-
72
- button {
73
- padding: 12px;
74
- background-color: #000000;
75
- color: #fff;
76
- border: none;
77
- border-radius: 5px;
78
- cursor: pointer;
79
- font-weight: bold;
80
- }
81
-
82
- button:hover {
83
- background-color: #494949;
84
- }
85
-
86
- footer {
87
- bottom: 20px;
88
- position: fixed;
89
- width: 100%;
90
- color: #fff;
91
- text-align: center;
92
- font-family: 'Courier New', monospace;
93
- font-size: small;
94
- }
95
- </style>
96
- <style>
97
- .password-container {
98
- position: relative;
99
- }
100
-
101
- .password-container input[type="password"],
102
- .password-container input[type="text"] {
103
- width: 100%;
104
- padding: 12px 36px 12px 12px;
105
- box-sizing: border-box;
106
- }
107
-
108
- .fa-eye, .fa-eye-slash {
109
- position: absolute;
110
- top: 40%;
111
- right: 4%;
112
- transform: translateY(-50%);
113
- cursor: pointer;
114
- color: lightgray;
115
- }
116
- </style>
117
- <noscript>
118
- <style>
119
- body {
120
- width: 100%;
121
- height: 100%;
122
- overflow: hidden;
123
- }
124
- </style>
125
- <div style="position: fixed; text-align:center; height: 100%; width: 100%; background-color: #151515;">
126
- <h2 style="margin-top:5%">This page requires JavaScript
127
- to be enabled.
128
- <br><br>
129
- Please refer <a href="https://www.enable-javascript.com/">enable-javascript</a> for how to.
130
- </h2>
131
- <form>
132
- <button type="submit" onClick="<meta httpEquiv='refresh' content='0'>">RETRY</button>
133
- </form>
134
- </div>
135
- </noscript>
136
- </head>
137
- <body>
138
- <div class="container">
139
- <div class="header">
140
- <i class="fa-solid fa-user"></i>
141
- </div>
142
- <div class="content">
143
- <!-- <form action="{ url_for('signin') }" method="post"> -->
144
- <form>
145
- <label for="username">Username:</label>
146
- <input type="text" id="username" name="username" required>
147
- <label for="password">Password:</label>
148
- <div class="password-container">
149
- <input type="password" id="password" name="password" required>
150
- <i class="fa-regular fa-eye" id="eye"></i>
151
- </div>
152
- <button type="submit" onclick="submitToAPI(event)">Sign In</button>
153
- </form>
154
- </div>
155
- </div>
156
- </body>
157
- <!-- control the behavior of the browser's navigation without triggering a full page reload -->
158
- <script>
159
- document.addEventListener('DOMContentLoaded', function () {
160
- history.pushState(null, document.title, location.href);
161
- window.addEventListener('popstate', function (event) {
162
- history.pushState(null, document.title, location.href);
163
- });
164
- });
165
- </script>
166
- <!-- handle authentication from login page -->
167
- <script>
168
- async function ConvertStringToHex(str) {
169
- let arr = [];
170
- for (let i = 0; i < str.length; i++) {
171
- arr[i] = ("00" + str.charCodeAt(i).toString(16)).slice(-4);
172
- }
173
- return "\\u" + arr.join("\\u");
174
- }
175
-
176
- async function CalculateHash(username, password, timestamp) {
177
- const message = username + password + timestamp;
178
- const encoder = new TextEncoder();
179
- const data = encoder.encode(message);
180
- if (crypto.subtle === undefined) {
181
- const wordArray = CryptoJS.lib.WordArray.create(data);
182
- const hash = CryptoJS.SHA512(wordArray);
183
- // Convert the hash to a hexadecimal string and return it
184
- return hash.toString(CryptoJS.enc.Hex);
185
- } else {
186
- const hashBuffer = await crypto.subtle.digest('SHA-512', data);
187
- const hashArray = Array.from(new Uint8Array(hashBuffer));
188
- // Convert each byte to a hexadecimal string, pad with zeros, and join them to form the final hash
189
- return hashArray.map(byte => byte.toString(16).padStart(2, '0')).join('');
190
- }
191
- }
192
-
193
- async function submitToAPI(event) {
194
- event.preventDefault();
195
- const username = $("#username").val();
196
- const password = $("#password").val();
197
- if (username === "" || password === "") {
198
- alert("ERROR: Username and password are required to authenticate your request!");
199
- return false;
200
- }
201
-
202
- let hex_user = await ConvertStringToHex(username);
203
- let hex_pass = await ConvertStringToHex(password);
204
- let timestamp = Math.round(new Date().getTime() / 1000);
205
- let hash = await CalculateHash(hex_user, hex_pass, timestamp)
206
- let authHeaderValue = hex_user + ',' + hash + ',' + timestamp;
207
- let origin = window.location.origin
208
- $.ajax({
209
- method: "POST",
210
- url: origin.concat("{{ signin }}"),
211
- headers: {
212
- 'accept': 'application/json',
213
- 'Authorization': `Bearer ${btoa(authHeaderValue)}`,
214
- },
215
- crossDomain: "true",
216
- contentType: "application/json; charset=utf-8",
217
- success: function (data) {
218
- // Check if the response contains a redirect URL
219
- if (data.redirect_url) {
220
- // Manually handle the redirect
221
- window.location.href = data.redirect_url;
222
- } else {
223
- console.log("Unhandled good response data")
224
- // Handle success if needed
225
- console.log(data);
226
- }
227
- },
228
- error: function (jqXHR, textStatus, errorThrown) {
229
- console.error(`Status: ${textStatus}, Error: ${errorThrown}`);
230
- if (jqXHR.hasOwnProperty("responseJSON")) {
231
- alert(jqXHR.responseJSON.detail);
232
- } else {
233
- alert(errorThrown);
234
- }
235
- }
236
- });
237
- }
238
-
239
- </script>
240
- <footer>
241
- <div class="footer">
242
- PyAPIAuthenticator - {{ version }}<br>
243
- <a href="https://github.com/thevickypedia/pyapiauthenticator">https://github.com/thevickypedia/pyapiauthenticator</a>
244
- </div>
245
- </footer>
246
- <script>
247
- const passwordInput = document.querySelector("#password")
248
- const eye = document.querySelector("#eye")
249
- eye.addEventListener("click", function () {
250
- if (passwordInput.type === "password") {
251
- passwordInput.type = "text";
252
- eye.className = "fa-regular fa-eye-slash"
253
- } else {
254
- passwordInput.type = "password";
255
- eye.className = "fa-regular fa-eye"
256
- }
257
- });
258
- </script>
259
- </html>
@@ -1,86 +0,0 @@
1
- <!DOCTYPE html>
2
- <html lang="en">
3
- <head>
4
- <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
5
- <title>FastAPI - Authenticator</title>
6
- <meta property="og:type" content="Authenticator">
7
- <meta name="keywords" content="Python, fastapi, JavaScript, HTML, CSS">
8
- <meta name="author" content="Vignesh Rao">
9
- <link property="og:image" rel="icon" href="https://thevickypedia.github.io/open-source/images/logo/fastapi.ico">
10
- <link property="og:image" rel="apple-touch-icon"
11
- href="https://thevickypedia.github.io/open-source/images/logo/fastapi.png">
12
- <meta content="width=device-width, initial-scale=1" name="viewport">
13
- <style>
14
- img {
15
- display: block;
16
- margin-left: auto;
17
- margin-right: auto;
18
- }
19
-
20
- :is(h1, h2, h3, h4, h5, h6) {
21
- text-align: center;
22
- color: #F0F0F0;
23
- font-family: 'Courier New', sans-serif;
24
- }
25
-
26
- button {
27
- width: 100px;
28
- margin: 0 auto;
29
- display: block;
30
- }
31
-
32
- body {
33
- background-color: #151515;
34
- }
35
-
36
- footer {
37
- bottom: 20px;
38
- position: fixed;
39
- width: 100%;
40
- color: #fff;
41
- text-align: center;
42
- font-family: 'Courier New', monospace;
43
- font-size: small;
44
- }
45
- </style>
46
- <noscript>
47
- <style>
48
- body {
49
- width: 100%;
50
- height: 100%;
51
- overflow: hidden;
52
- }
53
- </style>
54
- <div style="position: fixed; text-align:center; height: 100%; width: 100%; background-color: #151515">
55
- <h2 style="margin-top:5%">This page requires JavaScript
56
- to be enabled.
57
- <br><br>
58
- Please refer <a href="https://www.enable-javascript.com/">enable-javascript</a> for how to.
59
- </h2>
60
- <form>
61
- <button type="submit" onClick="<meta httpEquiv='refresh' content='0'>">RETRY</button>
62
- </form>
63
- </div>
64
- </noscript>
65
- </head>
66
- <body>
67
- <h2 style="margin-top:5%">{{ reason }}</h2>
68
- <h3>Authentication doesn't last forever ¯\_(ツ)_/¯ </h3>
69
- <p>
70
- <img src="https://thevickypedia.github.io/open-source/images/gif/shattered_fusion.gif"
71
- onerror="this.src='https://vigneshrao.com/open-source/images/gif/shattered_fusion.gif'"
72
- width="200" height="200" alt="Image" class="center">
73
- </p>
74
- <button style="text-align:center" onClick="window.location.href = '{{ fallback_path }}';">{{ fallback_button }}</button>
75
- <br>
76
- <button style="text-align:center" onClick="alert('Forgot Password?\n\nRelax and try to remember your password.');">HELP
77
- </button>
78
- <h4>Click <a href="https://vigneshrao.com/contact">HERE</a> to reach out.</h4>
79
- </body>
80
- <footer>
81
- <div class="footer">
82
- FastAPIAuthenticator - {{ version }}<br>
83
- <a href="https://github.com/thevickypedia/FastAPIAuthenticator">https://github.com/thevickypedia/FastAPIAuthenticator</a>
84
- </div>
85
- </footer>
86
- </html>
@@ -1,86 +0,0 @@
1
- <!DOCTYPE html>
2
- <html lang="en">
3
- <head>
4
- <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
5
- <title>FastAPI - Authenticator</title>
6
- <meta property="og:type" content="Authenticator">
7
- <meta name="keywords" content="Python, fastapi, JavaScript, HTML, CSS">
8
- <meta name="author" content="Vignesh Rao">
9
- <link property="og:image" rel="icon" href="https://thevickypedia.github.io/open-source/images/logo/fastapi.ico">
10
- <link property="og:image" rel="apple-touch-icon"
11
- href="https://thevickypedia.github.io/open-source/images/logo/fastapi.png">
12
- <meta content="width=device-width, initial-scale=1" name="viewport">
13
- <style>
14
- img {
15
- display: block;
16
- margin-left: auto;
17
- margin-right: auto;
18
- }
19
-
20
- :is(h1, h2, h3, h4, h5, h6) {
21
- text-align: center;
22
- color: #F0F0F0;
23
- font-family: 'Courier New', sans-serif;
24
- }
25
-
26
- button {
27
- width: 100px;
28
- margin: 0 auto;
29
- display: block;
30
- }
31
-
32
- body {
33
- background-color: #151515;
34
- }
35
-
36
- footer {
37
- bottom: 20px;
38
- position: fixed;
39
- width: 100%;
40
- color: #fff;
41
- text-align: center;
42
- font-family: 'Courier New', monospace;
43
- font-size: small;
44
- }
45
- </style>
46
- <noscript>
47
- <style>
48
- body {
49
- width: 100%;
50
- height: 100%;
51
- overflow: hidden;
52
- }
53
- </style>
54
- <div style="position: fixed; text-align:center; height: 100%; width: 100%; background-color: #151515;">
55
- <h2 style="margin-top:5%">This page requires JavaScript
56
- to be enabled.
57
- <br><br>
58
- Please refer <a href="https://www.enable-javascript.com/">enable-javascript</a> for how to.
59
- </h2>
60
- <form>
61
- <button type="submit" onClick="<meta httpEquiv='refresh' content='0'>">RETRY</button>
62
- </form>
63
- </div>
64
- </noscript>
65
- </head>
66
- <body>
67
- <h2 style="margin-top:5%">LOGIN FAILED</h2>
68
- <h3>USER ERROR - REPLACE USER</h3>
69
- <p>
70
- <img src="https://thevickypedia.github.io/open-source/images/gif/lockscape.gif"
71
- onerror="this.src='https://vigneshrao.com/open-source/images/gif/lockscape.gif'"
72
- width="200" height="170" alt="Image" class="center">
73
- </p>
74
- <button style="text-align:center" onClick="window.location.href = '{{ fallback_path }}';">{{ fallback_button }}</button>
75
- <br>
76
- <button style="text-align:center" onClick="alert('Forgot Password?\n\nRelax and try to remember your password.');">HELP
77
- </button>
78
- <h4>Click <a href="https://vigneshrao.com/contact">HERE</a> to reach out.</h4>
79
- </body>
80
- <footer>
81
- <div class="footer">
82
- FastAPIAuthenticator - {{ version }}<br>
83
- <a href="https://github.com/thevickypedia/FastAPIAuthenticator">https://github.com/thevickypedia/FastAPIAuthenticator</a>
84
- </div>
85
- </footer>
86
- </html>
@@ -1,167 +0,0 @@
1
- import logging
2
- import secrets
3
- from typing import Dict, List, NoReturn, Union
4
-
5
- from fastapi import status
6
- from fastapi.exceptions import HTTPException
7
- from fastapi.requests import Request
8
- from fastapi.responses import HTMLResponse, JSONResponse, RedirectResponse
9
- from fastapi.security import HTTPAuthorizationCredentials
10
-
11
- from fastapiauthenticator import enums, models, secure
12
-
13
- LOGGER = logging.getLogger("uvicorn.default")
14
-
15
-
16
- def failed_auth_counter(host: str) -> None:
17
- """Keeps track of failed login attempts from each host, and redirects if failed for 3 or more times.
18
-
19
- Args:
20
- host: Host header from the request.
21
- """
22
- try:
23
- models.ws_session.invalid[host] += 1
24
- except KeyError:
25
- models.ws_session.invalid[host] = 1
26
- if models.ws_session.invalid[host] >= 3:
27
- raise models.RedirectException(location=enums.APIEndpoints.fastapi_error)
28
-
29
-
30
- def redirect_exception_handler(
31
- request: Request, exception: models.RedirectException
32
- ) -> JSONResponse | RedirectResponse:
33
- """Custom exception handler to handle redirect.
34
-
35
- Args:
36
- request: Takes the ``Request`` object as an argument.
37
- exception: Takes the ``RedirectException`` object inherited from ``Exception`` as an argument.
38
-
39
- Returns:
40
- JSONResponse:
41
- Returns the JSONResponse with content, status code and cookie.
42
- """
43
- LOGGER.warning("Exception headers: %s", request.headers)
44
- LOGGER.warning("Exception cookies: %s", request.cookies)
45
- if request.url.path == enums.APIEndpoints.fastapi_verify_login:
46
- response = JSONResponse(
47
- content={"redirect_url": exception.location}, status_code=200
48
- )
49
- else:
50
- response = RedirectResponse(url=exception.location)
51
- if exception.detail:
52
- response.set_cookie(
53
- "detail", exception.detail.upper(), httponly=True, samesite="strict"
54
- )
55
- return response
56
-
57
-
58
- def raise_error(host: str) -> NoReturn:
59
- """Raises a 401 Unauthorized error in case of bad credentials.
60
-
61
- Args:
62
- host: Host header from the request.
63
- """
64
- failed_auth_counter(host)
65
- LOGGER.error(
66
- "Incorrect username or password: %d",
67
- models.ws_session.invalid[host],
68
- )
69
- raise HTTPException(
70
- status_code=status.HTTP_401_UNAUTHORIZED,
71
- detail="Incorrect username or password",
72
- headers=None,
73
- )
74
-
75
-
76
- def extract_credentials(
77
- authorization: HTTPAuthorizationCredentials, host: str
78
- ) -> List[str]:
79
- """Extract the credentials from ``Authorization`` headers and decode it before returning as a list of strings.
80
-
81
- Args:
82
- authorization: Authorization header from the request.
83
- host: Host header from the request.
84
- """
85
- if not authorization:
86
- raise_error(host)
87
- decoded_auth = secure.base64_decode(authorization.credentials)
88
- # convert hex to a string
89
- auth = secure.hex_decode(decoded_auth)
90
- return auth.split(",")
91
-
92
-
93
- def verify_login(
94
- authorization: HTTPAuthorizationCredentials,
95
- host: str,
96
- env_username: str,
97
- env_password: str,
98
- ) -> Dict[str, Union[str, int]]:
99
- """Verifies authentication and generates session token for each user.
100
-
101
- Returns:
102
- Dict[str, str]:
103
- Returns a dictionary with the payload required to create the session token.
104
- """
105
- username, signature, timestamp = extract_credentials(authorization, host)
106
- if secrets.compare_digest(username, env_username):
107
- hex_user = secure.hex_encode(env_username)
108
- hex_pass = secure.hex_encode(env_password)
109
- else:
110
- LOGGER.warning("User '%s' not allowed", username)
111
- raise_error(host)
112
- message = f"{hex_user}{hex_pass}{timestamp}"
113
- expected_signature = secure.calculate_hash(message)
114
- if secrets.compare_digest(signature, expected_signature):
115
- models.ws_session.invalid[host] = 0
116
- key = secrets.token_urlsafe(64)
117
- models.ws_session.client_auth[host] = dict(
118
- username=username, token=key, timestamp=int(timestamp)
119
- )
120
- return models.ws_session.client_auth[host]
121
- raise_error(host)
122
-
123
-
124
- def session_check(request: Request) -> None:
125
- """Check if the session is still valid.
126
-
127
- Args:
128
- request: Request object containing client information.
129
-
130
- Raises:
131
- HTTPException: If the session is invalid or expired.
132
- """
133
- stored_token = models.ws_session.client_auth.get(request.client.host, {}).get(
134
- "token"
135
- )
136
- session_token = request.cookies.get("session_token")
137
- if (
138
- stored_token
139
- and session_token
140
- and secrets.compare_digest(session_token, stored_token)
141
- ):
142
- LOGGER.info("Session is valid for host: %s", request.client.host)
143
- return
144
- raise models.RedirectException(
145
- location=enums.APIEndpoints.fastapi_session,
146
- detail="Session expired or invalid. Please log in again.",
147
- )
148
-
149
-
150
- def clear_session(request: Request, response: HTMLResponse) -> HTMLResponse:
151
- """Clear the session token from the response.
152
-
153
- Args:
154
- request: FastAPI ``request`` object.
155
- response: FastAPI ``response`` object.
156
-
157
- Returns:
158
- HTMLResponse:
159
- Returns the response object with the session token cleared.
160
- """
161
- for cookie in request.cookies:
162
- # Deletes all cookies stored in current session
163
- LOGGER.info("Deleting cookie: '%s'", cookie)
164
- response.delete_cookie(cookie)
165
- response.headers["Cache-Control"] = "no-cache, no-store, must-revalidate"
166
- response.headers["Authorization"] = ""
167
- return response