Skip to content

feat(auth): implement comprehensive JWT and Basic authentication#23

Merged
iklobato merged 3 commits intomasterfrom
feat/improve-authentication-jwt-and-basic
Mar 10, 2026
Merged

feat(auth): implement comprehensive JWT and Basic authentication#23
iklobato merged 3 commits intomasterfrom
feat/improve-authentication-jwt-and-basic

Conversation

@iklobato
Copy link
Copy Markdown
Owner

@iklobato iklobato commented Mar 10, 2026

  • Add JWT algorithm configuration with validation (HS256, HS384, HS512, RS256, RS384, RS512, ES256, ES384, ES512)
  • Implement BasicAuthentication class with proper 401 error responses
  • Add rate limiting for auth endpoints (10/min, 100/hour, 1000/day default)
  • Fix JWT expiration configuration not being passed to authentication instances
  • Add JWT claim validation to prevent overwriting reserved 'exp' claim
  • Consolidate Basic auth parsing in _login.py module
  • Add comprehensive test suite covering all auth scenarios
  • Update YAML configuration support for JWT algorithm and expiration
  • Fix typing issues in auth modules
  • Add proper error response differentiation (401 for auth failures, 403 for permissions)

BREAKING CHANGE: JWTAuthentication constructor now accepts optional parameters:

  • expiration: Override default 3600s token lifetime
  • algorithm: Override default HS256 algorithm
  • secret_key: Custom secret (defaults to LIGHTAPI_JWT_SECRET)

Security enhancements:

  • Rate limiting prevents brute force attacks on login endpoints
  • Basic auth returns proper WWW-Authenticate header semantics
  • JWT algorithm validation prevents insecure configurations

Summary by CodeRabbit

  • New Features

    • Automatic /auth/login and /auth/token endpoints for JWT and Basic auth; LightApi accepts a login_validator callable and customizable auth path
    • BasicAuthentication option and configurable JWT expiration, algorithm, and extra claims
    • In-memory rate limiter for auth endpoints
  • Documentation

    • Added README and YAML examples describing auth endpoints, validator callable, and JWT settings
  • Tests

    • Comprehensive tests covering login/token flows, JWT/Basic scenarios, error cases, and YAML config handling

- Add JWT algorithm configuration with validation (HS256, HS384, HS512, RS256, RS384, RS512, ES256, ES384, ES512)
- Implement BasicAuthentication class with proper 401 error responses
- Add rate limiting for auth endpoints (10/min, 100/hour, 1000/day default)
- Fix JWT expiration configuration not being passed to authentication instances
- Add JWT claim validation to prevent overwriting reserved 'exp' claim
- Consolidate Basic auth parsing in _login.py module
- Add comprehensive test suite covering all auth scenarios
- Update YAML configuration support for JWT algorithm and expiration
- Fix typing issues in auth modules
- Add proper error response differentiation (401 for auth failures, 403 for permissions)

BREAKING CHANGE: JWTAuthentication constructor now accepts optional parameters:
  - expiration: Override default 3600s token lifetime
  - algorithm: Override default HS256 algorithm
  - secret_key: Custom secret (defaults to LIGHTAPI_JWT_SECRET)

Security enhancements:
  - Rate limiting prevents brute force attacks on login endpoints
  - Basic auth returns proper WWW-Authenticate header semantics
  - JWT algorithm validation prevents insecure configurations
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 10, 2026

📝 Walkthrough

Walkthrough

Adds login/token endpoints, BasicAuthentication, login_validator registration, JWT configuration (expiration/extra claims/algorithm), per‑IP auth rate limiting, YAML config support for auth settings and callable resolution, state-backed registry/cache, and automatic /auth route registration when relevant backends are present. (33 words)

Changes

Cohort / File(s) Summary
Documentation
README.md, docs/examples/yaml-configuration.md
Documents /auth/login and /auth/token flows, login_validator hook, auth_path, and new YAML keys: jwt_expiration, jwt_extra_claims, jwt_algorithm.
Auth logic
lightapi/auth.py
Introduces BasicAuthentication, standardizes 401 error response, updates BaseAuthentication signature, and moves auth error helper; authentication now consults registry-provided validator.
Login endpoints
lightapi/_login.py
New login/token handler and LoginRequest model: POST-only credential parsing (Basic header or JSON), validator invocation, optional JWT creation, and rate-limit decorator integration.
Registry & state
lightapi/_registry.py, lightapi/cache.py
Replaces global singletons with a state container; adds LoginValidator type, set_login_validator/get_login_validator; Redis client stored in state.
Configuration
lightapi/config.py, lightapi/yaml_loader.py
Adds JWT algorithm whitelist and config property; extends Authentication with jwt_expiration, jwt_extra_claims, jwt_algorithm; YAML loader resolves dotted login_validator, adds AuthLoginConfig, and wires auth settings into load_config and endpoint metadata.
Framework wiring
lightapi/lightapi.py
LightApi accepts login_validator and auth_path; detects JWT/Basic backends, enforces validator, constructs auth endpoints, threads JWT/basic options into authenticators, and auto-registers /auth/login & /auth/token.
Rate limiting
lightapi/rate_limiter.py
New in-memory RateLimiter with per-IP minute/hour/day windows, cleanup, and standardized 429 responses; used for auth endpoints.
Package exports
lightapi/__init__.py
Adds BasicAuthentication to public exports.
Tests
tests/test_auth.py, tests/test_login_auth.py, tests/test_yaml_config.py
Updates tests to provide login_validator; adds extensive login/token tests (JWT & Basic) and YAML auth config tests including dotted-path validator resolution and JWT defaults.

Sequence Diagrams

sequenceDiagram
    participant Client
    participant LightApi as LightApi Server
    participant LoginHandler as Login Handler
    participant Validator as Login Validator
    participant JWT as JWT Generator

    Client->>LightApi: POST /auth/login (Basic header or JSON body)
    LightApi->>LoginHandler: route -> login_handler
    LoginHandler->>LoginHandler: extract credentials (header or body)
    LoginHandler->>Validator: login_validator(username, password)
    alt credentials valid
        Validator-->>LoginHandler: user dict
        alt JWT enabled
            LoginHandler->>JWT: generate_token(user, extra_claims, exp, alg)
            JWT-->>LoginHandler: token string
            LoginHandler-->>Client: 200 {token, user}
        else Basic-only
            LoginHandler-->>Client: 200 {user}
        end
    else invalid
        LoginHandler-->>Client: 401 {error: "authentication failed"}
    end
Loading

Estimated Code Review Effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Suggested Labels

codex

Poem

🐰 I hopped to the auth gate with a key,
Headers, JSON, and claims for me,
A validator sniffed, tokens tucked neat,
Rate limits patrolled the login street,
YAML mapped the path—what a treat!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 27.21% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: implementing comprehensive JWT and Basic authentication features across the codebase.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/improve-authentication-jwt-and-basic

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 15

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
lightapi/auth.py (1)

20-42: ⚠️ Potential issue | 🟠 Major

Please revert changes inside the frozen auth classes.

This PR edits both BaseAuthentication and JWTAuthentication, but those class bodies are explicitly frozen in this repository. Add the new behavior via new helpers/types rather than changing these two implementations.

As per coding guidelines, "lightapi/auth.py: Do not modify lightapi/auth.py: JWTAuthentication and BaseAuthentication class bodies are FROZEN"

Also applies to: 59-154

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lightapi/auth.py` around lines 20 - 42, Revert any edits to the frozen class
bodies for BaseAuthentication and JWTAuthentication in lightapi/auth.py so their
original implementations of methods like authenticate and
get_auth_error_response are restored; instead move the new behavior into a new
helper (e.g., a separate class or function such as CustomAuthHelper or
jwt_auth_helper) and update callers to use that helper where custom logic is
required, ensuring you do not change the BaseAuthentication or JWTAuthentication
class definitions themselves and that new helpers are defined in a new module or
below the frozen classes in the file.
lightapi/lightapi.py (2)

549-561: ⚠️ Potential issue | 🟠 Major

The auth pipeline still bypasses backend-specific 401 responses.

_check_auth() returns its own generic 401 instead of delegating to authenticator.get_auth_error_response(request), so the new backends never get to emit WWW-Authenticate or scheme-specific bodies.

Possible fix
-        if not authenticator.authenticate(request):
-            return JSONResponse(
-                {"detail": "Authentication credentials invalid."}, status_code=401
-            )
+        if not authenticator.authenticate(request):
+            return authenticator.get_auth_error_response(request)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lightapi/lightapi.py` around lines 549 - 561, The current _check_auth()
always returns a generic 401 JSONResponse when
authenticator.authenticate(request) fails, which prevents backends from
returning scheme-specific responses; change the failure path so that after
creating the authenticator (including the JWTAuthentication instantiation logic)
you check the result of authenticator.authenticate(request) and if it is falsy
call and return authenticator.get_auth_error_response(request) when that method
exists, otherwise fall back to the existing JSONResponse({"detail":
"Authentication credentials invalid."}, status_code=401); keep using backend,
JWTAuthentication and authenticate/get_auth_error_response to locate the
relevant code.

