Compare commits
2
Commits
ca5f7d7c61
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5a4408d577
|
||
|
|
6c76b8ac78
|
No files matched your search
@@ -13,6 +13,10 @@ 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
|
||||
|
||||
@@ -103,7 +103,12 @@ 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"`)
|
||||
(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.
|
||||
|
||||
The web API provides the following endpoints:
|
||||
|
||||
|
||||
+43
-8
@@ -18,6 +18,15 @@ 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"
|
||||
@@ -150,7 +159,7 @@ async def favico():
|
||||
|
||||
|
||||
@app.get("/login")
|
||||
async def login(request: Request):
|
||||
async def login(request: Request, redirect_uri: str | None = None):
|
||||
"""Redirect to Infomaniak OIDC login"""
|
||||
import secrets
|
||||
import urllib.parse
|
||||
@@ -194,20 +203,42 @@ async def login(request: Request):
|
||||
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("/?error=Invalid state parameter")
|
||||
response = RedirectResponse(
|
||||
f"{target_redirect}?error={urllib.parse.quote('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 = {
|
||||
@@ -225,14 +256,16 @@ 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"{frontend_url}?error={error_detail}")
|
||||
error_response = RedirectResponse(
|
||||
f"{target_redirect}?error={urllib.parse.quote(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"]
|
||||
@@ -248,19 +281,21 @@ 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
|
||||
frontend_url = os.environ.get("FRONTEND_URL", "/")
|
||||
response = RedirectResponse(f"{frontend_url}?error=User not authorized")
|
||||
response = RedirectResponse(
|
||||
f"{target_redirect}?error={urllib.parse.quote('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
|
||||
frontend_url = os.environ.get("FRONTEND_URL", "/")
|
||||
response = RedirectResponse(f"{frontend_url}#access_token={access_token}")
|
||||
response = RedirectResponse(f"{target_redirect}#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
|
||||
|
||||
|
||||
|
||||
Reference in new issue
Block a user