FastAPI-UI-Auth 0.3.1__py3-none-any.whl → 0.4.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,21 +1,27 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: FastAPI-UI-Auth
3
- Version: 0.3.1
3
+ Version: 0.4.0
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.34.*; extra == "dev"
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: httpx; extra == "test"
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.0.dist-info/licenses/LICENSE,sha256=_sOIKJWdD2o1WwwDIwYB2qTP2nlSWqT5Tyg9jr1Xa4w,1070
2
+ uiauth/__init__.py,sha256=O81sfpkVE91Kg0rWUzeKO7Uh_NUnqYkMC8SHicbOcdw,3331
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=yhiWOz0HoJGRRI9-JQ2eh_0AbByy-6psK08-kpTSHJw,18
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.0.dist-info/METADATA,sha256=d13AaIFPFyZDPdRtYsTZ0ndN0cmeN6vUEYRvGEWb1VY,4559
17
+ fastapi_ui_auth-0.4.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
18
+ fastapi_ui_auth-0.4.0.dist-info/entry_points.txt,sha256=UtbIj-rQknrrs0A6kMPrhkXHBadmPcAeVwm0iVhWKXg,72
19
+ fastapi_ui_auth-0.4.0.dist-info/top_level.txt,sha256=ra3nGTbDTgQ7eChlkngJ7xGXhSCeFTWMvb_b6q8uPVA,7
20
+ fastapi_ui_auth-0.4.0.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (82.0.0)
2
+ Generator: setuptools (82.0.1)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -0,0 +1,3 @@
1
+ [console_scripts]
2
+ uiauth-totp = uiauth:_totp
3
+ uiauth_totp = uiauth:_totp
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
+ raise SystemExit(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
+ raise SystemExit(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
@@ -19,6 +19,7 @@ def session(request: Request) -> HTMLResponse:
19
19
  return utils.deauthorize(
20
20
  models.templates.TemplateResponse(
21
21
  name="session.html",
22
+ request=request,
22
23
  context={
23
24
  "request": request,
24
25
  "signin": enums.APIEndpoints.fastapi_login,
@@ -41,10 +42,12 @@ def login(request: Request) -> HTMLResponse:
41
42
  return utils.deauthorize(
42
43
  models.templates.TemplateResponse(
43
44
  name="index.html",
45
+ request=request,
44
46
  context={
45
47
  "request": request,
46
48
  "signin": enums.APIEndpoints.fastapi_verify_login,
47
49
  "version": f"v{version}",
50
+ "totp_enabled": models.env.totp_token is not None,
48
51
  },
49
52
  ),
50
53
  )
@@ -65,6 +68,7 @@ def logout(request: Request) -> HTMLResponse:
65
68
  return utils.deauthorize(
66
69
  models.templates.TemplateResponse(
67
70
  name="logout.html",
71
+ request=request,
68
72
  context={
69
73
  "request": request,
70
74
  "detail": "You have been successfully logged out.",
@@ -87,6 +91,7 @@ def error(request: Request) -> HTMLResponse:
87
91
  return utils.deauthorize(
88
92
  models.templates.TemplateResponse(
89
93
  name="unauthorized.html",
94
+ request=request,
90
95
  context={
91
96
  "request": request,
92
97
  "signin": enums.APIEndpoints.fastapi_login,
uiauth/models.py CHANGED
@@ -1,13 +1,24 @@
1
1
  import os
2
2
  import pathlib
3
- from typing import Dict, Iterable, Optional
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) or os.getenv(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
- return EnvConfig(username=username, password=password)
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] | List[APIWebSocketRoute],
27
- timeout: int = 300,
27
+ routes: APIRoute | APIWebSocketRoute | List[APIRoute | APIWebSocketRoute],
28
28
  username: str = None,
29
29
  password: str = None,
30
- fallback_button: str = models.fallback.button,
31
- fallback_path: str = models.fallback.path,
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
- fallback_button: Title for the fallback button, defaults to "LOGIN".
43
- fallback_path: Fallback path to redirect to in case of session timeout or invalid session.
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(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)
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
- assert fallback_path.startswith("/"), "Fallback path must start with '/'"
72
- models.fallback.path = fallback_path
73
- models.fallback.button = fallback_button
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.timeout = timeout
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.timeout
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.timeout,
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.timeout,
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}
@@ -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
- let authHeaderValue = hex_user + ',' + hash + ',' + timestamp;
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
- "Incorrect username or password: %d",
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="Incorrect username or password",
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
- username, signature, timestamp = extract_credentials(authorization)
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.3.1"
1
+ version = "0.4.0"
@@ -1,18 +0,0 @@
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,,