47-81: ⚠️ Potential issue | 🟠 Major

Keep these auth changes out of lightapi/lightapi.py.

This file is explicitly preserved as legacy, but the PR adds new constructor state, route registration, and auth behavior here. Please move this wiring into the non-legacy entrypoint instead of extending lightapi/lightapi.py.

As per coding guidelines, "lightapi/lightapi.py: Do not modify lightapi/lightapi.py; it is preserved as legacy until init.py rewrite absorbs it"

Also applies to: 155-230, 549-556

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lightapi/lightapi.py` around lines 47 - 81, The patch added auth and
route-related state into the legacy LightAPI constructor (symbols:
login_validator parameter, auth_path, self._login_validator, self._auth_path,
and the set_login_validator call) and also touched route/registration areas (see
other edits around lines noted). Revert these changes in lightapi/lightapi.py so
the legacy file remains unchanged, then move the wiring: accept
login_validator/auth_path and call set_login_validator from the non-legacy
entrypoint (the new package entry module), and initialize
route/endpoint/middleware/cors state there instead (use the same symbols:
self._routes, self._endpoint_map, self._middlewares, self._cors_origins). Ensure
the legacy file no longer modifies auth behavior or registers routes and that
all new behavior lives in the modern entrypoint module.
🧹 Nitpick comments (1)
tests/test_login_auth.py (1)

530-548: Pin a single expected outcome for extra whitespace after Basic.

assert resp.status_code in (200, 401) makes this case non-diagnostic: the suite will pass whether the parser starts accepting or rejecting the same header. Once the contract is decided, assert that one behavior directly.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/test_login_auth.py` around lines 530 - 548, The test
test_login_basic_header_multiple_spaces_after_basic is nondeterministic because
it allows either 200 or 401; change it to assert a single expected outcome (pick
one behavior and enforce it). Replace the line "assert resp.status_code in (200,
401)" with a strict assertion, e.g. "assert resp.status_code == 401" (or "==
200" if you decide to accept extra spaces), and remove the conditional block
that checks resp.status_code == 200 (or update it to a direct assertion of the
JSON when expecting 200); update the test docstring to reflect the chosen
contract so test_login_basic_header_multiple_spaces_after_basic documents the
expected behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@docs/examples/yaml-configuration.md`:
- Around line 25-26: The YAML docs omit the jwt_algorithm setting; add a
jwt_algorithm entry alongside jwt_expiration and jwt_extra_claims in the example
(e.g., jwt_algorithm: HS256) and update the schema/documentation table to
include defaults.authentication.jwt_algorithm with its description and allowed
values (e.g., HS256, RS256) and default; this ensures the jwt_algorithm key is
discoverable for YAML users and matches the configuration schema.

In `@lightapi/_login.py`:
- Around line 35-36: The auth-scheme checks currently use startswith("Basic ")
which is case-sensitive; update the checks that inspect auth_header (the one at
lines checking startswith("Basic ") and the similar block at 57-59) to perform a
case-insensitive comparison (e.g., compare auth_header.lower().startswith("basic
") or use a case-insensitive regex like re.match(r"^basic\s+", auth_header,
re.I)) so requests with "basic" or other case variants are correctly recognized
as Basic auth before parsing the body. Ensure you update both occurrences that
reference auth_header to the same case-insensitive approach.
- Around line 77-78: The module-level rate limit on login_handler (decorated
with rate_limit_auth_endpoint) uses a global auth limiter and causes
cross-app/test interference; change the limiter to be app-scoped or
test-configurable by removing or modifying the module-level decorator: either
(a) remove the `@rate_limit_auth_endpoint` from login_handler and apply rate
limiting when registering the route so each FastAPI/ASGI app can attach its own
limiter instance, or (b) change rate_limit_auth_endpoint to obtain the limiter
from the request/app state (e.g., request.app.state.auth_limiter) so tests can
inject/disable a limiter per app; update route registration or app setup to
create and attach a per-app limiter or a no-op limiter for tests.

In `@lightapi/_registry.py`:
- Around line 18-23: The global _state["login_validator"] leaks validators
across LightApi instances; change the design to store the validator per-app and
read it from the app/registry context instead of the singleton. Specifically:
remove usage of the global login_validator entry in _state, have
LightApi.__init__ attach the validator to the instance-specific
registry/metadata object (or keyed by registry id inside _state["registry"]) and
update BasicAuthentication.authenticate to fetch the validator from the incoming
request's associated registry/metadata (or the registry-id keyed entry) rather
than reading _state["login_validator"]; update any helper code that reads/writes
_state to use the per-registry slot so multiple apps/tests don’t overwrite each
other.

In `@lightapi/config.py`:
- Around line 12-22: VALID_JWT_ALGORITHMS is defined as a mutable set which
triggers RUF012 and risks accidental mutation; change it to an immutable
collection (e.g., frozenset or tuple) and annotate it as a constant using
typing.Final to signal immutability. Update the declaration of
VALID_JWT_ALGORITHMS (the existing set literal) to use frozenset({...}) or a
tuple of strings and add "from typing import Final" then declare
VALID_JWT_ALGORITHMS: Final = frozenset(...) so linters and readers treat it as
an immutable constant. Ensure any places that iterate over it still work with
the chosen immutable type (frozenset supports membership and iteration).
- Around line 61-73: The per-endpoint jwt_algorithm value must be validated at
config parse time: in the initializer that sets self.jwt_algorithm (the function
shown), check that the provided jwt_algorithm is either None or a member of the
allowed algorithm list used by JWTAuthentication; if it is invalid, raise
ConfigurationError immediately. Reference Authentication.jwt_algorithm and
JWTAuthentication.__init__ behavior when implementing this check so invalid
YAML/per-endpoint values are rejected during config parsing rather than failing
later at runtime.

In `@lightapi/lightapi.py`:
- Around line 176-205: The register() logic inserts auth routes every time
auth_backends contains JWTAuthentication or BasicAuthentication, causing
duplicates; modify register() (which uses auth_backends, _make_login_endpoint,
_auth_path, _routes, and _endpoint_map) to only add the login/token routes once
by either: (a) introducing a one-time flag (e.g. self._auth_routes_registered)
set after first insertion and skip subsequent inserts, or (b) checking
_endpoint_map or existing entries in self._routes for
f"{self._auth_path.rstrip('/')}/login" and
f"{self._auth_path.rstrip('/')}/token" and replacing/skipping insertion if
found. Ensure the chosen approach prevents duplicate insertions while preserving
current behavior of _make_login_endpoint and route order.
- Around line 76-81: The constructor is registering login_validator globally
which makes it shared across all apps; remove the call to set_login_validator
from LightApi.__init__ (keep self._login_validator and self._auth_path) and
instead make authentication use the per-instance validator: update
BasicAuthentication.authenticate (or the auth middleware) to read the validator
from the app instance (e.g., request.app._login_validator) or accept the
validator when the authentication object/middleware is created so each LightApi
instance uses its own login_validator rather than the global set_login_validator
registry.

In `@lightapi/rate_limiter.py`:
- Around line 46-47: The cleanup routine is passing full redis keys (e.g.,
"auth:hour" / "auth:day") into _get_window_seconds(), which only recognizes bare
window names and thus returns 60 for unknown inputs; update _cleanup_old_entries
(and the other occurrences referenced around lines 64-73 and 112) to extract the
window suffix from the key (e.g., split the key on ":" and use the last segment
like "minute"/"hour"/"day") before calling _get_window_seconds(), so
window_seconds is computed correctly for hourly and daily buckets used by
is_rate_limited().
- Around line 75-85: The _get_client_ip function currently trusts
X-Forwarded-For/X-Real-IP/X-Client-IP headers which can be forged; change it to
default to request.client.host and only parse forwarded headers when the app is
configured to trust proxies (e.g., a trusted_proxies set or a boolean like
trust_proxy on the RateLimiter). Implement logic in _get_client_ip to: 1) obtain
remote_addr = request.client.host (or "0.0.0.0" fallback), 2) if trust_proxy is
enabled AND remote_addr is in trusted_proxies (or matches configured proxy
CIDRs), then parse the forwarded headers and return the first non-empty IP;
otherwise always return remote_addr. Update any constructor/initializer for the
RateLimiter to accept the trust_proxy/trusted_proxies configuration and
reference _get_client_ip accordingly.
- Around line 130-144: get_rate_limit_response always returns the per-minute
limit and a fixed 60s retry even when the hourly or daily quota caused the
block; update get_rate_limit_response to detect which quota/window actually
overflowed (minute/hour/day) using the rate limiter's counters or reset
timestamps (e.g., attributes like requests_per_minute, requests_per_hour,
requests_per_day and corresponding reset times or remaining counters on self)
and return headers matching that window: set Retry-After to the actual seconds
until that window resets, X-RateLimit-Limit to the correct window limit,
X-RateLimit-Remaining to the remaining for that window (usually 0), and
X-RateLimit-Reset to the epoch second when that window will reset; modify
get_rate_limit_response to choose the longest blocking window when multiple
quotas are exceeded.

In `@lightapi/yaml_loader.py`:
- Around line 404-419: The endpoint-level dict auth is currently overriding
defaults so jwt_* fields are lost; fix by building a merged authentication
source before reading jwt values (e.g. use the existing merge/helper used
elsewhere such as _make_authentication or a per-field fallback) and read
backend, jwt_expiration, jwt_extra_claims and jwt_algorithm from that merged
object instead of from meta.authentication alone; update the block around
attrs["authentication"] (referencing meta.authentication,
defaults.authentication, jwt_exp, jwt_claims, jwt_algo and Authentication) to
use the merged auth source so default JWT settings persist when only permissions
are overridden.
- Around line 86-103: The _resolve_callable function currently only checks
callable(), which allows async functions and wrong-arity callables; update
_resolve_callable to (1) reject coroutine functions by using
asyncio.iscoroutinefunction(fn) (or inspect.iscoroutinefunction) and raise
ConfigurationError mentioning the dotted path, and (2) inspect the function
signature (via inspect.signature(fn)) to ensure it accepts exactly two required
positional parameters (username, password) — i.e., count parameters that are
POSITIONAL_ONLY or POSITIONAL_OR_KEYWORD without defaults — and raise
ConfigurationError if the arity is not exactly 2; keep the existing import
resolution and only return fn after these validations so login_handler(username,
password) can safely call it synchronously.

In `@tests/test_auth.py`:
- Around line 62-64: The helper _login_validator declares unused parameters and
lacks a return annotation which trips Ruff ARG001 and mypy; update the signature
of _login_validator to prefix the parameters with underscores (e.g., _username,
_password) to mark them intentionally unused and add an explicit return type ->
None so the function becomes def _login_validator(_username: str, _password:
str) -> None: returning None.

In `@tests/test_yaml_config.py`:
- Around line 33-42: The helper _from_str is always passing a login_validator to
LightApi.from_config (injecting _dummy_login_validator when JWT/Basic appears),
which prevents YAML-specified auth.login_validator from being used; change
_from_str to stop injecting a dummy: detect needs_validator but do not set
login_validator when the caller passed None, and call LightApi.from_config
without the login_validator kwarg unless login_validator was explicitly
provided; apply the same conditional-kwarg pattern to the other occurrence noted
(lines 347-367) so YAML dotted-path validators can be exercised.

---

Outside diff comments:
In `@lightapi/auth.py`:
- Around line 20-42: Revert any edits to the frozen class bodies for
BaseAuthentication and JWTAuthentication in lightapi/auth.py so their original
implementations of methods like authenticate and get_auth_error_response are
restored; instead move the new behavior into a new helper (e.g., a separate
class or function such as CustomAuthHelper or jwt_auth_helper) and update
callers to use that helper where custom logic is required, ensuring you do not
change the BaseAuthentication or JWTAuthentication class definitions themselves
and that new helpers are defined in a new module or below the frozen classes in
the file.

In `@lightapi/lightapi.py`:
- Around line 549-561: The current _check_auth() always returns a generic 401
JSONResponse when authenticator.authenticate(request) fails, which prevents
backends from returning scheme-specific responses; change the failure path so
that after creating the authenticator (including the JWTAuthentication
instantiation logic) you check the result of authenticator.authenticate(request)
and if it is falsy call and return
authenticator.get_auth_error_response(request) when that method exists,
otherwise fall back to the existing JSONResponse({"detail": "Authentication
credentials invalid."}, status_code=401); keep using backend, JWTAuthentication
and authenticate/get_auth_error_response to locate the relevant code.
- Around line 47-81: The patch added auth and route-related state into the
legacy LightAPI constructor (symbols: login_validator parameter, auth_path,
self._login_validator, self._auth_path, and the set_login_validator call) and
also touched route/registration areas (see other edits around lines noted).
Revert these changes in lightapi/lightapi.py so the legacy file remains
unchanged, then move the wiring: accept login_validator/auth_path and call
set_login_validator from the non-legacy entrypoint (the new package entry
module), and initialize route/endpoint/middleware/cors state there instead (use
the same symbols: self._routes, self._endpoint_map, self._middlewares,
self._cors_origins). Ensure the legacy file no longer modifies auth behavior or
registers routes and that all new behavior lives in the modern entrypoint
module.

---

Nitpick comments:
In `@tests/test_login_auth.py`:
- Around line 530-548: The test
test_login_basic_header_multiple_spaces_after_basic is nondeterministic because
it allows either 200 or 401; change it to assert a single expected outcome (pick
one behavior and enforce it). Replace the line "assert resp.status_code in (200,
401)" with a strict assertion, e.g. "assert resp.status_code == 401" (or "==
200" if you decide to accept extra spaces), and remove the conditional block
that checks resp.status_code == 200 (or update it to a direct assertion of the
JSON when expecting 200); update the test docstring to reflect the chosen
contract so test_login_basic_header_multiple_spaces_after_basic documents the
expected behavior.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: bc4f1af1-c0e2-4c29-89f0-24a895db4abf

📥 Commits

Reviewing files that changed from the base of the PR and between b751271 and 91cf264.

⛔ Files ignored due to path filters (1)
  • uv.lock is excluded by !**/*.lock
📒 Files selected for processing (14)
  • README.md
  • docs/examples/yaml-configuration.md
  • lightapi/__init__.py
  • lightapi/_login.py
  • lightapi/_registry.py
  • lightapi/auth.py
  • lightapi/cache.py
  • lightapi/config.py
  • lightapi/lightapi.py
  • lightapi/rate_limiter.py
  • lightapi/yaml_loader.py
  • tests/test_auth.py
  • tests/test_login_auth.py
  • tests/test_yaml_config.py

Comment on lines +18 to +23
_state: dict[str, object | None] = {
"registry": None,
"metadata": None,
"engine": None,
"login_validator": None,
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

A single global login_validator breaks multi-app isolation.

LightApi.__init__ stores the validator in _state, while BasicAuthentication.authenticate() reads it back from the same singleton at request time. If two LightApi instances in one process configure different validators, the later one overwrites the earlier one for both apps; the same leakage can happen between tests.

Also applies to: 51-59

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lightapi/_registry.py` around lines 18 - 23, The global
_state["login_validator"] leaks validators across LightApi instances; change the
design to store the validator per-app and read it from the app/registry context
instead of the singleton. Specifically: remove usage of the global
login_validator entry in _state, have LightApi.__init__ attach the validator to
the instance-specific registry/metadata object (or keyed by registry id inside
_state["registry"]) and update BasicAuthentication.authenticate to fetch the
validator from the incoming request's associated registry/metadata (or the
registry-id keyed entry) rather than reading _state["login_validator"]; update
any helper code that reads/writes _state to use the per-registry slot so
multiple apps/tests don’t overwrite each other.

