FastAPI-UI-Auth 0.3.2__py3-none-any.whl → 0.4.0a0__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.3.2.dist-info → fastapi_ui_auth-0.4.0a0.dist-info}/METADATA +23 -4
- fastapi_ui_auth-0.4.0a0.dist-info/RECORD +20 -0
- fastapi_ui_auth-0.4.0a0.dist-info/entry_points.txt +3 -0
- uiauth/__init__.py +87 -0
- uiauth/endpoints.py +1 -0
- uiauth/models.py +18 -4
- uiauth/otp.py +69 -0
- uiauth/secure.py +8 -0
- uiauth/service.py +29 -17
- uiauth/templates/index.html +14 -1
- uiauth/utils.py +15 -3
- uiauth/version.py +1 -1
- fastapi_ui_auth-0.3.2.dist-info/RECORD +0 -18
- {fastapi_ui_auth-0.3.2.dist-info → fastapi_ui_auth-0.4.0a0.dist-info}/WHEEL +0 -0
- {fastapi_ui_auth-0.3.2.dist-info → fastapi_ui_auth-0.4.0a0.dist-info}/licenses/LICENSE +0 -0
- {fastapi_ui_auth-0.3.2.dist-info → fastapi_ui_auth-0.4.0a0.dist-info}/top_level.txt +0 -0
|
@@ -1,21 +1,27 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: FastAPI-UI-Auth
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0a0
|
|
4
4
|
Summary: Python module to add username and password authentication to specific FastAPI routes
|
|
5
|
+
Project-URL: Homepage, https://github.com/thevickypedia/FastAPI-UI-Auth
|
|
6
|
+
Project-URL: Source, https://github.com/thevickypedia/FastAPI-UI-Auth
|
|
7
|
+
Project-URL: Bug Tracker, https://github.com/thevickypedia/FastAPI-UI-Auth/issues
|
|
8
|
+
Project-URL: Release Notes, https://github.com/thevickypedia/FastAPI-UI-Auth/blob/main/release_notes.rst
|
|
5
9
|
Requires-Python: >=3.11
|
|
6
10
|
Description-Content-Type: text/markdown
|
|
7
11
|
License-File: LICENSE
|
|
8
12
|
Requires-Dist: fastapi
|
|
9
13
|
Requires-Dist: Jinja2
|
|
10
14
|
Requires-Dist: pydantic
|
|
15
|
+
Requires-Dist: pyotp
|
|
11
16
|
Provides-Extra: dev
|
|
12
17
|
Requires-Dist: websockets==15.0.*; extra == "dev"
|
|
13
18
|
Requires-Dist: pre-commit==4.2.*; extra == "dev"
|
|
14
|
-
Requires-Dist: uvicorn==0.
|
|
19
|
+
Requires-Dist: uvicorn==0.49.*; extra == "dev"
|
|
20
|
+
Requires-Dist: qrcode[pil]==8.2.*; extra == "dev"
|
|
15
21
|
Provides-Extra: test
|
|
16
22
|
Requires-Dist: pytest; extra == "test"
|
|
17
23
|
Requires-Dist: pytest-cov; extra == "test"
|
|
18
|
-
Requires-Dist:
|
|
24
|
+
Requires-Dist: httpx2; extra == "test"
|
|
19
25
|
Dynamic: license-file
|
|
20
26
|
|
|
21
27
|
# FastAPIUIAuth
|
|
@@ -45,6 +51,7 @@ pip install FastAPI-UI-Auth
|
|
|
45
51
|
## Usage
|
|
46
52
|
|
|
47
53
|
```python
|
|
54
|
+
import logging
|
|
48
55
|
import uiauth
|
|
49
56
|
|
|
50
57
|
from fastapi import FastAPI
|
|
@@ -60,17 +67,29 @@ async def private_route():
|
|
|
60
67
|
return {"message": "This is a private route"}
|
|
61
68
|
|
|
62
69
|
uiauth.protect(
|
|
70
|
+
# ------ MANDATORY ARGS ------
|
|
63
71
|
app=app,
|
|
64
72
|
routes=APIRoute(
|
|
65
73
|
path="/private",
|
|
66
74
|
endpoint=private_route
|
|
67
|
-
)
|
|
75
|
+
),
|
|
76
|
+
username="admin",
|
|
77
|
+
password="password123",
|
|
78
|
+
# ------ OPTIONAL ARGS ------
|
|
79
|
+
totp_token="JBSWSECUREDK3PXP",
|
|
80
|
+
session_timeout=3600,
|
|
81
|
+
fallback=uiauth.Fallback(button="GO BACK", path="/public"),
|
|
82
|
+
custom_logger=logging.getLogger("my_custom_logger")
|
|
68
83
|
)
|
|
69
84
|
```
|
|
70
85
|
|
|
71
86
|
> `FastAPI-UI-Auth` supports both `APIRoute` and `APIWebSocketRoute` routes.<br>
|
|
72
87
|
> Refer [samples] directory for different use-cases.
|
|
73
88
|
|
|
89
|
+
> [!NOTE]
|
|
90
|
+
> Use the CLI command `uiauth-totp` to generate a TOTP token and a QR code to scan with an authenticator app
|
|
91
|
+
> (e.g. Google Authenticator, Authy, etc.) for 2FA support.
|
|
92
|
+
|
|
74
93
|
## Coding Standards
|
|
75
94
|
Docstring format: [`Google`][google-docs] <br>
|
|
76
95
|
Styling conventions: [`PEP 8`][pep8] and [`isort`][isort]
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
fastapi_ui_auth-0.4.0a0.dist-info/licenses/LICENSE,sha256=_sOIKJWdD2o1WwwDIwYB2qTP2nlSWqT5Tyg9jr1Xa4w,1070
|
|
2
|
+
uiauth/__init__.py,sha256=bkIaE5hK-oDk97RuVSKAfdVcOOsgCq8Q9nKOhiEWcCE,3307
|
|
3
|
+
uiauth/endpoints.py,sha256=MIufjkQP0DOlX99gnJQYA8VJ7iitKLtmeU8wF83AYso,3020
|
|
4
|
+
uiauth/enums.py,sha256=W_9U2luXbscyBEROXqEhKY5DHpJRV1J4VUOpkyUOOzU,334
|
|
5
|
+
uiauth/logger.py,sha256=z67PBMs4zWOfy-Gfm_41dj5Uulm-ChvZxB_jmYKKXeI,391
|
|
6
|
+
uiauth/models.py,sha256=zYHM8wZbbZXm93uDJzJHoviEW3G-8H0B4fTC6dYNV7w,3782
|
|
7
|
+
uiauth/otp.py,sha256=oIucQ8aUVzCq-p2w0efkQa0ZwWAQEFBwhmnwoovDw_k,1759
|
|
8
|
+
uiauth/secure.py,sha256=ZIhiFlkL6P5tcIwnfJ4zaGIIN9gNHq_ddgShKaJ10NM,1267
|
|
9
|
+
uiauth/service.py,sha256=Bz3qgOqkRAsJ5Nf27z0Nfq9FeD7mZdQ__AgzWjNZXzo,8484
|
|
10
|
+
uiauth/utils.py,sha256=1vTdMcEVucofnsdKuGkaMZwtckPhOi_D0K0rx7pSWqk,7588
|
|
11
|
+
uiauth/version.py,sha256=vd8LWnkBPrvy6yVQjDXBrRaBpNpQeCNI9yGkt3_yHls,20
|
|
12
|
+
uiauth/templates/index.html,sha256=AMqKDELuORy84iglbglU0Aey42Ml64jOb3hgYcCyYRw,9626
|
|
13
|
+
uiauth/templates/logout.html,sha256=JrWBJCbK1E4NfrNipMsLzfJ_-Fs2C6D4S0B6O7JNoek,3504
|
|
14
|
+
uiauth/templates/session.html,sha256=EL4gajOED3IcOnrALMiJ2SzJl2at8GFfruTuExhgOVI,3040
|
|
15
|
+
uiauth/templates/unauthorized.html,sha256=ahv78zLM04_Lu83LdX0Ua_toKeP5JZkYsTCWCrfCvHA,3002
|
|
16
|
+
fastapi_ui_auth-0.4.0a0.dist-info/METADATA,sha256=CXcmjbzFq5ofQ0FmZ_rTO-luOem4LayZVK2l_cj0AH4,4561
|
|
17
|
+
fastapi_ui_auth-0.4.0a0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
18
|
+
fastapi_ui_auth-0.4.0a0.dist-info/entry_points.txt,sha256=UtbIj-rQknrrs0A6kMPrhkXHBadmPcAeVwm0iVhWKXg,72
|
|
19
|
+
fastapi_ui_auth-0.4.0a0.dist-info/top_level.txt,sha256=ra3nGTbDTgQ7eChlkngJ7xGXhSCeFTWMvb_b6q8uPVA,7
|
|
20
|
+
fastapi_ui_auth-0.4.0a0.dist-info/RECORD,,
|
uiauth/__init__.py
CHANGED
|
@@ -1,5 +1,92 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
|
|
1
3
|
from uiauth.enums import APIEndpoints # noqa: F401,E402
|
|
4
|
+
from uiauth.models import Fallback # noqa: F401,E402
|
|
5
|
+
from uiauth.otp import OTPConfig, generate_qr
|
|
2
6
|
from uiauth.service import FastAPIUIAuth as _authProduct # noqa: F401,E402
|
|
3
7
|
from uiauth.version import version # noqa: F401,E402
|
|
4
8
|
|
|
5
9
|
protect = _authProduct
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _totp() -> None:
|
|
13
|
+
"""Commandline trigger for TOTP QR code generation."""
|
|
14
|
+
assert sys.argv[0].endswith("totp"), "Invalid commandline trigger!!"
|
|
15
|
+
options = {
|
|
16
|
+
"--user | -U": "Username for the TOTP setup (e.g., your email or username)",
|
|
17
|
+
"--app | -A": "Issuer name for the TOTP setup (e.g., your app or company name)",
|
|
18
|
+
"--file | -F": "Filename for the generated QR code image (default: 'otp_qr.png')",
|
|
19
|
+
"--show | -S": "Show the generated QR code after creation (default: False)",
|
|
20
|
+
"--help | -H": "Prints the help section.",
|
|
21
|
+
}
|
|
22
|
+
# weird way to increase spacing to keep all values monotonic
|
|
23
|
+
_longest_key = max(len(k) for k in options)
|
|
24
|
+
_pretext = "\n\t* "
|
|
25
|
+
choices = _pretext + _pretext.join(
|
|
26
|
+
f"{k} {'·' * (_longest_key - len(k) + 8)}→ {v}".expandtabs()
|
|
27
|
+
for k, v in options.items()
|
|
28
|
+
)
|
|
29
|
+
args = [arg for arg in sys.argv[1:]]
|
|
30
|
+
if any(a.lower() in ("help", "--help", "-h") for a in args):
|
|
31
|
+
print(
|
|
32
|
+
f"Usage: uiauth-totp [arbitrary-command]\nOptions (and corresponding behavior):{choices}"
|
|
33
|
+
)
|
|
34
|
+
exit(0)
|
|
35
|
+
|
|
36
|
+
app = None
|
|
37
|
+
user = None
|
|
38
|
+
show_qr = False
|
|
39
|
+
filename = "otp_qr.png"
|
|
40
|
+
|
|
41
|
+
for idx, arg in enumerate(args):
|
|
42
|
+
if (
|
|
43
|
+
arg.startswith("--file=")
|
|
44
|
+
or arg.startswith("-F=")
|
|
45
|
+
or (arg in ("--file", "-F") and idx + 1 < len(args))
|
|
46
|
+
):
|
|
47
|
+
filename = arg.split("=", 1)[1] if "=" in arg else args[idx + 1]
|
|
48
|
+
elif (
|
|
49
|
+
arg.startswith("--user=")
|
|
50
|
+
or arg.startswith("-U=")
|
|
51
|
+
or (arg in ("--user", "-U") and idx + 1 < len(args))
|
|
52
|
+
):
|
|
53
|
+
user = arg.split("=", 1)[1] if "=" in arg else args[idx + 1]
|
|
54
|
+
elif (
|
|
55
|
+
arg.startswith("--app=")
|
|
56
|
+
or arg.startswith("-A=")
|
|
57
|
+
or (arg in ("--app", "-A") and idx + 1 < len(args))
|
|
58
|
+
):
|
|
59
|
+
app = arg.split("=", 1)[1] if "=" in arg else args[idx + 1]
|
|
60
|
+
elif (
|
|
61
|
+
arg in ("--show", "-S")
|
|
62
|
+
or arg.startswith("--show=")
|
|
63
|
+
or arg.startswith("-S=")
|
|
64
|
+
):
|
|
65
|
+
if "=" in arg:
|
|
66
|
+
show_qr = arg.split("=", 1)[1].lower() in ("true", "1", "yes")
|
|
67
|
+
else:
|
|
68
|
+
show_qr = (
|
|
69
|
+
args[idx + 1].lower() in ("true", "1", "yes")
|
|
70
|
+
if idx + 1 < len(args)
|
|
71
|
+
else True
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
if not all((app, user)):
|
|
75
|
+
print(
|
|
76
|
+
"Missing required options. Using default values for missing options:\n"
|
|
77
|
+
f"Please choose from {choices}"
|
|
78
|
+
)
|
|
79
|
+
exit(1)
|
|
80
|
+
|
|
81
|
+
config = OTPConfig(
|
|
82
|
+
qr_filename=filename, authenticator_user=user, authenticator_app=app
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
print(
|
|
86
|
+
"Using values:\n"
|
|
87
|
+
f" - Filename: {config.qr_filename}\n"
|
|
88
|
+
f" - User: {config.authenticator_user}\n"
|
|
89
|
+
f" - App: {config.authenticator_app}\n"
|
|
90
|
+
f" - Show QR: {show_qr}\n"
|
|
91
|
+
)
|
|
92
|
+
generate_qr(show_qr=show_qr, config=config)
|
uiauth/endpoints.py
CHANGED
uiauth/models.py
CHANGED
|
@@ -1,13 +1,24 @@
|
|
|
1
1
|
import os
|
|
2
2
|
import pathlib
|
|
3
|
-
from
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from typing import Dict, Iterable, NoReturn, Optional
|
|
4
5
|
|
|
6
|
+
import pyotp
|
|
5
7
|
from fastapi.templating import Jinja2Templates
|
|
6
8
|
from pydantic import BaseModel, Field
|
|
7
9
|
|
|
8
10
|
templates = Jinja2Templates(directory=pathlib.Path(__file__).parent / "templates")
|
|
9
11
|
|
|
10
12
|
|
|
13
|
+
def validate_totp_secret(token) -> None | NoReturn:
|
|
14
|
+
"""Validate the provided TOTP secret token."""
|
|
15
|
+
totp = pyotp.TOTP(token)
|
|
16
|
+
# Sampler can also be generated with totp.now()
|
|
17
|
+
now = datetime.now()
|
|
18
|
+
sampler = totp.generate_otp(totp.timecode(now))
|
|
19
|
+
assert totp.verify(sampler, for_time=now), "Invalid authenticatorToken!"
|
|
20
|
+
|
|
21
|
+
|
|
11
22
|
class EnvConfig(BaseModel):
|
|
12
23
|
"""Model for environment configuration.
|
|
13
24
|
|
|
@@ -17,6 +28,7 @@ class EnvConfig(BaseModel):
|
|
|
17
28
|
|
|
18
29
|
username: str
|
|
19
30
|
password: str
|
|
31
|
+
totp_token: str | None = None
|
|
20
32
|
|
|
21
33
|
|
|
22
34
|
def get_cred(keys: Iterable[str], kwargs: Dict[str, str]) -> str | None:
|
|
@@ -31,7 +43,7 @@ def get_cred(keys: Iterable[str], kwargs: Dict[str, str]) -> str | None:
|
|
|
31
43
|
The first found credential value or None if not found.
|
|
32
44
|
"""
|
|
33
45
|
for key in keys:
|
|
34
|
-
if value := kwargs.get(key
|
|
46
|
+
if value := kwargs.get(key, os.getenv(key)):
|
|
35
47
|
return value
|
|
36
48
|
return None
|
|
37
49
|
|
|
@@ -49,7 +61,9 @@ def env_loader(**kwargs) -> EnvConfig:
|
|
|
49
61
|
"""
|
|
50
62
|
username = get_cred(["username", "USERNAME", "user", "USER"], kwargs)
|
|
51
63
|
password = get_cred(["password", "PASSWORD", "pass", "PASS"], kwargs)
|
|
52
|
-
|
|
64
|
+
if totp_token := get_cred(["totp_token", "TOTP_TOKEN", "totp", "TOTP"], kwargs):
|
|
65
|
+
validate_totp_secret(totp_token)
|
|
66
|
+
return EnvConfig(username=username, password=password, totp_token=totp_token)
|
|
53
67
|
|
|
54
68
|
|
|
55
69
|
env = EnvConfig
|
|
@@ -107,4 +121,4 @@ class RedirectException(Exception):
|
|
|
107
121
|
|
|
108
122
|
|
|
109
123
|
ws_session = WSSession()
|
|
110
|
-
fallback = Fallback
|
|
124
|
+
fallback = Fallback
|
uiauth/otp.py
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
|
|
4
|
+
import pyotp
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dataclass
|
|
8
|
+
class OTPConfig:
|
|
9
|
+
"""Data class to hold OTP configuration.
|
|
10
|
+
|
|
11
|
+
>>> OTPConfig
|
|
12
|
+
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
qr_filename: str
|
|
16
|
+
authenticator_user: str
|
|
17
|
+
authenticator_app: str
|
|
18
|
+
secret: str = ""
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def display_secret(config: OTPConfig) -> None:
|
|
22
|
+
"""Displays the TOTP secret key."""
|
|
23
|
+
try:
|
|
24
|
+
term_size = os.get_terminal_size().columns
|
|
25
|
+
except OSError:
|
|
26
|
+
term_size = 120
|
|
27
|
+
base = "*" * term_size
|
|
28
|
+
print(
|
|
29
|
+
f"\n{base}\n"
|
|
30
|
+
f"\nYour TOTP secret key is: {config.secret}"
|
|
31
|
+
f"\nQR code saved as {config.qr_filename!r} (you can scan this with your authenticator app).\n"
|
|
32
|
+
f"\n{base}",
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def generate_qr(show_qr: bool, config: OTPConfig) -> None:
|
|
37
|
+
"""Generates a QR code for TOTP setup.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
- show_qr: If True, displays the QR code using the default image viewer.
|
|
41
|
+
"""
|
|
42
|
+
# STEP 1: Generate a new secret key for the user (store this securely!)
|
|
43
|
+
secret = pyotp.random_base32()
|
|
44
|
+
|
|
45
|
+
# STEP 2: Create a provisioning URI (for the QR code)
|
|
46
|
+
uri = pyotp.TOTP(secret).provisioning_uri(
|
|
47
|
+
name=str(config.authenticator_user), issuer_name=config.authenticator_app
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
# STEP 3: Generate a QR code (scan this with your authenticator app)
|
|
51
|
+
try:
|
|
52
|
+
import qrcode
|
|
53
|
+
except (ModuleNotFoundError, ImportError):
|
|
54
|
+
print(
|
|
55
|
+
"\nThe 'qrcode' library is required for OTP generation. "
|
|
56
|
+
"Please install it using 'pip install qrcode[pil]'.\n"
|
|
57
|
+
)
|
|
58
|
+
return
|
|
59
|
+
|
|
60
|
+
qr = qrcode.make(uri)
|
|
61
|
+
if show_qr:
|
|
62
|
+
qr.show()
|
|
63
|
+
|
|
64
|
+
# Save the QR code
|
|
65
|
+
qr.save(config.qr_filename)
|
|
66
|
+
|
|
67
|
+
# STEP 4: Update the config with the new secret
|
|
68
|
+
config.secret = secret
|
|
69
|
+
display_secret(config)
|
uiauth/secure.py
CHANGED
|
@@ -4,6 +4,8 @@ import hashlib
|
|
|
4
4
|
import string
|
|
5
5
|
from typing import Any
|
|
6
6
|
|
|
7
|
+
import pyotp
|
|
8
|
+
|
|
7
9
|
UNICODE_PREFIX = (
|
|
8
10
|
base64.b64decode(b"XA==").decode(encoding="ascii")
|
|
9
11
|
+ string.ascii_letters[20]
|
|
@@ -39,3 +41,9 @@ def hex_encode(value: str):
|
|
|
39
41
|
.decode(encoding="utf-8")
|
|
40
42
|
.split(sep="-")
|
|
41
43
|
)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def verify_totp(token: str, otp: str) -> bool:
|
|
47
|
+
"""Verify the provided OTP against the TOTP token."""
|
|
48
|
+
totp = pyotp.TOTP(token)
|
|
49
|
+
return totp.verify(otp)
|
uiauth/service.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import inspect
|
|
2
2
|
import logging
|
|
3
3
|
import time
|
|
4
|
+
import warnings
|
|
4
5
|
from typing import Dict, List
|
|
5
6
|
|
|
6
7
|
from fastapi import Depends, FastAPI, HTTPException, Request, Response, status
|
|
@@ -23,12 +24,12 @@ class FastAPIUIAuth:
|
|
|
23
24
|
def __init__(
|
|
24
25
|
self,
|
|
25
26
|
app: FastAPI,
|
|
26
|
-
routes: APIRoute | APIWebSocketRoute | List[APIRoute
|
|
27
|
-
timeout: int = 300,
|
|
27
|
+
routes: APIRoute | APIWebSocketRoute | List[APIRoute | APIWebSocketRoute],
|
|
28
28
|
username: str = None,
|
|
29
29
|
password: str = None,
|
|
30
|
-
|
|
31
|
-
|
|
30
|
+
totp_token: str = None,
|
|
31
|
+
session_timeout: int = 300,
|
|
32
|
+
fallback: models.Fallback = None,
|
|
32
33
|
custom_logger: logging.Logger = None,
|
|
33
34
|
):
|
|
34
35
|
"""Initialize the APIAuthenticator with the FastAPI app and secure function.
|
|
@@ -36,17 +37,22 @@ class FastAPIUIAuth:
|
|
|
36
37
|
Args:
|
|
37
38
|
app: FastAPI application instance to which the authenticator will be added.
|
|
38
39
|
routes: APIRoute or APIWebSocketRoute instance(s) representing the routes to be protected by authentication.
|
|
39
|
-
timeout: Session timeout in seconds, default is 300 seconds (5 minutes).
|
|
40
40
|
username: Username for authentication, can be set via environment variable 'USERNAME'.
|
|
41
41
|
password: Password for authentication, can be set via environment variable 'PASSWORD'.
|
|
42
|
-
|
|
43
|
-
|
|
42
|
+
totp_token: TOTP token for 2FA, can be set via environment variable 'TOTP_TOKEN'.
|
|
43
|
+
session_timeout: Session timeout in seconds, default is 300 seconds (5 minutes).
|
|
44
|
+
fallback: Fallback configuration for redirection, that takes a 'button' and 'path' as arguments.
|
|
44
45
|
custom_logger: Custom logger instance, defaults to the custom logger.
|
|
45
46
|
"""
|
|
47
|
+
# TODO:
|
|
48
|
+
# 1. Add a reset option in the UI to refresh page after cookie expires or session becomes invalid
|
|
49
|
+
# 2. Add support for multiple MFA methods (email, Telegram, etc.) and allow create models for it
|
|
46
50
|
assert (
|
|
47
|
-
isinstance(
|
|
48
|
-
), "Timeout must be an integer
|
|
49
|
-
models.env = models.env_loader(
|
|
51
|
+
isinstance(session_timeout, int) and 29 < session_timeout < 86_401
|
|
52
|
+
), "Timeout must be an integer between 30 seconds and 24 hours (86_400 seconds)"
|
|
53
|
+
models.env = models.env_loader(
|
|
54
|
+
username=username, password=password, totp_token=totp_token
|
|
55
|
+
)
|
|
50
56
|
assert (
|
|
51
57
|
models.env.username and models.env.password
|
|
52
58
|
), "Username and password must be provided either as arguments or environment variables"
|
|
@@ -68,9 +74,11 @@ class FastAPIUIAuth:
|
|
|
68
74
|
"Routes must be an instance of APIRoute or APIWebSocketRoute or a list of them"
|
|
69
75
|
)
|
|
70
76
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
77
|
+
if fallback:
|
|
78
|
+
assert fallback.path.startswith("/"), "Fallback path must start with '/'"
|
|
79
|
+
models.fallback = fallback
|
|
80
|
+
else:
|
|
81
|
+
models.fallback = models.Fallback()
|
|
74
82
|
|
|
75
83
|
# noinspection PyTypeChecker
|
|
76
84
|
self.app.add_exception_handler(
|
|
@@ -83,10 +91,14 @@ class FastAPIUIAuth:
|
|
|
83
91
|
custom_logger, logging.Logger
|
|
84
92
|
), "Custom logger must be an instance of logging.Logger"
|
|
85
93
|
logger.CUSTOM_LOGGER = custom_logger
|
|
86
|
-
self.
|
|
94
|
+
self.session_timeout = session_timeout
|
|
87
95
|
|
|
88
96
|
self._secure()
|
|
89
97
|
logger.CUSTOM_LOGGER.debug("Endpoints registered: %s", len(self.routes))
|
|
98
|
+
if not models.env.totp_token:
|
|
99
|
+
warning = "No TOTP token provided, skipping 2FA. This is not recommended for production use."
|
|
100
|
+
logger.CUSTOM_LOGGER.warning(warning)
|
|
101
|
+
warnings.warn(warning, UserWarning)
|
|
90
102
|
|
|
91
103
|
def _verify_auth(
|
|
92
104
|
self,
|
|
@@ -111,7 +123,7 @@ class FastAPIUIAuth:
|
|
|
111
123
|
)
|
|
112
124
|
if destination := request.cookies.get("X-Requested-By"):
|
|
113
125
|
logger.CUSTOM_LOGGER.info(
|
|
114
|
-
"Setting session timeout for %s seconds", self.
|
|
126
|
+
"Setting session timeout for %s seconds", self.session_timeout
|
|
115
127
|
)
|
|
116
128
|
# Set session_token cookie with a timeout, to be used for session validation when redirected
|
|
117
129
|
response.set_cookie(
|
|
@@ -119,11 +131,11 @@ class FastAPIUIAuth:
|
|
|
119
131
|
value=session_token,
|
|
120
132
|
httponly=True,
|
|
121
133
|
samesite="strict",
|
|
122
|
-
max_age=self.
|
|
134
|
+
max_age=self.session_timeout,
|
|
123
135
|
)
|
|
124
136
|
models.ws_session.client_auth[request.client.host] = {
|
|
125
137
|
"token": session_token,
|
|
126
|
-
"expires_at": time.time() + self.
|
|
138
|
+
"expires_at": time.time() + self.session_timeout,
|
|
127
139
|
}
|
|
128
140
|
response.delete_cookie(key="X-Requested-By")
|
|
129
141
|
return {"redirect_url": destination}
|
uiauth/templates/index.html
CHANGED
|
@@ -149,6 +149,10 @@
|
|
|
149
149
|
<input type="password" id="password" name="password" required>
|
|
150
150
|
<i class="fa-regular fa-eye" id="eye"></i>
|
|
151
151
|
</div>
|
|
152
|
+
{% if totp_enabled %}
|
|
153
|
+
<label for="otp">TOTP:</label>
|
|
154
|
+
<input type="text" id="otp" name="otp" required>
|
|
155
|
+
{% endif %}
|
|
152
156
|
<button type="submit" onclick="submitToAPI(event)">Sign In</button>
|
|
153
157
|
</form>
|
|
154
158
|
</div>
|
|
@@ -203,7 +207,16 @@
|
|
|
203
207
|
let hex_pass = await ConvertStringToHex(password);
|
|
204
208
|
let timestamp = Math.round(new Date().getTime() / 1000);
|
|
205
209
|
let hash = await CalculateHash(hex_user, hex_pass, timestamp)
|
|
206
|
-
|
|
210
|
+
{% if totp_enabled %}
|
|
211
|
+
const otp = $("#otp").val();
|
|
212
|
+
if (otp === "") {
|
|
213
|
+
alert("ERROR: TOTP is enabled, please provide the TOTP value to authenticate your request!");
|
|
214
|
+
return false;
|
|
215
|
+
}
|
|
216
|
+
let authHeaderValue = hex_user + ',' + hash + ',' + otp + ',' + timestamp;
|
|
217
|
+
{% else %}
|
|
218
|
+
let authHeaderValue = hex_user + ',' + hash + ',' + timestamp;
|
|
219
|
+
{% endif %}
|
|
207
220
|
let origin = window.location.origin
|
|
208
221
|
$.ajax({
|
|
209
222
|
method: "POST",
|
uiauth/utils.py
CHANGED
|
@@ -61,12 +61,12 @@ def raise_error(request: Request) -> NoReturn:
|
|
|
61
61
|
"""
|
|
62
62
|
failed_auth_counter(request)
|
|
63
63
|
logger.CUSTOM_LOGGER.error(
|
|
64
|
-
"
|
|
64
|
+
"Invalid credentials: %d",
|
|
65
65
|
models.ws_session.invalid[request.client.host],
|
|
66
66
|
)
|
|
67
67
|
raise HTTPException(
|
|
68
68
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
69
|
-
detail="
|
|
69
|
+
detail="Invalid credentials",
|
|
70
70
|
headers=None,
|
|
71
71
|
)
|
|
72
72
|
|
|
@@ -97,8 +97,13 @@ def verify_login(
|
|
|
97
97
|
str:
|
|
98
98
|
Returns the session token.
|
|
99
99
|
"""
|
|
100
|
+
otp = None
|
|
100
101
|
if authorization:
|
|
101
|
-
|
|
102
|
+
# Raises ValueError if the credentials are not in the expected format
|
|
103
|
+
if models.env.totp_token:
|
|
104
|
+
username, signature, otp, timestamp = extract_credentials(authorization)
|
|
105
|
+
else:
|
|
106
|
+
username, signature, timestamp = extract_credentials(authorization)
|
|
102
107
|
else:
|
|
103
108
|
raise_error(request)
|
|
104
109
|
if secrets.compare_digest(username, models.env.username):
|
|
@@ -110,6 +115,13 @@ def verify_login(
|
|
|
110
115
|
message = f"{hex_user}{hex_pass}{timestamp}"
|
|
111
116
|
expected_signature = secure.calculate_hash(message)
|
|
112
117
|
if secrets.compare_digest(signature, expected_signature):
|
|
118
|
+
if models.env.totp_token:
|
|
119
|
+
if not otp:
|
|
120
|
+
logger.CUSTOM_LOGGER.warning("TOTP token is required but not provided")
|
|
121
|
+
raise_error(request)
|
|
122
|
+
if not secure.verify_totp(token=models.env.totp_token, otp=otp):
|
|
123
|
+
logger.CUSTOM_LOGGER.warning("Invalid TOTP token provided")
|
|
124
|
+
raise_error(request)
|
|
113
125
|
models.ws_session.invalid[request.client.host] = 0
|
|
114
126
|
key = secrets.token_urlsafe(64)
|
|
115
127
|
return key
|
uiauth/version.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
version = "0.
|
|
1
|
+
version = "0.4.0a0"
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
fastapi_ui_auth-0.3.2.dist-info/licenses/LICENSE,sha256=_sOIKJWdD2o1WwwDIwYB2qTP2nlSWqT5Tyg9jr1Xa4w,1070
|
|
2
|
-
uiauth/__init__.py,sha256=hbHN-Vv4xTxDqpQW2lmgdl-OlEkAtL6JXAGL-nucaOU,211
|
|
3
|
-
uiauth/endpoints.py,sha256=nacWM8IJWnReweMwsDOEffyPKXll_qtXnGTSA4NJysY,2953
|
|
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=zjuPzQ2OlxNtrLXb5A3OlPsaRb_b_Ln51AWN6r9VRho,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.2.dist-info/METADATA,sha256=Yx80vcs80ZJtOEmn3uJcJ3MaIUXJ1p7KZHTOkGt2bIg,3662
|
|
16
|
-
fastapi_ui_auth-0.3.2.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
17
|
-
fastapi_ui_auth-0.3.2.dist-info/top_level.txt,sha256=ra3nGTbDTgQ7eChlkngJ7xGXhSCeFTWMvb_b6q8uPVA,7
|
|
18
|
-
fastapi_ui_auth-0.3.2.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|