5 Commits
Author SHA1 Message Date
herel 6a959a01e6 build: Update dependencies and pre-commit hooks
Bump mypy to 2.3.1 and refresh all pre-commit hooks (ruff 0.16,
markdownlint 0.18). Fix issues surfaced by the new tooling:

- Use lowercase samesite cookie values (mypy 2.x)
- Remove no-op global declarations and address new ruff findings;
  ignore rules that would require behavior changes
- Add missing comma in AI system prompt (strings were concatenated)
- Wrap long README lines for MD013
2026-09-16 15:23:57 +02:00
herel ff369143d2 build: Upgrade dependencies to latest versions
Bump minimum versions for requests, typer, pypdf, rl-ai-tools,
fastapi and pyjwt, and update dev dependencies (mypy,
types-requests). Remove unused ipykernel from dev group.

Regenerate poetry.lock, re-export requirements.txt and sync
uv.lock accordingly. requirements.txt now also includes pyjwt
and its crypto dependencies which were missing from the
previous export.
2026-09-16 13:10:24 +02:00
herel e77c034082 chore: update .gitignore 2026-06-29 23:11:51 +02:00
herel 0615dc6421 fix: remove Cross-Account Data Leakage 2026-06-29 23:04:48 +02:00
herel 71bbb67cef chore: remove debugging prints 2026-06-29 23:03:48 +02:00
3 changed files with 9 additions and 53 deletions

No files matched your search

-4
View File
@@ -13,10 +13,6 @@ repos:
- id: check-yaml
- id: check-added-large-files
- id: detect-private-key
- repo: https://github.com/gitleaks/gitleaks
rev: v8.30.1
hooks:
- id: gitleaks
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.16.7
+1 -6
View File
@@ -103,12 +103,7 @@ To configure OIDC authentication, set the following environment variables:
- `CLIENT_SECRET`: Your OIDC client secret
- `REDIRECT_URI`: The redirect URI (default: `http://localhost:8000/callback`)
- `ALLOWED_USERS`: Comma-separated list of allowed email addresses
(e.g. `"user1@example.com,user2@example.com"`)
- `ALLOWED_REDIRECT_URIS`: Comma-separated list of redirect targets external
apps may request via `/login?redirect_uri=...` (default: `myice://callback`
for the mobile app). Only allowlisted targets are honored; the callback
redirects back to the requesting app with the access token in the URL
fragment instead of the web frontend.
(e.g., `"user1@example.com,user2@example.com"`)
The web API provides the following endpoints:
+8 -43
View File
@@ -18,15 +18,6 @@ CLIENT_SECRET = os.environ.get("CLIENT_SECRET")
if not CLIENT_SECRET:
raise RuntimeError("CLIENT_SECRET environment variable must be set")
REDIRECT_URI = os.environ.get("REDIRECT_URI", "http://localhost:8000/callback")
FRONTEND_URL = os.environ.get("FRONTEND_URL", "/")
# Redirect targets an external app may request via /login?redirect_uri=...
# (comma-separated). Exact-match allowlist to prevent open redirects and
# access_token leakage to arbitrary hosts.
ALLOWED_REDIRECT_URIS = tuple(
uri.strip()
for uri in os.environ.get("ALLOWED_REDIRECT_URIS", "myice://callback").split(",")
if uri.strip()
)
SECRET_KEY = os.environ.get("SECRET_KEY", "change_me_in_production")
AUTHORIZATION_ENDPOINT = "https://login.infomaniak.com/authorize"
TOKEN_ENDPOINT = "https://login.infomaniak.com/token"
@@ -159,7 +150,7 @@ async def favico():
@app.get("/login")
async def login(request: Request, redirect_uri: str | None = None):
async def login(request: Request):
"""Redirect to Infomaniak OIDC login"""
import secrets
import urllib.parse
@@ -203,42 +194,20 @@ async def login(request: Request, redirect_uri: str | None = None):
samesite="lax",
max_age=300,
)
# Preserve an app-specific redirect target (e.g. myice://callback for the
# mobile app) so /callback can send the user back to the requesting app.
if redirect_uri and redirect_uri in ALLOWED_REDIRECT_URIS:
response.set_cookie(
"oidc_target_redirect",
redirect_uri,
httponly=True,
secure=True,
samesite="lax",
max_age=300,
)
return response
@app.get("/callback")
async def callback(request: Request, code: str, state: str):
"""Handle OIDC callback and exchange code for token"""
import urllib.parse
expected_state = request.cookies.get("oidc_state")
code_verifier = request.cookies.get("oidc_verifier")
# Final redirect target requested by the calling app (e.g. myice://callback
# for the mobile app), falling back to the web frontend. The cookie value
# is re-validated against the allowlist to guard against cookie tossing.
target_redirect = request.cookies.get("oidc_target_redirect")
if target_redirect not in ALLOWED_REDIRECT_URIS:
target_redirect = FRONTEND_URL
# We should also delete state/nonce cookies once read
if not expected_state or state != expected_state:
response = RedirectResponse(
f"{target_redirect}?error={urllib.parse.quote('Invalid state parameter')}"
)
response = RedirectResponse("/?error=Invalid state parameter")
response.delete_cookie("oidc_state")
response.delete_cookie("oidc_nonce")
response.delete_cookie("oidc_verifier")
response.delete_cookie("oidc_target_redirect")
return response
# Exchange authorization code for access token
token_data = {
@@ -256,16 +225,14 @@ async def callback(request: Request, code: str, state: str):
# Log the error for debugging
logger.error("Token exchange failed: %s", token_response)
# Redirect back to frontend with error
frontend_url = os.environ.get("FRONTEND_URL", "/")
error_detail = token_response.get(
"error_description", "Failed to obtain access token"
)
error_response = RedirectResponse(
f"{target_redirect}?error={urllib.parse.quote(error_detail)}"
)
error_response = RedirectResponse(f"{frontend_url}?error={error_detail}")
error_response.delete_cookie("oidc_state")
error_response.delete_cookie("oidc_nonce")
error_response.delete_cookie("oidc_verifier")
error_response.delete_cookie("oidc_target_redirect")
return error_response
access_token = token_response["access_token"]
@@ -281,21 +248,19 @@ async def callback(request: Request, code: str, state: str):
user_email = userinfo.get("email")
if ALLOWED_USERS and user_email not in ALLOWED_USERS:
# Redirect back to frontend with error
response = RedirectResponse(
f"{target_redirect}?error={urllib.parse.quote('User not authorized')}"
)
frontend_url = os.environ.get("FRONTEND_URL", "/")
response = RedirectResponse(f"{frontend_url}?error=User not authorized")
response.delete_cookie("oidc_state")
response.delete_cookie("oidc_nonce")
response.delete_cookie("oidc_verifier")
response.delete_cookie("oidc_target_redirect")
return response
# Redirect back to frontend with access token and user info
response = RedirectResponse(f"{target_redirect}#access_token={access_token}")
frontend_url = os.environ.get("FRONTEND_URL", "/")
response = RedirectResponse(f"{frontend_url}#access_token={access_token}")
response.delete_cookie("oidc_state")
response.delete_cookie("oidc_nonce")
response.delete_cookie("oidc_verifier")
response.delete_cookie("oidc_target_redirect")
return response