Comment on lines +130 to +144
def get_rate_limit_response(self, request: Request) -> JSONResponse:
"""Get standard rate limit exceeded response."""
return JSONResponse(
{
"error": "rate_limit_exceeded",
"detail": "Too many requests. Please try again later.",
},
status_code=429,
headers={
"Retry-After": "60", # Retry after 60 seconds
"X-RateLimit-Limit": str(self.requests_per_minute),
"X-RateLimit-Remaining": "0",
"X-RateLimit-Reset": str(int(time.time() + 60)),
},
)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Return 429 headers for the window that actually overflowed.

get_rate_limit_response() always advertises the per-minute limit and a 60-second retry, even when the hourly or daily quota is what blocked the request. That will tell well-behaved clients to retry far too early.

🧰 Tools
🪛 Ruff (0.15.5)

[warning] 130-130: Unused method argument: request

(ARG002)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lightapi/rate_limiter.py` around lines 130 - 144, get_rate_limit_response
always returns the per-minute limit and a fixed 60s retry even when the hourly
or daily quota caused the block; update get_rate_limit_response to detect which
quota/window actually overflowed (minute/hour/day) using the rate limiter's
counters or reset timestamps (e.g., attributes like requests_per_minute,
requests_per_hour, requests_per_day and corresponding reset times or remaining
counters on self) and return headers matching that window: set Retry-After to
the actual seconds until that window resets, X-RateLimit-Limit to the correct
window limit, X-RateLimit-Remaining to the remaining for that window (usually
0), and X-RateLimit-Reset to the epoch second when that window will reset;
modify get_rate_limit_response to choose the longest blocking window when
multiple quotas are exceeded.

Comment on lines +62 to +64
def _login_validator(username: str, password: str):
"""Test validator; always returns None (tests use _make_token for tokens)."""
return None
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Prefix the unused params and annotate the return type.

This helper will trip Ruff (ARG001) as written, and leaving the return type implicit is unnecessary friction for mypy --strict.

Suggested cleanup
-def _login_validator(username: str, password: str):
+def _login_validator(
+    _username: str, _password: str
+) -> dict[str, object] | None:
     """Test validator; always returns None (tests use _make_token for tokens)."""
     return None

As per coding guidelines, "Pass all ruff check linting before merge" and "Pass mypy --strict type checking before merge".

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def _login_validator(username: str, password: str):
"""Test validator; always returns None (tests use _make_token for tokens)."""
return None
def _login_validator(
_username: str, _password: str
) -> dict[str, object] | None:
"""Test validator; always returns None (tests use _make_token for tokens)."""
return None
🧰 Tools
🪛 Ruff (0.15.5)

[warning] 62-62: Unused function argument: username

(ARG001)


[warning] 62-62: Unused function argument: password

(ARG001)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/test_auth.py` around lines 62 - 64, The helper _login_validator
declares unused parameters and lacks a return annotation which trips Ruff ARG001
and mypy; update the signature of _login_validator to prefix the parameters with
underscores (e.g., _username, _password) to mark them intentionally unused and
add an explicit return type -> None so the function becomes def
_login_validator(_username: str, _password: str) -> None: returning None.

