Repository navigation
fix: stop leaking secrets through logs and session storage #206
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
7e539fb
445f44c
ad0c2ee
3fe1664
2a2c7aa
514f840
c8a01f5
70b2cb8
ddab53f
6bf860b
222b6a9
f724922
0564311
e1c1b73
d7d64c7
100274d
ee69be8
4e1ceb7
b515150
9619f94
07a60c8
9307506
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -3,6 +3,7 @@ | |||||||||||||||||||||||||||||||
| from fastapi.responses import RedirectResponse, HTMLResponse | ||||||||||||||||||||||||||||||||
| from kinde_sdk.core.framework.framework_interface import FrameworkInterface | ||||||||||||||||||||||||||||||||
| from kinde_sdk.auth.oauth import OAuth | ||||||||||||||||||||||||||||||||
| from kinde_sdk.core.exceptions import KindeLoginException, KindeTokenException | ||||||||||||||||||||||||||||||||
| from ..middleware.framework_middleware import FrameworkMiddleware | ||||||||||||||||||||||||||||||||
| import os | ||||||||||||||||||||||||||||||||
| import uuid | ||||||||||||||||||||||||||||||||
|
|
@@ -139,7 +140,6 @@ async def login(request: Request): | |||||||||||||||||||||||||||||||
| login_options['invitation_code'] = invitation_code | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| url = await self._oauth.login(login_options) | ||||||||||||||||||||||||||||||||
| self._logger.warning(f"[Login] Session is: {request.session}") | ||||||||||||||||||||||||||||||||
| return RedirectResponse(url=url) | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| # Callback route | ||||||||||||||||||||||||||||||||
|
|
@@ -182,17 +182,32 @@ async def callback(request: Request, code: Optional[str] = None, state: Optional | |||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| return RedirectResponse(login_url) | ||||||||||||||||||||||||||||||||
| except Exception as ex: | ||||||||||||||||||||||||||||||||
| return HTMLResponse(f"Error parsing reauth state: {str(ex)}", status_code=400) | ||||||||||||||||||||||||||||||||
| self._logger.warning("Invalid reauth state: %s", type(ex).__name__) | ||||||||||||||||||||||||||||||||
| return HTMLResponse("Error parsing reauth state", status_code=400) | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| # Reject before handle_redirect, which would consume the pending state | ||||||||||||||||||||||||||||||||
| if not code: | ||||||||||||||||||||||||||||||||
| return HTMLResponse("Authentication failed: Missing authorization code", status_code=400) | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| user_id = request.session.get('user_id') or str(uuid.uuid4()) | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| # Exception messages can carry token-endpoint details, so they are | ||||||||||||||||||||||||||||||||
| # logged by type only and never returned to the browser | ||||||||||||||||||||||||||||||||
| try: | ||||||||||||||||||||||||||||||||
| assert self._oauth is not None | ||||||||||||||||||||||||||||||||
| await self._oauth.handle_redirect(code, user_id, state) | ||||||||||||||||||||||||||||||||
| except KindeLoginException: | ||||||||||||||||||||||||||||||||
| # Missing, forged or stale callbacks are client errors, as in the Flask route | ||||||||||||||||||||||||||||||||
| self._logger.warning("OAuth callback rejected") | ||||||||||||||||||||||||||||||||
| return HTMLResponse("Authentication failed: invalid or expired login request", status_code=400) | ||||||||||||||||||||||||||||||||
| except KindeTokenException: | ||||||||||||||||||||||||||||||||
| # Kinde rejected the authorization code; the message is already logged by type and status | ||||||||||||||||||||||||||||||||
| self._logger.warning("OAuth callback token exchange failed") | ||||||||||||||||||||||||||||||||
| return HTMLResponse("Authentication failed", status_code=400) | ||||||||||||||||||||||||||||||||
| except Exception as e: | ||||||||||||||||||||||||||||||||
| if "State not found" in str(e): | ||||||||||||||||||||||||||||||||
| return HTMLResponse("Error: State not found. Please check Kinde Python SDK documentation.\n" + str(e), status_code=500) | ||||||||||||||||||||||||||||||||
| raise e | ||||||||||||||||||||||||||||||||
| # Anything else is a server-side fault, not a bad request | ||||||||||||||||||||||||||||||||
| self._logger.error("OAuth callback failed: %s", type(e).__name__) | ||||||||||||||||||||||||||||||||
| return HTMLResponse("Authentication failed", status_code=500) | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| request.session['user_id'] = user_id | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
|
|
@@ -206,23 +221,28 @@ async def callback(request: Request, code: Optional[str] = None, state: Optional | |||||||||||||||||||||||||||||||
| if not post_login_redirect.startswith('http'): | ||||||||||||||||||||||||||||||||
| post_login_redirect = str(request.base_url).rstrip('/') + post_login_redirect | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| parsed = urlparse(post_login_redirect) | ||||||||||||||||||||||||||||||||
| if state: | ||||||||||||||||||||||||||||||||
| query_dict = parse_qs(parsed.query) | ||||||||||||||||||||||||||||||||
| query_dict['state'] = [state] | ||||||||||||||||||||||||||||||||
| new_query = urlencode(query_dict, doseq=True) | ||||||||||||||||||||||||||||||||
| redirect_url = urlunparse((parsed.scheme, parsed.netloc, parsed.path, parsed.params, new_query, parsed.fragment)) | ||||||||||||||||||||||||||||||||
| else: | ||||||||||||||||||||||||||||||||
| redirect_url = post_login_redirect | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| return RedirectResponse(redirect_url) | ||||||||||||||||||||||||||||||||
| # The consumed OAuth state is not forwarded: it would only end up in | ||||||||||||||||||||||||||||||||
| # browser history, Referer headers and access logs | ||||||||||||||||||||||||||||||||
| return RedirectResponse(post_login_redirect) | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| # Logout route | ||||||||||||||||||||||||||||||||
| @self.app.get("/logout") | ||||||||||||||||||||||||||||||||
| async def logout(request: Request): | ||||||||||||||||||||||||||||||||
| """Logout the user and redirect to Kinde logout page.""" | ||||||||||||||||||||||||||||||||
| # Pass the user_id so the SDK also drops the server-side tokens | ||||||||||||||||||||||||||||||||
| user_id = request.session.get('user_id') | ||||||||||||||||||||||||||||||||
| # Log out before clearing the session: after a restart, or on another worker, | ||||||||||||||||||||||||||||||||
| # the SDK reads the user's tokens from the session to revoke them and to build | ||||||||||||||||||||||||||||||||
| # the logout URL | ||||||||||||||||||||||||||||||||
| try: | ||||||||||||||||||||||||||||||||
| logout_url = await self._oauth.logout(user_id) | ||||||||||||||||||||||||||||||||
| except Exception as e: | ||||||||||||||||||||||||||||||||
| # Still sign the user out here and at Kinde. Returning a response (not | ||||||||||||||||||||||||||||||||
| # raising) is what saves the cleared session to the cookie | ||||||||||||||||||||||||||||||||
| self._logger.error("Logout failed: %s", type(e).__name__) | ||||||||||||||||||||||||||||||||
| logout_url = await self._oauth.logout() | ||||||||||||||||||||||||||||||||
| request.session.clear() | ||||||||||||||||||||||||||||||||
| return RedirectResponse(url=await self._oauth.logout()) | ||||||||||||||||||||||||||||||||
| return RedirectResponse(url=logout_url) | ||||||||||||||||||||||||||||||||
|
Comment on lines
+234
to
+245
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Clear the session even when The route now calls Keep the new order, but clear the session in a Proposed fix
Committable suggestion
|
||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| # Register route | ||||||||||||||||||||||||||||||||
| @self.app.get("/register") | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
Uh oh!
There was an error while loading. Please reload this page.