- Fix case-insensitive Basic auth header check
- Make rate limiter app-scoped and configurable
- Fix global login_validator breaking multi-app isolation
- Make VALID_JWT_ALGORITHMS immutable
- Validate per-endpoint jwt_algorithm before storing
- Fix duplicate auth routes in register()
- Fix rate limiter cleanup for hourly/daily buckets
- Improve IP header trust for rate limiting
- Fix rate limit response headers for correct window
- Validate login validator signature at load time
- Fix dict-form method auth dropping default JWT settings
- Fix test helper signatures and type annotations
- Update YAML documentation with jwt_algorithm
- Fix circular imports and linting issues
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
lightapi/auth.py (1)

22-44: ⚠️ Potential issue | 🟠 Major

BaseAuthentication and JWTAuthentication are marked frozen in repo rules.

This PR edits both class bodies directly. Please move these extensions behind a new module or adapter instead of changing the frozen implementations.

As per coding guidelines, "Do not modify lightapi/auth.py: JWTAuthentication and BaseAuthentication class bodies are FROZEN".

Also applies to: 61-156

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lightapi/auth.py` around lines 22 - 44, You modified frozen class bodies
BaseAuthentication and JWTAuthentication in lightapi/auth.py; revert those
changes and implement your behavior in a new adapter module (e.g., auth_adapter)
that subclasses or wraps BaseAuthentication/JWTAuthentication instead of editing
them. Create a new class (e.g., AdapterJWTAuthentication or
AuthenticationAdapter) that implements authenticate(request: Request) -> bool
and get_auth_error_response(request: Request) -> JSONResponse, delegate to the
original JWTAuthentication/BaseAuthentication for shared logic, and update call
sites to import and use the adapter class rather than the frozen classes. Ensure
you reference the original symbols BaseAuthentication, JWTAuthentication,
authenticate, and get_auth_error_response when locating where to delegate or
replace usage.
lightapi/lightapi.py (2)

593-596: ⚠️ Potential issue | 🟠 Major

Use the backend's auth-error response here.

This hard-coded 401 drops backend-specific behavior. In particular, Basic auth can never emit its WWW-Authenticate challenge while _check_auth() returns this generic JSON body instead.

🛠️ Possible fix
-        if not authenticator.authenticate(request):
-            return JSONResponse(
-                {"detail": "Authentication credentials invalid."}, status_code=401
-            )
+        if not authenticator.authenticate(request):
+            return authenticator.get_auth_error_response(request)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lightapi/lightapi.py` around lines 593 - 596, Replace the hard-coded
JSONResponse used when authentication fails so backend-specific behavior (e.g.,
WWW-Authenticate header for Basic auth) is preserved: instead of returning
JSONResponse({"detail": "Authentication credentials invalid."}, status_code=401)
in the block that checks authenticator.authenticate(request), call the
authenticator's auth-error response helper (e.g.,
authenticator.auth_error_response(request) or
authenticator.get_auth_error_response(request)) and return that response; if the
authenticator has no such method provide a fallback that preserves
headers/status from the backend (including WWW-Authenticate) rather than
emitting the static JSONResponse. Ensure this change is applied where
authenticator.authenticate(request) is tested (and in any related _check_auth()
flow).

56-90: ⚠️ Potential issue | 🟠 Major

Please move these auth changes out of the frozen legacy entrypoint.

This file is preserved for the __init__.py rewrite, so adding auth-route registration, limiter wiring, and _check_auth() changes here breaks the repo contract and makes the planned migration harder.

As per coding guidelines, "Do not modify lightapi/lightapi.py; it is preserved as legacy until init.py rewrite absorbs it".

Also applies to: 163-258, 553-596

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lightapi/lightapi.py` around lines 56 - 90, Revert the auth-related wiring
added inside the LightAPI constructor: remove any registration of auth routes,
rate-limiter setup/assignment to _auth_rate_limiter, and any modifications to or
calls of _check_auth in lightapi.py; keep only storing login_validator and
calling set_login_validator(login_validator) as before. Specifically undo
additions that introduce or use attributes _auth_path, _auth_rate_limiter,
auth-route registration code, and any new _check_auth logic in the LightAPI
__init__ (constructor) so this legacy entrypoint remains unchanged; move the
auth-route registration and limiter wiring into the new/init entrypoint where
auth is intended to be implemented. Ensure set_login_validator and
login_validator handling remain intact and nothing else related to routing or
rate-limiting is wired here.
♻️ Duplicate comments (2)
lightapi/_login.py (1)

57-59: ⚠️ Potential issue | 🟠 Major

Make this Basic-scheme guard case-insensitive too.

_parse_basic_header() already supports basic, but this pre-check still requires the exact Basic prefix. Lowercase variants fall through to body parsing and hit the wrong auth/error path.

🛠️ Possible fix
     auth_header = request.headers.get("Authorization")
-    if auth_header and auth_header.startswith("Basic "):
-        return _parse_basic_header(auth_header)
+    if auth_header:
+        parts = auth_header.split(None, 1)
+        if len(parts) == 2 and parts[0].lower() == "basic":
+            return _parse_basic_header(auth_header)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lightapi/_login.py` around lines 57 - 59, The Authorization header guard
currently checks for auth_header.startswith("Basic ") which is case-sensitive
and lets lowercase "basic " fall through; update the guard in the handler that
reads request.headers.get("Authorization") to perform a case-insensitive scheme
check (e.g., compare auth_header[:6].lower() == "basic " or lower()/startswith
on a lowercase copy) and still call _parse_basic_header(auth_header) when
matched so _parse_basic_header (which already accepts "basic") is used for any
case variant.
lightapi/lightapi.py (1)

183-188: ⚠️ Potential issue | 🟠 Major

Only auto-register /auth routes when login support is actually configured.

Right now any JWT/Basic backend triggers built-in login routes, a fixed limiter, and a startup failure when login_validator is absent. Basic auth really does need the validator, but JWT-only apps can validate externally issued tokens without it; this is also where the current ConfigurationError/429 test failures are coming from.

Also applies to: 190-194

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lightapi/lightapi.py` around lines 183 - 188, The current check using
auth_backends & {JWTAuthentication, BasicAuthentication} wrongly triggers
auto-registration of /auth routes, creation of self._auth_rate_limiter, and a
startup ConfigurationError even for JWT-only apps without a login_validator;
change the conditional logic so /auth routes and the RateLimiter
(self._auth_rate_limiter) are only created when login support is actually
configured: require either a provided login_validator or the presence of
BasicAuthentication (which must still raise ConfigurationError if
login_validator is missing), while JWT-only backends without login_validator
should skip route registration and not instantiate the limiter; update the
checks around auth_backends, login_validator, and the /auth registration code
paths to reflect this behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@lightapi/auth.py`:
- Around line 202-212: The 401 response from get_auth_error_response currently
lacks the WWW-Authenticate header required for HTTP Basic challenge; update
get_auth_error_response to return the JSONResponse with a WWW-Authenticate
header (e.g. 'Basic realm="Authentication Required"') so clients/browsers
trigger the Basic auth flow—use the JSONResponse headers parameter (or
equivalent on JSONResponse) to set 'WWW-Authenticate' and keep the existing body
and 401 status code.

In `@lightapi/config.py`:
- Around line 73-75: Validate and reject reserved JWT claim names in the
configuration so invalid settings fail at startup: in the config initialization
where jwt_extra_claims and jwt_expiration are set (the attribute
jwt_extra_claims), check the provided jwt_extra_claims against the reserved
claim set used by JWTAuthentication.generate_token (e.g., "exp", "iat", "nbf",
"iss", "aud", "sub", "jti"); if any reserved names are present, raise
ConfigurationError with a clear message identifying the offending claim(s) so
the error surfaces at config time rather than causing a 500 during
JWTAuthentication.generate_token().

In `@lightapi/rate_limiter.py`:
- Around line 192-194: The wrapper coroutine currently treats the tuple returned
by _auth_rate_limiter.is_rate_limited(...) as a boolean; unpack the return into
(is_limited, window) and branch on is_limited, e.g. call
_auth_rate_limiter.get_rate_limit_response(request, window) only when is_limited
is True so the wrapped endpoint can run when not rate limited; update the
wrapper signature where it calls is_rate_limited and pass the unpacked window
into get_rate_limit_response.

In `@lightapi/yaml_loader.py`:
- Around line 436-462: The merge loop currently carries backend and jwt_* fields
forward but never merges defaults.authentication.permission, so when
meta.methods is a dict and the default permission only exists in defaults the
permission_map remains empty and _check_auth() falls back to AllowAny; fix by
adding a merged_permission variable (e.g., merged_permission = None), populate
it from the same loop by checking source.permission, and then incorporate it
into the permission_map logic before creating the Authentication object (either
by using merged_permission as a fallback when permission_map is empty or by
merging keys from merged_permission into permission_map), then pass the
resulting permission_map into Authentication(permission=...) so
defaults.authentication.permission is honored.

---

Outside diff comments:
In `@lightapi/auth.py`:
- Around line 22-44: You modified frozen class bodies BaseAuthentication and
JWTAuthentication in lightapi/auth.py; revert those changes and implement your
behavior in a new adapter module (e.g., auth_adapter) that subclasses or wraps
BaseAuthentication/JWTAuthentication instead of editing them. Create a new class
(e.g., AdapterJWTAuthentication or AuthenticationAdapter) that implements
authenticate(request: Request) -> bool and get_auth_error_response(request:
Request) -> JSONResponse, delegate to the original
JWTAuthentication/BaseAuthentication for shared logic, and update call sites to
import and use the adapter class rather than the frozen classes. Ensure you
reference the original symbols BaseAuthentication, JWTAuthentication,
authenticate, and get_auth_error_response when locating where to delegate or
replace usage.

In `@lightapi/lightapi.py`:
- Around line 593-596: Replace the hard-coded JSONResponse used when
authentication fails so backend-specific behavior (e.g., WWW-Authenticate header
for Basic auth) is preserved: instead of returning JSONResponse({"detail":
"Authentication credentials invalid."}, status_code=401) in the block that
checks authenticator.authenticate(request), call the authenticator's auth-error
response helper (e.g., authenticator.auth_error_response(request) or
authenticator.get_auth_error_response(request)) and return that response; if the
authenticator has no such method provide a fallback that preserves
headers/status from the backend (including WWW-Authenticate) rather than
emitting the static JSONResponse. Ensure this change is applied where
authenticator.authenticate(request) is tested (and in any related _check_auth()
flow).
- Around line 56-90: Revert the auth-related wiring added inside the LightAPI
constructor: remove any registration of auth routes, rate-limiter
setup/assignment to _auth_rate_limiter, and any modifications to or calls of
_check_auth in lightapi.py; keep only storing login_validator and calling
set_login_validator(login_validator) as before. Specifically undo additions that
introduce or use attributes _auth_path, _auth_rate_limiter, auth-route
registration code, and any new _check_auth logic in the LightAPI __init__
(constructor) so this legacy entrypoint remains unchanged; move the auth-route
registration and limiter wiring into the new/init entrypoint where auth is
intended to be implemented. Ensure set_login_validator and login_validator
handling remain intact and nothing else related to routing or rate-limiting is
wired here.

---

Duplicate comments:
In `@lightapi/_login.py`:
- Around line 57-59: The Authorization header guard currently checks for
auth_header.startswith("Basic ") which is case-sensitive and lets lowercase
"basic " fall through; update the guard in the handler that reads
request.headers.get("Authorization") to perform a case-insensitive scheme check
(e.g., compare auth_header[:6].lower() == "basic " or lower()/startswith on a
lowercase copy) and still call _parse_basic_header(auth_header) when matched so
_parse_basic_header (which already accepts "basic") is used for any case
variant.

In `@lightapi/lightapi.py`:
- Around line 183-188: The current check using auth_backends &
{JWTAuthentication, BasicAuthentication} wrongly triggers auto-registration of
/auth routes, creation of self._auth_rate_limiter, and a startup
ConfigurationError even for JWT-only apps without a login_validator; change the
conditional logic so /auth routes and the RateLimiter (self._auth_rate_limiter)
are only created when login support is actually configured: require either a
provided login_validator or the presence of BasicAuthentication (which must
still raise ConfigurationError if login_validator is missing), while JWT-only
backends without login_validator should skip route registration and not
instantiate the limiter; update the checks around auth_backends,
login_validator, and the /auth registration code paths to reflect this behavior.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 9e212585-5b21-4714-ae79-59b3274dbf66

📥 Commits

Reviewing files that changed from the base of the PR and between 91cf264 and a92eb5d.

📒 Files selected for processing (9)
  • docs/examples/yaml-configuration.md
  • lightapi/_login.py
  • lightapi/auth.py
  • lightapi/config.py
  • lightapi/lightapi.py
  • lightapi/rate_limiter.py
  • lightapi/yaml_loader.py
  • tests/test_auth.py
  • tests/test_yaml_config.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • docs/examples/yaml-configuration.md

Comment on lines +192 to +194
async def wrapper(request: Request, *args: Any, **kwargs: Any) -> Any:
if _auth_rate_limiter.is_rate_limited(request, endpoint="auth"):
return _auth_rate_limiter.get_rate_limit_response(request)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
python3 - <<'PY'
result = (False, None)
print(f"bool({result!r}) = {bool(result)}")
PY

Repository: iklobato/lightapi

Length of output: 87


🏁 Script executed:

# First, find the file and check its size
fd -t f "rate_limiter.py" lightapi

# Get line count to determine how to read it
wc -l lightapi/rate_limiter.py 2>/dev/null || echo "File not found"

Repository: iklobato/lightapi

Length of output: 114


🏁 Script executed:

# Search for is_rate_limited method definition and the decorator usage around lines 192-194
rg "def is_rate_limited|rate_limit_auth_endpoint" lightapi/rate_limiter.py -B 2 -A 10

Repository: iklobato/lightapi

Length of output: 662


🏁 Script executed:

# Get the exact lines 192-194 and surrounding context
sed -n '185,205p' lightapi/rate_limiter.py

Repository: iklobato/lightapi

Length of output: 480


🏁 Script executed:

# Search for all usages of is_rate_limited to understand the return type
rg "is_rate_limited\(" lightapi/rate_limiter.py -B 1 -A 1

Repository: iklobato/lightapi

Length of output: 359


🏁 Script executed:

# Check if there are other usages of is_rate_limited that might show the correct pattern
rg "is_rate_limited" lightapi/rate_limiter.py -B 2 -A 2

# Also check if there are type hints or tests that might show the intended usage
fd -t f "test" lightapi/ --name "*rate*"

Repository: iklobato/lightapi

Length of output: 739


🏁 Script executed:

# Let's also see the full method to understand the return value structure
sed -n '67,95p' lightapi/rate_limiter.py

Repository: iklobato/lightapi

Length of output: 1038


🏁 Script executed:

# Let's see the full is_rate_limited method implementation and its return statement
sed -n '68,110p' lightapi/rate_limiter.py

Repository: iklobato/lightapi

Length of output: 1411


🏁 Script executed:

# Also check what get_rate_limit_response returns
rg "def get_rate_limit_response" lightapi/rate_limiter.py -A 10

Repository: iklobato/lightapi

Length of output: 479


🏁 Script executed:

# Let's check if is_rate_limited is used elsewhere and how
rg "is_rate_limited" . -A 2 -B 2

Repository: iklobato/lightapi

Length of output: 1517


🏁 Script executed:

# Also verify the exact line numbers around 192-194 once more to be absolutely sure
sed -n '190,196p' lightapi/rate_limiter.py

Repository: iklobato/lightapi

Length of output: 341


Unpack is_rate_limited() before branching.

is_rate_limited() returns tuple[bool, str | None], and a non-empty tuple is always truthy in Python—even (False, None) evaluates to True. As written, the condition is always true, so the decorated endpoint always returns 429 and the function never executes.

Unpack the tuple and branch on the first element, following the pattern used in lightapi/_login.py:

is_limited, window = _auth_rate_limiter.is_rate_limited(request, endpoint="auth")
if is_limited:
    return _auth_rate_limiter.get_rate_limit_response(request, window)

Note: The current call to get_rate_limit_response() also omits the window parameter that should be passed.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lightapi/rate_limiter.py` around lines 192 - 194, The wrapper coroutine
currently treats the tuple returned by _auth_rate_limiter.is_rate_limited(...)
as a boolean; unpack the return into (is_limited, window) and branch on
is_limited, e.g. call _auth_rate_limiter.get_rate_limit_response(request,
window) only when is_limited is True so the wrapped endpoint can run when not
rate limited; update the wrapper signature where it calls is_rate_limited and
pass the unpacked window into get_rate_limit_response.

- Add WWW-Authenticate header to Basic auth 401 response
- Add login_validator parameter to BasicAuthentication constructor
- Fix case-insensitive Basic auth header check
- Reject reserved claims in jwt_extra_claims at config time
- Fix rate limiter by removing unused global decorator
- Fix YAML dict-method permission merging
- Update imports and fix linting issues
@iklobato iklobato force-pushed the feat/improve-authentication-jwt-and-basic branch from 4b9e6d4 to 563976d Compare March 10, 2026 03:16
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

♻️ Duplicate comments (2)
lightapi/rate_limiter.py (2)

83-90: ⚠️ Potential issue | 🟠 Major

Don't fall back to forwarded headers without explicit proxy trust.

If request.client is unavailable, these headers become the bucket key even though they are client-controlled unless the app has trusted-proxy handling. That still leaves an auth-rate-limit bypass by rotating header values. Safer default is to keep a fixed fallback here and only parse forwarded headers behind an explicit trust_proxy / trusted_proxies setting.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lightapi/rate_limiter.py` around lines 83 - 90, The code falls back to
client-controlled forwarded headers when request.client is missing; change
get-client-ip logic to only parse X-Forwarded-For/X-Real-IP/X-Client-IP when an
explicit trust setting is enabled. Add/consume a trust_proxy (bool) or
trusted_proxies (list) flag on the rate limiter or calling context, check
request.client first, and if missing only inspect forwarded headers when
trust_proxy is true (or the parsed IP is validated against trusted_proxies);
otherwise keep the fixed fallback (e.g., "0.0.0.0"). Update the logic around
request.client and request.headers to gate header parsing on
trust_proxy/trusted_proxies.

118-130: ⚠️ Potential issue | 🟡 Minor

Return the window that actually keeps the client blocked.

is_rate_limited() stops at the first exceeded window, and get_rate_limit_response() replies with a full 60/3600/86400-second delay from “now”. If minute and hour/day are exhausted together, the response can tell clients to retry at the wrong time. Pick the window with the latest next-available timestamp and derive Retry-After / X-RateLimit-Reset from that bucket’s stored timestamps.

Also applies to: 139-167

🧹 Nitpick comments (2)
lightapi/auth.py (1)

187-190: Avoid catching bare Exception.

Catching bare Exception masks all errors including programming bugs. Consider catching specific exceptions that the validator might raise, or at minimum re-raise unexpected exceptions after logging.

Suggested approach
         try:
             payload = validator(username, password)
-        except Exception:
+        except (ValueError, TypeError, LookupError) as exc:
+            # Log the error for debugging if needed
             return False

Alternatively, if the validator contract is well-defined, document what exceptions it may raise and catch only those.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lightapi/auth.py` around lines 187 - 190, The try/except around calling
validator(username, password) is catching bare Exception which masks programming
errors; update the block in which validator is invoked (the call that assigns
payload) to catch only the specific exceptions the validator can raise (e.g.,
ValidationError or AuthError) or catch Exception as e, log the unexpected error,
and re-raise it—ensure you reference the validator function and the variables
username, password and payload so the handler only swallows known validation
errors and does not hide other exceptions.
lightapi/yaml_loader.py (1)

113-117: Silently returning uninspectable callables may cause runtime failures.

When inspect.signature(fn) raises ValueError for builtins or C extensions, the function returns fn without validating arity. If such a callable has wrong arity, the first login attempt will fail with a confusing error instead of failing at config time.

Consider either documenting this limitation or raising a warning/error when signature inspection fails.

Suggested approach
     try:
         sig = inspect.signature(fn)
     except ValueError:
         # Some callables (e.g., builtins) don't have inspectable signatures
-        return fn
+        import warnings
+        warnings.warn(
+            f"Cannot validate signature of login_validator '{dotted_path}'. "
+            "Ensure it accepts (username, password) arguments.",
+            stacklevel=2,
+        )
+        return fn
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lightapi/yaml_loader.py` around lines 113 - 117, The code currently swallows
inspect.signature(fn) ValueError and returns fn, which defers arity errors to
runtime; change this to fail-fast by replacing the bare return fn in the except
ValueError block with a clear failure or warning: either raise a descriptive
TypeError/ValueError (including the callable name and expected arity) so config
loading fails immediately, or emit warnings.warn(...) with the same descriptive
message so the user is alerted at load time; reference inspect.signature(fn),
the except ValueError handler, and the current return fn to locate and update
the logic.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@lightapi/auth.py`:
- Line 2: Remove the unused symbol Any from the typing import in
lightapi/auth.py to resolve the F401 lint error; update the import line that
currently reads "from typing import Any, Dict, Optional" to only import the
actually used names (Dict and Optional) so the file no longer imports unused
Any.

In `@lightapi/rate_limiter.py`:
- Around line 139-140: The get_rate_limit_response function currently has an
unused parameter named request which triggers Ruff ARG002; rename the parameter
to _request (i.e., change the signature in get_rate_limit_response to use
_request: Request) to mark it as intentionally unused, and update any internal
references (there should be none) and call sites if they rely on named
args—otherwise no further changes are needed.
- Around line 29-34: The in-memory per-process counters in RateLimiter (the
_store attribute in rate_limiter.py) are unsuitable for auth throttling because
each worker gets its own RateLimiter instance, multiplying attacker budgets;
replace the process-local _store with a shared backend (e.g., Redis) and use
atomic/increment and expiry operations for windowed counters, or explicitly
document and enforce single-process use. Concretely: modify RateLimiter to
accept a pluggable storage client (e.g., redis_client) instead of _store,
implement increment/get/cleanup via Redis commands (INCR, EXPIRE, HINCRBY/HGET,
or Lua for multi-key atomicity) and update the code paths that instantiate
RateLimiter so they pass a shared redis client; alternatively, if you cannot add
shared storage now, add clear docs and runtime guard in RateLimiter to refuse
use unless explicitly enabled for single-process mode.
- Around line 118-135: The loop over windows currently updates per-window
counters as it iterates, so a request that is later blocked still increments
earlier window buckets (see the loop using for window_name, limit in windows and
the writes to self._store[client_ip][window_key][current_time]); change the
logic to first check all windows for violations without mutating state
(calculate counts for each window using self._store reads), and only if no
window would block, perform the writes for all relevant window_keys (i.e., move
the self._store[...] assignment out of the initial loop and execute it once
after all checks pass); this prevents rejected requests from consuming earlier
buckets and preserves correct reporting of the blocking window.

---

Duplicate comments:
In `@lightapi/rate_limiter.py`:
- Around line 83-90: The code falls back to client-controlled forwarded headers
when request.client is missing; change get-client-ip logic to only parse
X-Forwarded-For/X-Real-IP/X-Client-IP when an explicit trust setting is enabled.
Add/consume a trust_proxy (bool) or trusted_proxies (list) flag on the rate
limiter or calling context, check request.client first, and if missing only
inspect forwarded headers when trust_proxy is true (or the parsed IP is
validated against trusted_proxies); otherwise keep the fixed fallback (e.g.,
"0.0.0.0"). Update the logic around request.client and request.headers to gate
header parsing on trust_proxy/trusted_proxies.

---

Nitpick comments:
In `@lightapi/auth.py`:
- Around line 187-190: The try/except around calling validator(username,
password) is catching bare Exception which masks programming errors; update the
block in which validator is invoked (the call that assigns payload) to catch
only the specific exceptions the validator can raise (e.g., ValidationError or
AuthError) or catch Exception as e, log the unexpected error, and re-raise
it—ensure you reference the validator function and the variables username,
password and payload so the handler only swallows known validation errors and
does not hide other exceptions.

In `@lightapi/yaml_loader.py`:
- Around line 113-117: The code currently swallows inspect.signature(fn)
ValueError and returns fn, which defers arity errors to runtime; change this to
fail-fast by replacing the bare return fn in the except ValueError block with a
clear failure or warning: either raise a descriptive TypeError/ValueError
(including the callable name and expected arity) so config loading fails
immediately, or emit warnings.warn(...) with the same descriptive message so the
user is alerted at load time; reference inspect.signature(fn), the except
ValueError handler, and the current return fn to locate and update the logic.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 8bdab05a-3741-468f-889c-d4ba1cddf54a

📥 Commits

Reviewing files that changed from the base of the PR and between a92eb5d and 4b9e6d4.

📒 Files selected for processing (4)
  • lightapi/auth.py
  • lightapi/config.py
  • lightapi/rate_limiter.py
  • lightapi/yaml_loader.py

@@ -1,10 +1,11 @@
from datetime import datetime, timedelta
from typing import Dict, Optional
from typing import Any, Dict, Optional
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Remove unused import to fix pipeline failure.

Any is imported but not used anywhere in the file, causing the F401 linting error in the pipeline.

Proposed fix
-from typing import Any, Dict, Optional
+from typing import Dict, Optional
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
from typing import Any, Dict, Optional
from typing import Dict, Optional
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lightapi/auth.py` at line 2, Remove the unused symbol Any from the typing
import in lightapi/auth.py to resolve the F401 lint error; update the import
line that currently reads "from typing import Any, Dict, Optional" to only
import the actually used names (Dict and Optional) so the file no longer imports
unused Any.

Comment on lines +118 to +135
for window_name, limit in windows:
window_seconds = self._get_window_seconds(window_name)
window_key = f"{endpoint}:{window_name}" if endpoint else window_name

# Count requests in this window
count = 0
for timestamp, request_count in self._store[client_ip][window_key].items():
if current_time - timestamp < window_seconds:
count += request_count

if count >= limit:
# Don't count this request since it's being blocked
return (True, window_name)

# Add current request
self._store[client_ip][window_key][current_time] = (
self._store[client_ip][window_key].get(current_time, 0) + 1
)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Blocked requests are still consuming earlier buckets.

The method increments minute before checking hour, and increments minute/hour before checking day. So a request rejected by a later window has already been recorded in earlier ones, which burns quota for traffic that never ran and can change which window gets reported next.

Suggested fix
-        for window_name, limit in windows:
+        window_keys: list[tuple[str, str]] = []
+        blocked_window: str | None = None
+
+        for window_name, limit in windows:
             window_seconds = self._get_window_seconds(window_name)
             window_key = f"{endpoint}:{window_name}" if endpoint else window_name
+            window_keys.append((window_name, window_key))
 
             # Count requests in this window
             count = 0
             for timestamp, request_count in self._store[client_ip][window_key].items():
                 if current_time - timestamp < window_seconds:
                     count += request_count
 
             if count >= limit:
-                # Don't count this request since it's being blocked
-                return (True, window_name)
-
-            # Add current request
-            self._store[client_ip][window_key][current_time] = (
-                self._store[client_ip][window_key].get(current_time, 0) + 1
-            )
+                blocked_window = window_name
+                break
+
+        if blocked_window is not None:
+            return (True, blocked_window)
+
+        for _, window_key in window_keys:
+            self._store[client_ip][window_key][current_time] = (
+                self._store[client_ip][window_key].get(current_time, 0) + 1
+            )
 
         return (False, None)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lightapi/rate_limiter.py` around lines 118 - 135, The loop over windows
currently updates per-window counters as it iterates, so a request that is later
blocked still increments earlier window buckets (see the loop using for
window_name, limit in windows and the writes to
self._store[client_ip][window_key][current_time]); change the logic to first
check all windows for violations without mutating state (calculate counts for
each window using self._store reads), and only if no window would block, perform
the writes for all relevant window_keys (i.e., move the self._store[...]
assignment out of the initial loop and execute it once after all checks pass);
this prevents rejected requests from consuming earlier buckets and preserves
correct reporting of the blocking window.

Comment on lines +139 to +140
def get_rate_limit_response(
self, request: Request, window: str = "minute"
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Rename the unused request argument to satisfy Ruff.

request is never read here, so ARG002 will keep this file from meeting the repo’s lint gate. Rename it to _request or drop it if you can change the call sites. As per coding guidelines "Pass all ruff check linting before merge".

🧰 Tools
🪛 Ruff (0.15.5)

[warning] 140-140: Unused method argument: request

(ARG002)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lightapi/rate_limiter.py` around lines 139 - 140, The get_rate_limit_response
function currently has an unused parameter named request which triggers Ruff
ARG002; rename the parameter to _request (i.e., change the signature in
get_rate_limit_response to use _request: Request) to mark it as intentionally
unused, and update any internal references (there should be none) and call sites
if they rely on named args—otherwise no further changes are needed.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
lightapi/auth.py (1)

1-163: ⚠️ Potential issue | 🔴 Critical

Critical: JWTAuthentication class is missing, breaking imports in dependent modules.

The class is imported by:

  • lightapi/lightapi.py
  • lightapi/_login.py
  • Multiple test files

Per the coding guidelines, JWTAuthentication class is FROZEN and must not be removed. It must be restored to fix the build failure.

Additionally, the following issues must be resolved before merge:

  1. Dead code: Lines 109–141 contain unreachable code after the return statement on line 107. Remove the old basic auth parsing logic that duplicates the authenticate method.

  2. Ruff linting failures (8 errors, all must pass):

    • ARG001/ARG002: Remove unused self/request parameters in BaseAuthentication.authenticate() (line 20), module-level get_auth_error_response() (line 33), and AllowAny.has_permission() (line 147).
    • F811: Remove duplicate from lightapi._registry import get_login_validator at lines 80 and 126; use the module-level import (line 8).
    • BLE001: Replace blind except Exception at lines 88 and 134 with specific exception types.
  3. Structural issue: The module-level get_auth_error_response() function (lines 33–47) is defined outside any class but has a self parameter. Either move it into a class or remove the self parameter.

♻️ Duplicate comments (1)
lightapi/rate_limiter.py (1)

118-135: ⚠️ Potential issue | 🟠 Major

Blocked requests still consume earlier buckets.

The loop increments counters for earlier windows (e.g., minute) before checking later windows (e.g., hour, day). If a request is blocked by the hour limit, it has already been counted in the minute bucket, burning quota for traffic that never executed.

Suggested fix: check all windows first, then increment
+        # First pass: check all windows without incrementing
+        window_keys: list[tuple[str, str, int]] = []
         for window_name, limit in windows:
             window_seconds = self._get_window_seconds(window_name)
             window_key = f"{endpoint}:{window_name}" if endpoint else window_name
+            window_keys.append((window_name, window_key, window_seconds))
 
             # Count requests in this window
             count = 0
-            for timestamp, request_count in self._store[client_ip][window_key].items():
+            for ts, req_count in self._store[client_ip][window_key].items():
-                if current_time - timestamp < window_seconds:
-                    count += request_count
+                if current_time - ts < window_seconds:
+                    count += req_count
 
             if count >= limit:
-                # Don't count this request since it's being blocked
                 return (True, window_name)
 
-            # Add current request
-            self._store[client_ip][window_key][current_time] = (
-                self._store[client_ip][window_key].get(current_time, 0) + 1
-            )
+        # Second pass: increment all windows (only reached if not blocked)
+        for _, window_key, _ in window_keys:
+            self._store[client_ip][window_key][current_time] = (
+                self._store[client_ip][window_key].get(current_time, 0) + 1
+            )
 
         return (False, None)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lightapi/rate_limiter.py` around lines 118 - 135, The code currently counts
and increments each window inside the for loop (using windows,
_get_window_seconds, window_key, self._store[client_ip][window_key]) which can
burn quota in earlier buckets even if a later window blocks the request; change
the logic in the method to first iterate over all windows to compute counts and
determine if any window would block (collecting counts per window but not
mutating self._store), returning (True, window_name) immediately if any limit is
exceeded, and only if all windows pass perform a second pass to increment
self._store[client_ip][window_key][current_time] for each window (preserve use
of current_time, client_ip, endpoint and existing keys).
🧹 Nitpick comments (2)
lightapi/config.py (1)

77-77: Consider extracting RESERVED_CLAIMS as a class constant.

The RESERVED_CLAIMS set is defined inline within __init__. If this set is needed elsewhere (e.g., for documentation or other validation), consider promoting it to a class-level constant on Authentication or _Config.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lightapi/config.py` at line 77, Move the inline RESERVED_CLAIMS set out of
the Authentication.__init__ and promote it to a class-level constant (e.g.,
Authentication.RESERVED_CLAIMS or _Config.RESERVED_CLAIMS) so it’s reusable;
update any references in __init__ and other methods to use the new class
constant name (RESERVED_CLAIMS) instead of the local variable, and ensure the
constant is documented with a brief docstring/comment near the class definition.
lightapi/auth.py (1)

20-20: Consider renaming unused request parameter in BaseAuthentication.authenticate.

The base implementation doesn't use request, which triggers Ruff ARG002. Rename to _request to indicate intentional non-use.

-    def authenticate(self, request: Request) -> bool:
+    def authenticate(self, _request: Request) -> bool:
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lightapi/auth.py` at line 20, Rename the unused parameter in
BaseAuthentication.authenticate from request to _request to signal intentional
non-use and satisfy Ruff ARG002; update the method signature in the
BaseAuthentication class (authenticate) accordingly and ensure any internal
references are adjusted (there should be none), leaving subclass interfaces
unchanged if they override authenticate with a used request parameter.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@lightapi/auth.py`:
- Line 80: Remove the redundant local import of get_login_validator in
lightapi/auth.py (the duplicate import shown in the diff) — rely on the
module-level import already present (get_login_validator) instead; delete the
local "from lightapi._registry import get_login_validator" and verify no local
variable or function redefines get_login_validator to avoid the Ruff F811
redefinition error.
- Around line 33-47: Remove the incorrect module-level function
get_auth_error_response that accepts a self parameter (it's
misplaced/duplicated), and ensure the class method
BasicAuthentication.get_auth_error_response is the single implementation; update
BasicAuthentication.get_auth_error_response to add the header
{"WWW-Authenticate": 'Basic realm="Restricted Area"'} to its JSONResponse so
behavior is consistent, and delete the standalone/duplicate module-level
get_auth_error_response definition.
- Around line 109-141: Remove the duplicated Basic auth block (the second copy
starting with auth_header = request.headers.get("Authorization") through the
final return True) so only the original authentication logic remains; locate and
delete the redundant code in the auth function where the block repeats the same
decode/validator/payload steps (search for the duplicate sequence of auth_header
extraction, base64 decoding, username/password split, get_login_validator and
request.state.user assignment) and leave the first, correct implementation
intact.

---

Duplicate comments:
In `@lightapi/rate_limiter.py`:
- Around line 118-135: The code currently counts and increments each window
inside the for loop (using windows, _get_window_seconds, window_key,
self._store[client_ip][window_key]) which can burn quota in earlier buckets even
if a later window blocks the request; change the logic in the method to first
iterate over all windows to compute counts and determine if any window would
block (collecting counts per window but not mutating self._store), returning
(True, window_name) immediately if any limit is exceeded, and only if all
windows pass perform a second pass to increment
self._store[client_ip][window_key][current_time] for each window (preserve use
of current_time, client_ip, endpoint and existing keys).

---

Nitpick comments:
In `@lightapi/auth.py`:
- Line 20: Rename the unused parameter in BaseAuthentication.authenticate from
request to _request to signal intentional non-use and satisfy Ruff ARG002;
update the method signature in the BaseAuthentication class (authenticate)
accordingly and ensure any internal references are adjusted (there should be
none), leaving subclass interfaces unchanged if they override authenticate with
a used request parameter.

In `@lightapi/config.py`:
- Line 77: Move the inline RESERVED_CLAIMS set out of the
Authentication.__init__ and promote it to a class-level constant (e.g.,
Authentication.RESERVED_CLAIMS or _Config.RESERVED_CLAIMS) so it’s reusable;
update any references in __init__ and other methods to use the new class
constant name (RESERVED_CLAIMS) instead of the local variable, and ensure the
constant is documented with a brief docstring/comment near the class definition.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 5178939c-6682-469a-bd85-c867c0a2748d

📥 Commits

Reviewing files that changed from the base of the PR and between 4b9e6d4 and 563976d.

📒 Files selected for processing (4)
  • lightapi/auth.py
  • lightapi/config.py
  • lightapi/rate_limiter.py
  • lightapi/yaml_loader.py

Comment on lines +33 to +47
def get_auth_error_response(self, request: Request) -> JSONResponse:
"""
Get the response to return when authentication fails.

Args:
request: The HTTP request object.

class JWTAuthentication(BaseAuthentication):
Returns:
Response object for authentication error.
"""
JWT (JSON Web Token) based authentication.
return JSONResponse(
{"error": "authentication failed"},
status_code=401,
headers={"WWW-Authenticate": 'Basic realm="Restricted Area"'},
)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Function get_auth_error_response has incorrect signature and placement.

This is defined as a module-level function but has a self parameter, which makes no sense. It should either be:

  1. A method inside BasicAuthentication (removing the duplicate at lines 97-107), or
  2. A standalone function without self

Additionally, this function includes WWW-Authenticate: Basic header, but there's another get_auth_error_response method in BasicAuthentication (lines 97-107) that does NOT include this header. This inconsistency needs to be resolved.

Suggested fix: Remove the module-level function
-def get_auth_error_response(self, request: Request) -> JSONResponse:
-    """
-    Get the response to return when authentication fails.
-
-    Args:
-        request: The HTTP request object.
-
-    Returns:
-        Response object for authentication error.
-    """
-    return JSONResponse(
-        {"error": "authentication failed"},
-        status_code=401,
-        headers={"WWW-Authenticate": 'Basic realm="Restricted Area"'},
-    )

Then update BasicAuthentication.get_auth_error_response to include the header:

     def get_auth_error_response(self, request: Request) -> JSONResponse:
-        return JSONResponse({"error": "authentication failed"}, status_code=401)
+        return JSONResponse(
+            {"error": "authentication failed"},
+            status_code=401,
+            headers={"WWW-Authenticate": 'Basic realm="lightapi"'},
+        )
🧰 Tools
🪛 Ruff (0.15.5)

[warning] 33-33: Unused function argument: self

(ARG001)


[warning] 33-33: Unused function argument: request

(ARG001)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lightapi/auth.py` around lines 33 - 47, Remove the incorrect module-level
function get_auth_error_response that accepts a self parameter (it's
misplaced/duplicated), and ensure the class method
BasicAuthentication.get_auth_error_response is the single implementation; update
BasicAuthentication.get_auth_error_response to add the header
{"WWW-Authenticate": 'Basic realm="Restricted Area"'} to its JSONResponse so
behavior is consistent, and delete the standalone/duplicate module-level
get_auth_error_response definition.

return False

username, password = credentials
from lightapi._registry import get_login_validator
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Remove redundant import of get_login_validator.

get_login_validator is already imported at the module level (line 8). The local import at line 80 triggers Ruff F811 (redefinition).

         username, password = credentials
-        from lightapi._registry import get_login_validator
-
         validator = self.login_validator or get_login_validator()
🧰 Tools
🪛 Ruff (0.15.5)

[error] 80-80: Redefinition of unused get_login_validator from line 8: get_login_validator redefined here

(F811)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lightapi/auth.py` at line 80, Remove the redundant local import of
get_login_validator in lightapi/auth.py (the duplicate import shown in the diff)
— rely on the module-level import already present (get_login_validator) instead;
delete the local "from lightapi._registry import get_login_validator" and verify
no local variable or function redefines get_login_validator to avoid the Ruff
F811 redefinition error.

Comment on lines +109 to +141
auth_header = request.headers.get("Authorization")
if not auth_header or not auth_header.lower().startswith("basic "):
return False

Returns:
dict: The decoded token payload.
try:
import base64

Raises:
jwt.InvalidTokenError: If the token is invalid or expired.
"""
return jwt.decode(token, self.secret_key, algorithms=[self.algorithm])
token = auth_header.split(" ", 1)[1]
decoded = base64.b64decode(token).decode("utf-8")
except (ValueError, IndexError, UnicodeDecodeError):
return False

parts = decoded.split(":", 1)
if len(parts) != 2:
return False

username, password = parts[0], parts[1]
from lightapi._registry import get_login_validator

validator = get_login_validator()
if validator is None:
return False

try:
payload = validator(username, password)
except Exception:
return False

if payload is None:
return False

request.state.user = payload
return True
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Dead code after return statement.

Lines 109-141 contain unreachable code that duplicates the authentication logic already present in lines 64-95. This appears to be leftover from a merge conflict or copy-paste error.

Remove the dead code
         return JSONResponse({"error": "authentication failed"}, status_code=401)
-
-        auth_header = request.headers.get("Authorization")
-        if not auth_header or not auth_header.lower().startswith("basic "):
-            return False
-
-        try:
-            import base64
-
-            token = auth_header.split(" ", 1)[1]
-            decoded = base64.b64decode(token).decode("utf-8")
-        except (ValueError, IndexError, UnicodeDecodeError):
-            return False
-
-        parts = decoded.split(":", 1)
-        if len(parts) != 2:
-            return False
-
-        username, password = parts[0], parts[1]
-        from lightapi._registry import get_login_validator
-
-        validator = get_login_validator()
-        if validator is None:
-            return False
-
-        try:
-            payload = validator(username, password)
-        except Exception:
-            return False
-
-        if payload is None:
-            return False
-
-        request.state.user = payload
-        return True
🧰 Tools
🪛 Ruff (0.15.5)

[error] 126-126: Redefinition of unused get_login_validator from line 8: get_login_validator redefined here

(F811)


[warning] 134-134: Do not catch blind exception: Exception

(BLE001)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lightapi/auth.py` around lines 109 - 141, Remove the duplicated Basic auth
block (the second copy starting with auth_header =
request.headers.get("Authorization") through the final return True) so only the
original authentication logic remains; locate and delete the redundant code in
the auth function where the block repeats the same decode/validator/payload
steps (search for the duplicate sequence of auth_header extraction, base64
decoding, username/password split, get_login_validator and request.state.user
assignment) and leave the first, correct implementation intact.

@iklobato iklobato merged commit ea7e5bb into master Mar 10, 2026
2 of 6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant